Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 30 Jan 2014 16:36:04 -0500
changeset 182149 4fa639ef73d7ac9320a38daa90f8ee50900adc1c
parent 182148 f33534019590d1ec9f00851a05d577bf96f215d4 (current diff)
parent 182070 6f544aa66c1ac8fb80ccef6c4b4f0d25086e3b61 (diff)
child 182150 96ae326e19d0defee76e3b6e7d3bc382f588f3a5
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound.
b2g/app/b2g.js
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -573,16 +573,17 @@ pref("device.storage.enabled", true);
 // Enable pre-installed applications
 pref("dom.webapps.useCurrentProfile", true);
 
 // Enable system message
 pref("dom.sysmsg.enabled", true);
 pref("media.plugins.enabled", false);
 pref("media.omx.enabled", true);
 pref("media.rtsp.enabled", true);
+pref("media.rtsp.video.enabled", false);
 
 // Disable printing (particularly, window.print())
 pref("dom.disable_window_print", true);
 
 // Disable window.showModalDialog
 pref("dom.disable_window_showModalDialog", true);
 
 // Enable new experimental html forms
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <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="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <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="61866f17977a440a3297aa8ade8f410c68d2cdd7"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="09064f43116d1b965cb3ab6516fa0f1fa3c98a4c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eda08beb3ba9a159843c70ffde0f9660ec351eb9"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="87aa8679560ce09f6445621d6f370d9de722cdba"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="e33ea242b4328fb0d1824c951f379332b5021512"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0ff2eb11982b9d3c7a3333900cf0d3a5c0f77e32"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="317f25e0a4cb3e8e86e2b76c37a14081372f0307">
     <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="61866f17977a440a3297aa8ade8f410c68d2cdd7"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="09064f43116d1b965cb3ab6516fa0f1fa3c98a4c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="e33ea242b4328fb0d1824c951f379332b5021512"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0ff2eb11982b9d3c7a3333900cf0d3a5c0f77e32"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <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="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <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="61866f17977a440a3297aa8ade8f410c68d2cdd7"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="09064f43116d1b965cb3ab6516fa0f1fa3c98a4c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eda08beb3ba9a159843c70ffde0f9660ec351eb9"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="87aa8679560ce09f6445621d6f370d9de722cdba"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="e33ea242b4328fb0d1824c951f379332b5021512"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0ff2eb11982b9d3c7a3333900cf0d3a5c0f77e32"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "d369bbb3b7c415097c7c1fa303843cf5683cd843", 
+    "revision": "7240a5ab28eff83c26891c7a9141613149f226e9", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <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="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <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="61866f17977a440a3297aa8ade8f410c68d2cdd7"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="09064f43116d1b965cb3ab6516fa0f1fa3c98a4c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="e33ea242b4328fb0d1824c951f379332b5021512"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0ff2eb11982b9d3c7a3333900cf0d3a5c0f77e32"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -5,17 +5,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <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="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <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="61866f17977a440a3297aa8ade8f410c68d2cdd7"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="09064f43116d1b965cb3ab6516fa0f1fa3c98a4c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="e33ea242b4328fb0d1824c951f379332b5021512"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/inari/sources.xml
+++ b/b2g/config/inari/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <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="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <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="61866f17977a440a3297aa8ade8f410c68d2cdd7"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="09064f43116d1b965cb3ab6516fa0f1fa3c98a4c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="e33ea242b4328fb0d1824c951f379332b5021512"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0ff2eb11982b9d3c7a3333900cf0d3a5c0f77e32"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/b2g/config/leo/sources.xml
+++ b/b2g/config/leo/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <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="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <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="61866f17977a440a3297aa8ade8f410c68d2cdd7"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="09064f43116d1b965cb3ab6516fa0f1fa3c98a4c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="e33ea242b4328fb0d1824c951f379332b5021512"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0ff2eb11982b9d3c7a3333900cf0d3a5c0f77e32"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/config/mako/sources.xml
+++ b/b2g/config/mako/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="317f25e0a4cb3e8e86e2b76c37a14081372f0307">
     <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="61866f17977a440a3297aa8ade8f410c68d2cdd7"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="09064f43116d1b965cb3ab6516fa0f1fa3c98a4c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="e33ea242b4328fb0d1824c951f379332b5021512"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0ff2eb11982b9d3c7a3333900cf0d3a5c0f77e32"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <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="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <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="61866f17977a440a3297aa8ade8f410c68d2cdd7"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="09064f43116d1b965cb3ab6516fa0f1fa3c98a4c"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="e33ea242b4328fb0d1824c951f379332b5021512"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0ff2eb11982b9d3c7a3333900cf0d3a5c0f77e32"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -28,16 +28,19 @@ pref("browser.hiddenWindowChromeURL", "c
 pref("extensions.logging.enabled", false);
 
 // Disables strict compatibility, making addons compatible-by-default.
 pref("extensions.strictCompatibility", false);
 
 // Specifies a minimum maxVersion an addon needs to say it's compatible with
 // for it to be compatible by default.
 pref("extensions.minCompatibleAppVersion", "4.0");
+// Temporary preference to forcibly make themes more safe with Australis even if
+// extensions.checkCompatibility=false has been set.
+pref("extensions.checkCompatibility.temporaryThemeOverride_minAppVersion", "29.0a1");
 
 // Preferences for AMO integration
 pref("extensions.getAddons.cache.enabled", true);
 pref("extensions.getAddons.maxResults", 15);
 pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/guid:%IDS%?src=firefox&appOS=%OS%&appVersion=%VERSION%");
 pref("extensions.getAddons.getWithPerformance.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/guid:%IDS%?src=firefox&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
 pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/firefox/search?q=%TERMS%&platform=%OS%&appver=%VERSION%");
 pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%/%COMPATIBILITY_MODE%?src=firefox");
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -285,16 +285,24 @@ toolbarpaletteitem > #personal-bookmarks
 #urlbar-container {
   min-width: 28ch;
 }
 
 #search-container {
   min-width: 25ch;
 }
 
+#urlbar,
+.searchbar-textbox {
+  /* Setting a width and min-width to let the location & search bars maintain
+     a constant width in case they haven't be resized manually. (bug 965772) */
+  width: 1px;
+  min-width: 1px;
+}
+
 #main-window:-moz-lwtheme {
   background-repeat: no-repeat;
   background-position: top right;
 }
 
 %ifdef XP_MACOSX
 #main-window[inFullscreen="true"] {
   padding-top: 0; /* override drawintitlebar="true" */
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -166,40 +166,41 @@ const CustomizableWidgets = [{
 
       if (PlacesUIUtils.shouldEnableTabsFromOtherComputersMenuitem()) {
         tabsFromOtherComputers.removeAttribute("disabled");
       } else {
         tabsFromOtherComputers.setAttribute("disabled", true);
       }
 #endif
 
-      let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment(doc.defaultView, "toolbarbutton");
+      let utils = RecentlyClosedTabsAndWindowsMenuUtils;
+      let tabsFragment = utils.getTabsFragment(doc.defaultView, "toolbarbutton", true,
+                                               "menuRestoreAllTabsSubview.label");
       let separator = doc.getElementById("PanelUI-recentlyClosedTabs-separator");
       let elementCount = tabsFragment.childElementCount;
       separator.hidden = !elementCount;
       while (--elementCount >= 0) {
-        if (tabsFragment.children[elementCount].localName != "toolbarbutton")
-          continue;
-        tabsFragment.children[elementCount].setAttribute("class", "subviewbutton");
+        tabsFragment.children[elementCount].classList.add("subviewbutton");
       }
       recentlyClosedTabs.appendChild(tabsFragment);
 
-      let windowsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment(doc.defaultView, "toolbarbutton");
+      let windowsFragment = utils.getWindowsFragment(doc.defaultView, "toolbarbutton", true,
+                                                     "menuRestoreAllWindowsSubview.label");
       separator = doc.getElementById("PanelUI-recentlyClosedWindows-separator");
       elementCount = windowsFragment.childElementCount;
       separator.hidden = !elementCount;
       while (--elementCount >= 0) {
-        if (windowsFragment.children[elementCount].localName != "toolbarbutton")
-          continue;
-        windowsFragment.children[elementCount].setAttribute("class", "subviewbutton");
+        windowsFragment.children[elementCount].classList.add("subviewbutton");
       }
       recentlyClosedWindows.appendChild(windowsFragment);
+      aEvent.target.addEventListener("command", win.PanelUI);
     },
     onViewHiding: function(aEvent) {
       LOG("History view is being hidden!");
+      aEvent.target.removeEventListener("command", win.PanelUI);
     }
   }, {
     id: "privatebrowsing-button",
     shortcutId: "key_privatebrowsing",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(e) {
       if (e.target && e.target.ownerDocument && e.target.ownerDocument.defaultView) {
         let win = e.target.ownerDocument.defaultView;
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -1368,16 +1368,20 @@ CustomizeMode.prototype = {
     }
 
     let draggedItemId =
       aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
 
     let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
     draggedWrapper.hidden = false;
     draggedWrapper.removeAttribute("mousedown");
+    if (this._dragOverItem) {
+      this._cancelDragActive(this._dragOverItem);
+      this._dragOverItem = null;
+    }
     this._showPanelCustomizationPlaceholders();
   },
 
   _isUnwantedDragDrop: function(aEvent) {
     // The simulated events generated by synthesizeDragStart/synthesizeDrop in
     // mochitests are used only for testing whether the right data is being put
     // into the dataTransfer. Neither cause a real drop to occur, so they don't
     // set the source node. There isn't a means of testing real drag and drops,
--- a/browser/components/sessionstore/src/RecentlyClosedTabsAndWindowsMenuUtils.jsm
+++ b/browser/components/sessionstore/src/RecentlyClosedTabsAndWindowsMenuUtils.jsm
@@ -23,19 +23,25 @@ let ss = Cc["@mozilla.org/browser/sessio
 this.RecentlyClosedTabsAndWindowsMenuUtils = {
 
   /**
   * Builds up a document fragment of UI items for the recently closed tabs.
   * @param   aWindow
   *          The window that the tabs were closed in.
   * @param   aTagName
   *          The tag name that will be used when creating the UI items.
+  * @param   aPrefixRestoreAll (defaults to false)
+  *          Whether the 'restore all tabs' item is suffixed or prefixed to the list.
+  *          If suffixed (the default) a separator will be inserted before it.
+  * @param   aRestoreAllLabel (defaults to "menuRestoreAllTabs.label")
+  *          Which localizable string to use for the 'restore all tabs' item.
   * @returns A document fragment with UI items for each recently closed tab.
   */
-  getTabsFragment: function(aWindow, aTagName) {
+  getTabsFragment: function(aWindow, aTagName, aPrefixRestoreAll=false,
+                            aRestoreAllLabel="menuRestoreAllTabs.label") {
     let doc = aWindow.document;
     let fragment = doc.createDocumentFragment();
     if (ss.getClosedTabCount(aWindow) != 0) {
       let closedTabs = JSON.parse(ss.getClosedTabData(aWindow));
       for (let i = 0; i < closedTabs.length; i++) {
         let element = doc.createElementNS(kNSXUL, aTagName);
         element.setAttribute("label", closedTabs[i].title);
         if (closedTabs[i].image) {
@@ -57,34 +63,46 @@ this.RecentlyClosedTabsAndWindowsMenuUti
         }
 
         element.addEventListener("click", this._undoCloseMiddleClick, false);
         if (i == 0)
           element.setAttribute("key", "key_undoCloseTab");
         fragment.appendChild(element);
       }
 
-      fragment.appendChild(doc.createElementNS(kNSXUL, "menuseparator"));
-      let restoreAllTabs = fragment.appendChild(doc.createElementNS(kNSXUL, aTagName));
-      restoreAllTabs.setAttribute("label", navigatorBundle.GetStringFromName("menuRestoreAllTabs.label"));
+      let restoreAllTabs = doc.createElementNS(kNSXUL, aTagName);
+      restoreAllTabs.classList.add("restoreallitem");
+      restoreAllTabs.setAttribute("label", navigatorBundle.GetStringFromName(aRestoreAllLabel));
       restoreAllTabs.setAttribute("oncommand",
               "for (var i = 0; i < " + closedTabs.length + "; i++) undoCloseTab();");
+      if (!aPrefixRestoreAll) {
+        fragment.appendChild(doc.createElementNS(kNSXUL, "menuseparator"));
+        fragment.appendChild(restoreAllTabs);
+      } else {
+        fragment.insertBefore(restoreAllTabs, fragment.firstChild);
+      }
     }
     return fragment;
   },
 
   /**
   * Builds up a document fragment of UI items for the recently closed windows.
   * @param   aWindow
   *          A window that can be used to create the elements and document fragment.
   * @param   aTagName
   *          The tag name that will be used when creating the UI items.
+  * @param   aPrefixRestoreAll (defaults to false)
+  *          Whether the 'restore all windows' item is suffixed or prefixed to the list.
+  *          If suffixed (the default) a separator will be inserted before it.
+  * @param   aRestoreAllLabel (defaults to "menuRestoreAllWindows.label")
+  *          Which localizable string to use for the 'restore all windows' item.
   * @returns A document fragment with UI items for each recently closed window.
   */
-  getWindowsFragment: function(aWindow, aTagName) {
+  getWindowsFragment: function(aWindow, aTagName, aPrefixRestoreAll=false,
+                            aRestoreAllLabel="menuRestoreAllWindows.label") {
     let closedWindowData = JSON.parse(ss.getClosedWindowData());
     let fragment = aWindow.document.createDocumentFragment();
     if (closedWindowData.length != 0) {
       let menuLabelString = navigatorBundle.GetStringFromName("menuUndoCloseWindowLabel");
       let menuLabelStringSingleTab =
         navigatorBundle.GetStringFromName("menuUndoCloseWindowSingleTabLabel");
 
       let doc = aWindow.document;
@@ -113,21 +131,27 @@ this.RecentlyClosedTabsAndWindowsMenuUti
           item.setAttribute("targetURI", selectedTab.entries[activeIndex].url);
 
         if (i == 0)
           item.setAttribute("key", "key_undoCloseWindow");
         fragment.appendChild(item);
       }
 
       // "Open All in Windows"
-      fragment.appendChild(doc.createElementNS(kNSXUL, "menuseparator"));
-      let restoreAllWindows = fragment.appendChild(doc.createElementNS(kNSXUL, aTagName));
-      restoreAllWindows.setAttribute("label", navigatorBundle.GetStringFromName("menuRestoreAllWindows.label"));
+      let restoreAllWindows = doc.createElementNS(kNSXUL, aTagName);
+      restoreAllWindows.classList.add("restoreallitem");
+      restoreAllWindows.setAttribute("label", navigatorBundle.GetStringFromName(aRestoreAllLabel));
       restoreAllWindows.setAttribute("oncommand",
         "for (var i = 0; i < " + closedWindowData.length + "; i++) undoCloseWindow();");
+      if (!aPrefixRestoreAll) {
+        fragment.appendChild(doc.createElementNS(kNSXUL, "menuseparator"));
+        fragment.appendChild(restoreAllWindows);
+      } else {
+        fragment.insertBefore(restoreAllWindows, fragment.firstChild);
+      }
     }
     return fragment;
   },
 
 
   /**
     * Re-open a closed tab and put it to the end of the tab strip.
     * Used for a middle click.
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -830,17 +830,19 @@ StackFrames.prototype = {
     let { environment, where } = frame;
     if (!environment) {
       return;
     }
 
     // Don't change the editor's location if the execution was paused by a
     // public client evaluation. This is useful for adding overlays on
     // top of the editor, like a variable inspection popup.
-    if (this._currentFrameDescription != FRAME_TYPE.PUBLIC_CLIENT_EVAL) {
+    let isClientEval = this._currentFrameDescription == FRAME_TYPE.PUBLIC_CLIENT_EVAL;
+    let isPopupShown = DebuggerView.VariableBubble.contentsShown();
+    if (!isClientEval && !isPopupShown) {
       // Move the editor's caret to the proper url and line.
       DebuggerView.setEditorLocation(where.url, where.line);
       // Highlight the breakpoint at the specified url and line if it exists.
       DebuggerView.Sources.highlightBreakpoint(where, { noEditorUpdate: true });
     }
 
     // Don't display the watch expressions textbox inputs in the pane.
     DebuggerView.WatchExpressions.toggleContents(false);
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -1904,16 +1904,26 @@ VariableBubbleView.prototype = {
    * Hides the inspection popup.
    */
   hideContents: function() {
     clearNamedTimeout("editor-mouse-move");
     this._tooltip.hide();
   },
 
   /**
+   * Checks whether the inspection popup is shown.
+   *
+   * @return boolean
+   *         True if the panel is shown or showing, false otherwise.
+   */
+  contentsShown: function() {
+    return this._tooltip.isShown();
+  },
+
+  /**
    * Functions for getting customized variables view evaluation macros.
    *
    * @param string aPrefix
    *        See the corresponding VariablesView.* functions.
    */
   _getSimpleValueEvalMacro: function(aPrefix) {
     return (item, string) =>
       VariablesView.simpleValueEvalMacro(item, string, aPrefix);
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -238,16 +238,17 @@ support-files =
 [browser_dbg_variables-view-popup-02.js]
 [browser_dbg_variables-view-popup-03.js]
 [browser_dbg_variables-view-popup-04.js]
 [browser_dbg_variables-view-popup-05.js]
 [browser_dbg_variables-view-popup-06.js]
 [browser_dbg_variables-view-popup-07.js]
 [browser_dbg_variables-view-popup-08.js]
 [browser_dbg_variables-view-popup-09.js]
+[browser_dbg_variables-view-popup-10.js]
 [browser_dbg_variables-view-reexpand-01.js]
 [browser_dbg_variables-view-reexpand-02.js]
 [browser_dbg_variables-view-webidl.js]
 [browser_dbg_watch-expressions-01.js]
 [browser_dbg_watch-expressions-02.js]
 [browser_dbg_chrome-create.js]
 skip-if = os == "linux" # Bug 847558
 [browser_dbg_on-pause-raise.js]
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-popup-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-popup-03.js
@@ -15,25 +15,29 @@ function test() {
 
     // Allow this generator function to yield first.
     executeSoon(() => debuggee.start());
     yield waitForSourceAndCaretAndScopes(panel, ".html", 24);
 
     // Inspect variable.
     yield openVarPopup(panel, { line: 15, ch: 12 });
 
+    ok(bubble.contentsShown(),
+      "The variable should register as being shown.");
     ok(!bubble._tooltip.isEmpty(),
       "The variable inspection popup isn't empty.");
     ok(bubble._markedText,
       "There's some marked text in the editor.");
     ok(bubble._markedText.clear,
       "The marked text in the editor can be cleared.");
 
     yield hideVarPopup(panel);
 
+    ok(!bubble.contentsShown(),
+      "The variable should register as being hidden.");
     ok(bubble._tooltip.isEmpty(),
       "The variable inspection popup is now empty.");
     ok(!bubble._markedText,
       "The marked text in the editor was removed.");
 
     yield resumeDebuggerThenCloseAndFinish(panel);
   });
 }
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-popup-08.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-popup-08.js
@@ -45,22 +45,24 @@ function test() {
     yield waitForSourceAndCaretAndScopes(panel, ".html", 20);
     checkView(0, 20);
 
     // Inspect variable in topmost frame.
     yield openVarPopup(panel, { line: 18, ch: 12 });
     verifyContents("\"second scope\"", "token-string");
     checkView(0, 20);
 
-    // Change frame.
+    // Hide the popup and change the frame.
+    yield hideVarPopup(panel);
+
     let updatedFrame = waitForDebuggerEvents(panel, events.FETCHED_SCOPES);
     frames.selectedDepth = 1;
     yield updatedFrame;
     checkView(1, 15);
 
     // Inspect variable in oldest frame.
-    yield reopenVarPopup(panel, { line: 13, ch: 12 });
+    yield openVarPopup(panel, { line: 13, ch: 12 });
     verifyContents("\"first scope\"", "token-string");
     checkView(1, 15);
 
     yield resumeDebuggerThenCloseAndFinish(panel);
   });
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-popup-10.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Makes sure the source editor's scroll location doesn't change when
+ * a variable inspection popup is opened and a watch expression is
+ * also evaluated at the same time.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html";
+
+function test() {
+  Task.spawn(function() {
+    let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
+    let win = panel.panelWin;
+    let events = win.EVENTS;
+    let editor = win.DebuggerView.editor;
+    let editorContainer = win.document.getElementById("editor");
+    let bubble = win.DebuggerView.VariableBubble;
+    let expressions = win.DebuggerView.WatchExpressions;
+    let tooltip = bubble._tooltip.panel;
+
+    // Allow this generator function to yield first.
+    executeSoon(() => debuggee.start());
+    yield waitForSourceAndCaretAndScopes(panel, ".html", 24);
+
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    expressions.addExpression("this");
+    editor.focus();
+    yield expressionsEvaluated;
+
+    // Scroll to the top of the editor and inspect variables.
+    let breakpointScrollPosition = editor.getScrollInfo().top;
+    editor.setFirstVisibleLine(0);
+    let topmostScrollPosition = editor.getScrollInfo().top;
+
+    ok(topmostScrollPosition < breakpointScrollPosition,
+      "The editor is now scrolled to the top (0).");
+    is(editor.getFirstVisibleLine(), 0,
+      "The editor is now scrolled to the top (1).");
+
+    let failPopup = () => ok(false, "The popup has got unexpectedly hidden.");
+    let failScroll = () => ok(false, "The editor has got unexpectedly scrolled.");
+    tooltip.addEventListener("popuphiding", failPopup);
+    editorContainer.addEventListener("scroll", failScroll);
+    editor.on("scroll", () => {
+      if (editor.getScrollInfo().top > topmostScrollPosition) {
+        ok(false, "The editor scrolled back to the breakpoint location.");
+      }
+    });
+
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    yield openVarPopup(panel, { line: 14, ch: 15 });
+    yield expressionsEvaluated;
+
+    tooltip.removeEventListener("popuphiding", failPopup);
+    editorContainer.removeEventListener("scroll", failScroll);
+
+    yield resumeDebuggerThenCloseAndFinish(panel);
+  });
+}
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -544,21 +544,23 @@ InspectorPanel.prototype = {
   hideNodeMenu: function InspectorPanel_hideNodeMenu() {
     this.nodemenu.hidePopup();
   },
 
   /**
    * Disable the delete item if needed. Update the pseudo classes.
    */
   _setupNodeMenu: function InspectorPanel_setupNodeMenu() {
+    let isSelectionElement = this.selection.isElementNode();
+
     // Set the pseudo classes
     for (let name of ["hover", "active", "focus"]) {
       let menu = this.panelDoc.getElementById("node-menu-pseudo-" + name);
 
-      if (this.selection.isElementNode()) {
+      if (isSelectionElement) {
         let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
         menu.setAttribute("checked", checked);
         menu.removeAttribute("disabled");
       } else {
         menu.setAttribute("disabled", "true");
       }
     }
 
@@ -570,33 +572,44 @@ InspectorPanel.prototype = {
       deleteNode.removeAttribute("disabled");
     }
 
     // Disable / enable "Copy Unique Selector", "Copy inner HTML" &
     // "Copy outer HTML" as appropriate
     let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector");
     let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner");
     let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter");
-    let selectionIsElement = this.selection.isElementNode();
-    if (selectionIsElement) {
+    if (isSelectionElement) {
       unique.removeAttribute("disabled");
       copyInnerHTML.removeAttribute("disabled");
       copyOuterHTML.removeAttribute("disabled");
     } else {
       unique.setAttribute("disabled", "true");
       copyInnerHTML.setAttribute("disabled", "true");
       copyOuterHTML.setAttribute("disabled", "true");
     }
 
+    // Enable the "edit HTML" item if the selection is an element and the root
+    // actor has the appropriate trait (isOuterHTMLEditable)
     let editHTML = this.panelDoc.getElementById("node-menu-edithtml");
-    if (this.isOuterHTMLEditable && selectionIsElement) {
+    if (this.isOuterHTMLEditable && isSelectionElement) {
       editHTML.removeAttribute("disabled");
     } else {
       editHTML.setAttribute("disabled", "true");
     }
+
+    // Enable the "copy image data-uri" item if the selection is previewable
+    // which essentially checks if it's an image or canvas tag
+    let copyImageData = this.panelDoc.getElementById("node-menu-copyimagedatauri");
+    let markupContainer = this.markup.getContainer(this.selection.nodeFront);
+    if (markupContainer && markupContainer.isPreviewable()) {
+      copyImageData.removeAttribute("disabled");
+    } else {
+      copyImageData.setAttribute("disabled", "true");
+    }
   },
 
   _resetNodeMenu: function InspectorPanel_resetNodeMenu() {
     // Remove any extra items
     while (this.lastNodemenuItem.nextSibling) {
       let toDelete = this.lastNodemenuItem.nextSibling;
       toDelete.parentNode.removeChild(toDelete);
     }
@@ -712,17 +725,29 @@ InspectorPanel.prototype = {
   {
     if (!this.selection.isNode()) {
       return;
     }
 
     this._copyLongStr(this.walker.outerHTML(this.selection.nodeFront));
   },
 
-  _copyLongStr: function(promise) {
+  /**
+   * Copy the data-uri for the currently selected image in the clipboard.
+   */
+  copyImageDataUri: function InspectorPanel_copyImageDataUri()
+  {
+    let container = this.markup.getContainer(this.selection.nodeFront);
+    if (container && container.isPreviewable()) {
+      container.copyImageDataUri();
+    }
+  },
+
+  _copyLongStr: function InspectorPanel_copyLongStr(promise)
+  {
     return promise.then(longstr => {
       return longstr.string().then(toCopy => {
         longstr.release().then(null, console.error);
         clipboardHelper.copyString(toCopy);
       });
     }).then(null, console.error);
   },
 
--- a/browser/devtools/inspector/inspector.xul
+++ b/browser/devtools/inspector/inspector.xul
@@ -47,16 +47,19 @@
       <menuitem id="node-menu-copyouter"
         label="&inspectorHTMLCopyOuter.label;"
         accesskey="&inspectorHTMLCopyOuter.accesskey;"
         oncommand="inspector.copyOuterHTML()"/>
       <menuitem id="node-menu-copyuniqueselector"
         label="&inspectorCopyUniqueSelector.label;"
         accesskey="&inspectorCopyUniqueSelector.accesskey;"
         oncommand="inspector.copyUniqueSelector()"/>
+      <menuitem id="node-menu-copyimagedatauri"
+        label="&inspectorCopyImageDataUri.label;"
+        oncommand="inspector.copyImageDataUri()"/>
       <menuseparator/>
       <menuitem id="node-menu-delete"
         label="&inspectorHTMLDelete.label;"
         accesskey="&inspectorHTMLDelete.accesskey;"
         oncommand="inspector.deleteNode()"/>
       <menuseparator/>
       <menuitem id="node-menu-pseudo-hover"
         label=":hover" type="checkbox"
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -23,16 +23,17 @@ const {gDevTools} = Cu.import("resource:
 const {HTMLEditor} = require("devtools/markupview/html-editor");
 const promise = require("sdk/core/promise");
 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
 const EventEmitter = require("devtools/shared/event-emitter");
 
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
  return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
 });
 loader.lazyGetter(this, "AutocompletePopup", () => {
   return require("devtools/shared/autocomplete-popup").AutocompletePopup
 });
 
@@ -1274,46 +1275,64 @@ function MarkupContainer(aMarkupView, aN
   this._prepareImagePreview();
 }
 
 MarkupContainer.prototype = {
   toString: function() {
     return "[MarkupContainer for " + this.node + "]";
   },
 
-  _prepareImagePreview: function() {
+  isPreviewable: function() {
     if (this.node.tagName) {
       let tagName = this.node.tagName.toLowerCase();
       let srcAttr = this.editor.getAttributeElement("src");
       let isImage = tagName === "img" && srcAttr;
       let isCanvas = tagName === "canvas";
 
+      return isImage || isCanvas;
+    } else {
+      return false;
+    }
+  },
+
+  _prepareImagePreview: function() {
+    if (this.isPreviewable()) {
       // Get the image data for later so that when the user actually hovers over
       // the element, the tooltip does contain the image
-      if (isImage || isCanvas) {
-        let def = promise.defer();
+      let def = promise.defer();
 
-        this.tooltipData = {
-          target: isImage ? srcAttr : this.editor.tag,
-          data: def.promise
-        };
+      this.tooltipData = {
+        target: this.editor.getAttributeElement("src") || this.editor.tag,
+        data: def.promise
+      };
 
-        this.node.getImageData(IMAGE_PREVIEW_MAX_DIM).then(data => {
-          if (data) {
-            data.data.string().then(str => {
-              let res = {data: str, size: data.size};
-              // Resolving the data promise and, to always keep tooltipData.data
-              // as a promise, create a new one that resolves immediately
-              def.resolve(res);
-              this.tooltipData.data = promise.resolve(res);
-            });
-          }
+      this.node.getImageData(IMAGE_PREVIEW_MAX_DIM).then(data => {
+        if (data) {
+          data.data.string().then(str => {
+            let res = {data: str, size: data.size};
+            // Resolving the data promise and, to always keep tooltipData.data
+            // as a promise, create a new one that resolves immediately
+            def.resolve(res);
+            this.tooltipData.data = promise.resolve(res);
+          });
+        }
+      });
+    }
+  },
+
+  copyImageDataUri: function() {
+    // We need to send again a request to gettooltipData even if one was sent for
+    // the tooltip, because we want the full-size image
+    this.node.getImageData().then(data => {
+      if (data) {
+        data.data.string().then(str => {
+          clipboardHelper.copyString(str, this.markup.doc);
         });
       }
-    }
+    });
   },
 
   _buildTooltipContent: function(target, tooltip) {
     if (this.tooltipData && target === this.tooltipData.target) {
       this.tooltipData.data.then(({data, size}) => {
         tooltip.setImageContent(data, size);
       });
       return true;
@@ -2063,8 +2082,13 @@ function parseAttributeValues(attr, doc)
 
   // Attributes return from DOMParser in reverse order from how they are entered.
   return attributes.reverse();
 }
 
 loader.lazyGetter(MarkupView.prototype, "strings", () => Services.strings.createBundle(
   "chrome://browser/locale/devtools/inspector.properties"
 ));
+
+XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
+  return Cc["@mozilla.org/widget/clipboardhelper;1"].
+    getService(Ci.nsIClipboardHelper);
+});
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -16,8 +16,9 @@ skip-if = true
 [browser_inspector_markup_edit_outerhtml.js]
 [browser_inspector_markup_edit_outerhtml2.js]
 [browser_inspector_markup_mutation.js]
 [browser_inspector_markup_mutation_flashing.js]
 [browser_inspector_markup_navigation.js]
 [browser_inspector_markup_subset.js]
 [browser_inspector_markup_765105_tooltip.js]
 [browser_inspector_markup_950732.js]
+[browser_inspector_markup_964014_copy_image_data.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_964014_copy_image_data.js
@@ -0,0 +1,131 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that image nodes have the "copy data-uri" contextual menu item enabled
+// and that clicking it puts the image data into the clipboard
+
+let doc;
+let inspector;
+let markup;
+
+const PAGE_CONTENT = [
+  '<div></div>',
+  '<img class="data" src="data:image/png;base64,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" />',
+  '<canvas class="canvas" width="600" height="600"></canvas>'
+].join("\n");
+
+function test() {
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload(evt) {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = "data:text/html,markup view copy image as data-uri";
+}
+
+function createDocument() {
+  doc.body.innerHTML = PAGE_CONTENT;
+  let context = doc.querySelector(".canvas").getContext("2d");
+  context.beginPath();
+  context.moveTo(300, 0);
+  context.lineTo(600, 600);
+  context.lineTo(0, 600);
+  context.closePath();
+  context.fillStyle = "#ffc821";
+  context.fill();
+
+  openInspector().then(startTests);
+}
+
+function startTests(aInspector, aToolbox) {
+  inspector = aInspector;
+  markup = inspector.markup;
+
+  Task.spawn(function() {
+    yield selectNode("div", inspector);
+    yield assertCopyImageDataNotAvailable();
+
+    yield selectNode("img", inspector);
+    yield assertCopyImageDataAvailable();
+    yield triggerCopyImageUrlAndWaitForClipboard(doc.querySelector("img").src);
+
+    yield selectNode("canvas", inspector);
+    yield assertCopyImageDataAvailable();
+    let canvas = doc.querySelector(".canvas");
+    yield triggerCopyImageUrlAndWaitForClipboard(canvas.toDataURL());
+
+    // Check again that the menu isn't available on the DIV (to make sure our
+    // menu updating mechanism works)
+    yield selectNode("div", inspector);
+    yield assertCopyImageDataNotAvailable();
+  }).then(null, ok.bind(null, false)).then(endTests);
+}
+
+function endTests() {
+  doc = inspector = markup = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function assertCopyImageDataNotAvailable() {
+  return openNodeMenu().then(menu => {
+    let item = menu.getElementsByAttribute("id", "node-menu-copyimagedatauri")[0];
+    ok(item, "The menu item was found in the contextual menu");
+    is(item.getAttribute("disabled"), "true", "The menu item is disabled");
+  }).then(closeNodeMenu);
+}
+
+function assertCopyImageDataAvailable() {
+  return openNodeMenu().then(menu => {
+    let item = menu.getElementsByAttribute("id", "node-menu-copyimagedatauri")[0];
+    ok(item, "The menu item was found in the contextual menu");
+    is(item.getAttribute("disabled"), "", "The menu item is enabled");
+  }).then(closeNodeMenu);
+}
+
+function openNodeMenu() {
+  let deferred = promise.defer();
+
+  inspector.nodemenu.addEventListener("popupshown", function onOpen() {
+    inspector.nodemenu.removeEventListener("popupshown", onOpen, false);
+    deferred.resolve(inspector.nodemenu);
+  }, false);
+  inspector.nodemenu.hidden = false;
+  inspector.nodemenu.openPopup();
+
+  return deferred.promise;
+}
+
+function closeNodeMenu() {
+  let deferred = promise.defer();
+
+  inspector.nodemenu.addEventListener("popuphidden", function onClose() {
+    inspector.nodemenu.removeEventListener("popuphidden", onClose, false);
+    deferred.resolve(inspector.nodemenu);
+  }, false);
+  inspector.nodemenu.hidden = true;
+  inspector.nodemenu.hidePopup();
+
+  return deferred.promise;
+}
+
+function triggerCopyImageUrlAndWaitForClipboard(expected) {
+  let deferred = promise.defer();
+
+  SimpleTest.waitForClipboard(expected, () => {
+    markup.getContainer(inspector.selection.nodeFront).copyImageDataUri();
+  }, () => {
+    ok(true, "The clipboard contains the expected value " + expected.substring(0, 50) + "...");
+    deferred.resolve();
+  }, () => {
+    ok(false, "The clipboard doesn't contain the expected value " + expected.substring(0, 50) + "...");
+    deferred.resolve();
+  });
+
+  return deferred.promise;
+}
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const Cu = Components.utils;
 
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let TargetFactory = devtools.TargetFactory;
 let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+let promise = devtools.require("sdk/core/promise");
 
 // Clear preferences that may be set during the course of tests.
 function clearUserPrefs() {
   Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen");
   Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
   Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
 }
 
@@ -22,8 +23,43 @@ SimpleTest.registerCleanupFunction(() =>
   Services.prefs.clearUserPref("devtools.debugger.log");
 });
 
 function getContainerForRawNode(markupView, rawNode) {
   let front = markupView.walker.frontForRawNode(rawNode);
   let container = markupView.getContainer(front);
   return container;
 }
+
+/**
+ * Open the toolbox, with the inspector tool visible.
+ * @return a promise that resolves when the inspector is ready
+ */
+function openInspector() {
+  let deferred = promise.defer();
+
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+    let inspector = toolbox.getCurrentPanel();
+    inspector.once("inspector-updated", () => {
+      deferred.resolve(inspector, toolbox);
+    });
+  }).then(null, console.error);
+
+  return deferred.promise;
+}
+
+/**
+ * Set the inspector's current selection to the first match of the given css
+ * selector
+ * @return a promise that resolves when the inspector is updated with the new
+ * node
+ */
+function selectNode(selector, inspector) {
+  let deferred = promise.defer();
+  let node = content.document.querySelector(selector);
+  ok(node, "A node was found for selector " + selector + ". Selecting it now");
+  inspector.selection.setNode(node, "test");
+  inspector.once("inspector-updated", () => {
+    deferred.resolve(node);
+  });
+  return deferred.promise;
+}
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -84,16 +84,17 @@ const CM_MAPPING = [
   "getSelection",
   "replaceSelection",
   "extendSelection",
   "undo",
   "redo",
   "clearHistory",
   "openDialog",
   "refresh",
+  "getScrollInfo",
   "getOption",
   "setOption"
 ];
 
 const { cssProperties, cssValues, cssColors } = getCSSKeywords();
 
 const editors = new WeakMap();
 
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -61,8 +61,9 @@ support-files = browser_ruleview_pseudoe
 support-files =
   sourcemaps.html
   sourcemaps.css
   sourcemaps.css.map
   sourcemaps.scss
 [browser_computedview_original_source_link.js]
 [browser_bug946331_close_tooltip_on_new_selection.js]
 [browser_bug942297_user_property_reset.js]
+[browser_styleinspector_outputparser.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_outputparser.js
@@ -0,0 +1,194 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test expected outputs of the output-parser's parseCssProperty function.
+
+// This is more of a unit test than a mochitest-browser test, but can't be
+// tested with an xpcshell test as the output-parser requires the DOM to work.
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let {OutputParser} = devtools.require("devtools/output-parser");
+
+const COLOR_CLASS = "color-class";
+const URL_CLASS = "url-class";
+
+function test() {
+  waitForExplicitFinish();
+
+  let testData = [
+    {
+      name: "width",
+      value: "100%",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 0);
+        is(fragment.textContent, "100%");
+      }
+    },
+    {
+      name: "width",
+      value: "blue",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 0);
+      }
+    },
+    {
+      name: "content",
+      value: "'red url(test.png) repeat top left'",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 0);
+      }
+    },
+    {
+      name: "content",
+      value: "\"blue\"",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 0);
+      }
+    },
+    {
+      name: "margin-left",
+      value: "url(something.jpg)",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 0);
+      }
+    },
+    {
+      name: "background-color",
+      value: "transparent",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 1);
+        is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
+        is(fragment.textContent, "transparent");
+      }
+    },
+    {
+      name: "color",
+      value: "red",
+      test: fragment => {
+        is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
+        is(fragment.textContent, "red");
+      }
+    },
+    {
+      name: "color",
+      value: "#F06",
+      test: fragment => {
+        is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
+        is(fragment.textContent, "#F06");
+      }
+    },
+    {
+      name: "border-top-left-color",
+      value: "rgba(14, 255, 20, .5)",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 1);
+        is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
+        is(fragment.textContent, "rgba(14, 255, 20, .5)");
+      }
+    },
+    {
+      name: "border",
+      value: "80em dotted pink",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 1);
+        is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
+        is(fragment.querySelector("." + COLOR_CLASS).textContent, "pink");
+      }
+    },
+    {
+      name: "color",
+      value: "red !important",
+      test: fragment => {
+        is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
+        is(fragment.textContent, "red !important");
+      }
+    },
+    {
+      name: "background",
+      value: "red url(test.png) repeat top left",
+      test: fragment => {
+        is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
+        is(fragment.querySelectorAll("." + URL_CLASS).length, 1);
+        is(fragment.querySelector("." + COLOR_CLASS).textContent, "red");
+        is(fragment.querySelector("." + URL_CLASS).textContent, "test.png");
+        is(fragment.querySelectorAll("*").length, 2);
+      }
+    },
+    {
+      name: "background",
+      value: "blue url(test.png) repeat top left !important",
+      test: fragment => {
+        is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
+        is(fragment.querySelectorAll("." + URL_CLASS).length, 1);
+        is(fragment.querySelector("." + COLOR_CLASS).textContent, "blue");
+        is(fragment.querySelector("." + URL_CLASS).textContent, "test.png");
+        is(fragment.querySelectorAll("*").length, 2);
+      }
+    },
+    {
+      name: "list-style-image",
+      value: "url(\"images/arrow.gif\")",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 1);
+        is(fragment.querySelector("." + URL_CLASS).textContent, "images/arrow.gif");
+      }
+    },
+    {
+      name: "list-style-image",
+      value: "url(\"images/arrow.gif\")!important",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 1);
+        is(fragment.querySelector("." + URL_CLASS).textContent, "images/arrow.gif");
+        is(fragment.textContent, "url('images/arrow.gif')!important");
+      }
+    },
+    {
+      name: "-moz-binding",
+      value: "url(http://somesite.com/path/to/binding.xml#someid)",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 1);
+        is(fragment.querySelectorAll("." + URL_CLASS).length, 1);
+        is(fragment.querySelector("." + URL_CLASS).textContent, "http://somesite.com/path/to/binding.xml#someid");
+      }
+    },
+    {
+      name: "background",
+      value: "linear-gradient(to right, rgba(183,222,237,1) 0%, rgba(33,180,226,1) 30%, rgba(31,170,217,.5) 44%, #F06 75%, red 100%)",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 5);
+        let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
+        is(allSwatches.length, 5);
+        is(allSwatches[0].textContent, "rgba(183,222,237,1)");
+        is(allSwatches[1].textContent, "rgba(33,180,226,1)");
+        is(allSwatches[2].textContent, "rgba(31,170,217,.5)");
+        is(allSwatches[3].textContent, "#F06");
+        is(allSwatches[4].textContent, "red");
+      }
+    },
+    {
+      name: "background",
+      value: "-moz-radial-gradient(center 45deg, circle closest-side, orange 0%, red 100%)",
+      test: fragment => {
+        is(fragment.querySelectorAll("*").length, 2);
+        let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
+        is(allSwatches.length, 2);
+        is(allSwatches[0].textContent, "orange");
+        is(allSwatches[1].textContent, "red");
+      }
+    }
+  ];
+
+  let parser = new OutputParser();
+  for (let i = 0; i < testData.length; i ++) {
+    let data = testData[i];
+    info("Output-parser test data " + i + ". {" + data.name + " : " + data.value + ";}");
+    data.test(parser.parseCssProperty(data.name, data.value, {
+      colorClass: COLOR_CLASS,
+      urlClass: URL_CLASS,
+      defaultColorType: false
+    }));
+  }
+
+  finish();
+}
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -198,19 +198,25 @@ update.downloadAndInstallButton.accesske
 
 # RSS Pretty Print
 feedShowFeedNew=Subscribe to '%S'…
 
 menuOpenAllInTabs.label=Open All in Tabs
 
 # History menu
 menuRestoreAllTabs.label=Restore All Tabs
+# LOCALIZATION NOTE (menuRestoreAllTabsSubview.label): like menuRestoreAllTabs.label,
+# but used in the history subview in the panel UI, so needs to mention these are *closed* tabs.
+menuRestoreAllTabsSubview.label=Restore Closed Tabs
 # LOCALIZATION NOTE (menuRestoreAllWindows, menuUndoCloseWindowLabel, menuUndoCloseWindowSingleTabLabel):
 # see bug 394759
 menuRestoreAllWindows.label=Restore All Windows
+# LOCALIZATION NOTE (menuRestoreAllWindowsSubview.label): like menuRestoreAllWindows.label,
+# but used in the history subview in the panel UI, so needs to mention these are *closed* windows.
+menuRestoreAllWindowsSubview.label=Restore Closed Windows
 # LOCALIZATION NOTE (menuUndoCloseWindowLabel): Semicolon-separated list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 Window Title, #2 Number of tabs
 menuUndoCloseWindowLabel=#1 (and #2 other tab);#1 (and #2 other tabs)
 menuUndoCloseWindowSingleTabLabel=#1
 
 # Unified Back-/Forward Popup
 tabHistory.current=Stay on this page
@@ -494,13 +500,13 @@ mixedContentBlocked.unblock.accesskey = 
 slowStartup.message = %S seems slow… to… start.
 slowStartup.helpButton.label = Learn How to Speed It Up
 slowStartup.helpButton.accesskey = L
 slowStartup.disableNotificationButton.label = Don't Tell Me Again
 slowStartup.disableNotificationButton.accesskey = A
 
 
 # LOCALIZATION NOTE(tipSection.tip0): %1$S will be replaced with the text defined
-# in tipSection.tip0.hint, %2$S will be replaced with a hyperlink containing the
-# text defined in tipSection.tip0.learnMore.
-tipSection.tip0 = %1$S: You can customize Firefox to work the way you do. Simply drag any of the above to the menu or toolbar. %2$S about customizing Firefox.
+# in tipSection.tip0.hint, %2$S will be replaced with brandShortName, %3$S will
+# be replaced with a hyperlink containing the text defined in tipSection.tip0.learnMore.
+tipSection.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
 tipSection.tip0.hint = Hint
 tipSection.tip0.learnMore = Learn more
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.dtd
@@ -12,8 +12,10 @@
 
 <!ENTITY inspectorHTMLDelete.label          "Delete Node">
 <!ENTITY inspectorHTMLDelete.accesskey      "D">
 
 <!ENTITY inspector.selectButton.tooltip     "Select element with mouse">
 
 <!ENTITY inspectorSearchHTML.label          "Search HTML">
 <!ENTITY inspectorSearchHTML.key            "F">
+
+<!ENTITY inspectorCopyImageDataUri.label       "Copy Image Data-URL">
--- a/browser/metro/base/content/contenthandlers/SelectionHandler.js
+++ b/browser/metro/base/content/contenthandlers/SelectionHandler.js
@@ -295,22 +295,28 @@ var SelectionHandler = {
     }
     this.closeSelection();
   },
 
   /*
    * Called any time SelectionHelperUI would like us to
    * recalculate the selection bounds.
    */
-  _onSelectionUpdate: function _onSelectionUpdate() {
+  _onSelectionUpdate: function _onSelectionUpdate(aMsg) {
     if (!this._contentWindow) {
       this._onFail("_onSelectionUpdate was called without proper view set up");
       return;
     }
 
+    if (aMsg && aMsg.isInitiatedByAPZC) {
+      let {offset: offset} = Content.getCurrentWindowAndOffset(
+        this._targetCoordinates.x, this._targetCoordinates.y);
+      this._contentOffset = offset;
+    }
+
     // Update the position of our selection monocles
     this._updateSelectionUI("update", true, true);
   },
 
   /*
    * Called if for any reason we fail during the selection
    * process. Cancels the selection.
    */
@@ -382,16 +388,17 @@ var SelectionHandler = {
     this._clearTimers();
     this._cache = null;
     this._contentWindow = null;
     this._targetElement = null;
     this._selectionMoveActive = false;
     this._contentOffset = null;
     this._domWinUtils = null;
     this._targetIsEditable = false;
+    this._targetCoordinates = null;
     sendSyncMessage("Content:HandlerShutdown", {});
   },
 
   /*
    * Find content within frames - cache the target nsIDOMWindow,
    * client coordinate offset, target element, and dom utils interface.
    */
   _initTargetInfo: function _initTargetInfo(aX, aY) {
@@ -404,16 +411,21 @@ var SelectionHandler = {
     if (!contentWindow) {
       return false;
     }
     this._targetElement = element;
     this._contentWindow = contentWindow;
     this._contentOffset = offset;
     this._domWinUtils = utils;
     this._targetIsEditable = Util.isEditable(this._targetElement);
+    this._targetCoordinates = {
+      x: aX,
+      y: aY
+    };
+
     return true;
   },
 
   /*
    * _calcNewContentPosition - calculates the distance the browser should be
    * raised to move the focused form input out of the way of the soft
    * keyboard.
    *
@@ -528,17 +540,17 @@ var SelectionHandler = {
         this._onSelectionCopy(json);
         break;
 
       case "Browser:SelectionDebug":
         this._onSelectionDebug(json);
         break;
 
       case "Browser:SelectionUpdate":
-        this._onSelectionUpdate();
+        this._onSelectionUpdate(json);
         break;
 
       case "Browser:RepositionInfoRequest":
         // This message is sent simultaneously with a tap event.
         // Wait a bit to make sure we have the most up-to-date tap co-ordinates
         // before a call to _calcNewContentPosition() which accesses them.
         content.setTimeout (function () {
           SelectionHandler._repositionInfoRequest(json);
--- a/browser/metro/base/content/helperui/SelectionHelperUI.js
+++ b/browser/metro/base/content/helperui/SelectionHelperUI.js
@@ -356,17 +356,19 @@ var SelectionHelperUI = {
         break;
 
       case "apzc-transform-end":
         // The selection range callback will check to see if the new
         // position is off the screen, in which case it shuts down and
         // clears the selection.
         if (this.isActive && this.layerMode == kContentLayer) {
           this._showAfterUpdate = true;
-          this._sendAsyncMessage("Browser:SelectionUpdate", {});
+          this._sendAsyncMessage("Browser:SelectionUpdate", {
+            isInitiatedByAPZC: true
+          });
         }
         break;
       }
   },
 
   /*
    * Public apis
    */
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/browser_selection_frame_in_scrollable_container.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      body {
+        padding: 40px;
+      }
+
+      .scrollable-container {
+        border: 1px solid blue;
+        overflow: auto;
+        max-height: 100px;
+        width: 200px;
+      }
+
+      .internal-frame {
+        display:block;
+        padding: 0px;
+        width: 150px;
+        visibility: visible;
+        height: 150px;
+        border: 1px solid red;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="scrollable-container">
+      <iframe id="frame1" src="res/documentindesignmode.html" tabindex="1" frameborder="0"
+              class="internal-frame">
+      </iframe>
+    </div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/browser_selection_frame_in_scrollable_container.js
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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";
+
+let gWindow = null;
+var gFrame = null;
+
+function setUpAndTearDown() {
+  emptyClipboard();
+
+  if (gWindow) {
+    clearSelection(gWindow);
+  }
+
+  if (gFrame) {
+    clearSelection(gFrame);
+  }
+
+  yield waitForCondition(function () {
+    return !SelectionHelperUI.isSelectionUIVisible;
+  });
+
+  InputSourceHelper.isPrecise = false;
+  InputSourceHelper.fireUpdate();
+}
+
+gTests.push({
+  desc: "Selection monocles for frame content that is located inside " +
+        "scrollable container.",
+  setUp: setUpAndTearDown,
+  tearDown: setUpAndTearDown,
+  run: function test() {
+    let urlToLoad = chromeRoot +
+        "browser_selection_frame_in_scrollable_container.html";
+    info(urlToLoad);
+    yield addTab(urlToLoad);
+
+    ContextUI.dismiss();
+    yield waitForCondition(() => !ContextUI.navbarVisible);
+
+    gWindow = Browser.selectedTab.browser.contentWindow;
+    gFrame = gWindow.document.getElementById("frame1");
+
+    // Select some content inside frame.
+    let promise = waitForEvent(document, "popupshown");
+    sendContextMenuClickToWindow(gFrame.contentWindow, 10, 10);
+    yield promise;
+
+    let selectMenuItem = document.getElementById("context-select");
+    promise = waitForEvent(document, "popuphidden");
+    sendElementTap(gWindow, selectMenuItem);
+    yield promise;
+    yield waitForCondition(()=>SelectionHelperUI.isSelectionUIVisible);
+
+    // Scroll frame inside scrollable container.
+    let initialYPos = SelectionHelperUI.endMark.yPos;
+    let touchDrag = new TouchDragAndHold();
+    touchDrag.useNativeEvents = true;
+    yield touchDrag.start(gWindow, 100, 90, 100, 50);
+    touchDrag.end();
+
+    yield waitForCondition(() => !SelectionHelperUI.hasActiveDrag);
+    yield SelectionHelperUI.pingSelectionHandler();
+
+    yield waitForCondition(()=>SelectionHelperUI.isSelectionUIVisible);
+
+    ok(initialYPos - SelectionHelperUI.endMark.yPos > 10,
+        "Selection monocles followed scrolled content.");
+  }
+});
+
+function test() {
+  // We need this until bug 859742 is fully resolved.
+  setDevPixelEqualToPx();
+  runTests();
+}
--- a/browser/metro/base/tests/mochitest/metro.ini
+++ b/browser/metro/base/tests/mochitest/metro.ini
@@ -11,16 +11,17 @@ support-files =
   browser_link_click.html
   browser_onscreen_keyboard.html
   browser_progress_indicator.xul
   browser_selection_basic.html
   browser_selection_caretfocus.html
   browser_selection_contenteditable.html
   browser_selection_frame_content.html
   browser_selection_frame_inputs.html
+  browser_selection_frame_in_scrollable_container.html
   browser_selection_frame_textarea.html
   browser_selection_inputs.html
   browser_selection_textarea.html
   browser_tilegrid.xul
   head.js
   helpers/BookmarksHelper.js
   helpers/HistoryHelper.js
   helpers/ViewStateHelper.js
@@ -28,16 +29,17 @@ support-files =
   res/textblock01.html
   res/textdivs01.html
   res/textinput01.html
   res/textarea01.html
   res/testEngine.xml
   res/blankpage1.html
   res/blankpage2.html
   res/blankpage3.html
+  res/documentindesignmode.html
 
 [browser_apzc_basic.js]
 [browser_bookmarks.js]
 [browser_canonizeURL.js]
 [browser_circular_progress_indicator.js]
 [browser_colorUtils.js]
 [browser_crashprompt.js]
 [browser_context_menu_tests.js]
@@ -69,16 +71,18 @@ support-files =
 
 # These tests have known failures in debug builds
 [browser_selection_basic.js]
 skip-if = debug
 [browser_selection_textarea.js]
 skip-if = debug
 [browser_selection_frame_content.js]
 skip-if = debug
+[browser_selection_frame_in_scrollable_container.js]
+skip-if = debug
 [browser_selection_inputs.js]
 skip-if = debug
 [browser_selection_frame_textarea.js]
 skip-if = debug
 [browser_selection_frame_inputs.js]
 skip-if = debug
 [browser_selection_urlbar.js]
 skip-if = debug
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/res/documentindesignmode.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <style>
+    body {
+      border: none;
+      background: none repeat scroll 0% 0% transparent;
+      margin: 0;
+      min-width: 0px;
+      min-height: 100px;
+      width: 100px;
+      height: 100%;
+      direction: ltr;
+      overflow: hidden;
+    }
+
+    .test-div {
+      font-size: 22px;
+    }
+  </style>
+</head>
+<body role="textbox" hidefocus="true">
+  <div class="test-div div1">1</div>
+  <div class="test-div div2">2</div>
+  <div class="test-div div3">3</div>
+  <div class="test-div div4">4</div>
+  <div class="test-div div5">5</div>
+</body>
+  <script type="text/javascript">
+    document.designMode = "on";
+  </script>
+</html>
--- a/browser/themes/linux/searchbar.css
+++ b/browser/themes/linux/searchbar.css
@@ -1,16 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 .searchbar-textbox {
   min-height: 22px;
-  width: 6em;
-  min-width: 6em;
   background-color: -moz-field;
 }
 
 .autocomplete-textbox-container {
   -moz-box-align: stretch;
 }
 
 .textbox-input-box {
--- a/browser/themes/osx/searchbar.css
+++ b/browser/themes/osx/searchbar.css
@@ -1,15 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 .searchbar-textbox {
   border-radius: 10000px;
-  min-width: 6em;
 }
 
 .searchbar-engine-button {
   -moz-padding-start: 6px;
   -moz-padding-end: 2px;
   margin: 0;
   -moz-margin-end: 2px;
   -moz-appearance: none;
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -246,23 +246,23 @@ toolbarpaletteitem[place="palette"] > to
   margin: 0;
 }
 
 #PanelUI-footer-inner {
   display: flex;
   box-shadow: 0 -1px 0 rgba(0,0,0,.15);
 }
 
-#PanelUI-footer > toolbarseparator {
+#PanelUI-footer-inner > toolbarseparator {
   border: 0;
   border-left: 1px solid rgba(0,0,0,0.1);
   margin: 7px 0 7px;
 }
 
-#PanelUI-footer:hover > toolbarseparator {
+#PanelUI-footer-inner:hover > toolbarseparator {
   margin: 0;
 }
 
 #PanelUI-help,
 #PanelUI-fxa-status,
 #PanelUI-customize,
 #PanelUI-quit {
   margin: 0;
@@ -274,39 +274,46 @@ toolbarpaletteitem[place="palette"] > to
   border: 1px solid transparent;
   border-bottom-style: none;
   border-radius: 0;
   transition: background-color;
   -moz-box-orient: horizontal;
 }
 
 #PanelUI-fxa-status {
-  width: calc(@menuPanelWidth@ + 20px);
   border-bottom-style: solid;
 }
 
+#PanelUI-fxa-status > .toolbarbutton-text {
+  width: 0; /* Fancy cropping solution for flexbox. */
+}
+
 #PanelUI-fxa-status[signedin] {
   font-weight: bold;
 }
 
 #PanelUI-help,
 #PanelUI-quit {
   min-width: 46px;
 }
 
 #PanelUI-fxa-status > .toolbarbutton-text,
 #PanelUI-customize > .toolbarbutton-text {
+  margin: 0;
+  padding: 0 6px;
   text-align: start;
 }
 
 #PanelUI-help > .toolbarbutton-text,
 #PanelUI-quit > .toolbarbutton-text {
   display: none;
 }
 
+#PanelUI-fxa-status > .toolbarbutton-icon,
+#PanelUI-customize > .toolbarbutton-icon,
 #PanelUI-help > .toolbarbutton-icon,
 #PanelUI-quit > .toolbarbutton-icon {
   -moz-margin-end: 0;
 }
 
 #PanelUI-fxa-status,
 #PanelUI-customize {
   flex: 1;
@@ -500,16 +507,20 @@ panelview toolbarseparator,
   -moz-margin-end: auto;
   color: hsl(0,0%,50%);
 }
 
 #PanelUI-historyItems > toolbarbutton {
   list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
 }
 
+.restoreallitem.subviewbutton > .toolbarbutton-icon {
+  display: none;
+}
+
 #PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
 #PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
 #PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
   width: 16px;
   height: 16px;
 }
 
 #PanelUI-footer.panel-multiview-anchor,
--- a/browser/themes/windows/searchbar.css
+++ b/browser/themes/windows/searchbar.css
@@ -1,17 +1,12 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-.searchbar-textbox {
-  width: 6em;
-  min-width: 6em;
-}
-
 .autocomplete-textbox-container {
   -moz-box-align: stretch;
 }
 
 .textbox-input-box {
   margin: 0;
 }
 
--- a/content/media/MediaRecorder.cpp
+++ b/content/media/MediaRecorder.cpp
@@ -167,45 +167,47 @@ class MediaRecorder::Session: public nsI
       MOZ_ASSERT(NS_IsMainThread() && mSession.get());
       MediaRecorder *recorder = mSession->mRecorder;
 
       // SourceMediaStream is ended, and send out TRACK_EVENT_END notification.
       // Read Thread will be terminate soon.
       // We need to switch MediaRecorder to "Stop" state first to make sure
       // MediaRecorder is not associated with this Session anymore, then, it's
       // safe to delete this Session.
-      if (recorder->mState != RecordingState::Inactive) {
+      // Also avoid to run if this session already call stop before
+      if (!mSession->mStopIssued) {
         ErrorResult result;
         recorder->Stop(result);
         NS_DispatchToMainThread(new DestroyRunnable(mSession.forget()));
 
         return NS_OK;
       }
 
       // Dispatch stop event and clear MIME type.
       recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop"));
-      recorder->SetMimeType(NS_LITERAL_STRING(""));
-
+      mSession->mMimeType = NS_LITERAL_STRING("");
+      recorder->SetMimeType(mSession->mMimeType);
       return NS_OK;
     }
 
   private:
     // Call mSession::Release automatically while DestroyRunnable be destroy.
     nsRefPtr<Session> mSession;
   };
 
   friend class PushBlobRunnable;
   friend class ExtractRunnable;
   friend class DestroyRunnable;
   friend class TracksAvailableCallback;
 
 public:
   Session(MediaRecorder* aRecorder, int32_t aTimeSlice)
     : mRecorder(aRecorder),
-      mTimeSlice(aTimeSlice)
+      mTimeSlice(aTimeSlice),
+      mStopIssued(false)
   {
     MOZ_ASSERT(NS_IsMainThread());
 
     AddRef();
     mEncodedBufferCache = new EncodedBufferCache(MAX_ALLOW_MEMORY_BUFFER);
     mLastBlobTimeStamp = TimeStamp::Now();
   }
 
@@ -221,16 +223,17 @@ public:
 
     SetupStreams();
   }
 
   void Stop()
   {
     MOZ_ASSERT(NS_IsMainThread());
 
+    mStopIssued = true;
     CleanupStreams();
     nsContentUtils::UnregisterShutdownObserver(this);
   }
 
   nsresult Pause()
   {
     NS_ENSURE_TRUE(NS_IsMainThread() && mTrackUnionStream, NS_ERROR_FAILURE);
     mTrackUnionStream->ChangeExplicitBlockerCount(-1);
@@ -243,20 +246,17 @@ public:
     NS_ENSURE_TRUE(NS_IsMainThread() && mTrackUnionStream, NS_ERROR_FAILURE);
     mTrackUnionStream->ChangeExplicitBlockerCount(1);
 
     return NS_OK;
   }
 
   already_AddRefed<nsIDOMBlob> GetEncodedData()
   {
-    nsString mimeType;
-    mRecorder->GetMimeType(mimeType);
-
-    return mEncodedBufferCache->ExtractBlob(mimeType);
+    return mEncodedBufferCache->ExtractBlob(mMimeType);
   }
 
   bool IsEncoderError()
   {
     if (mEncoder && mEncoder->HasError()) {
       return true;
     }
     return false;
@@ -269,20 +269,18 @@ private:
   {
     MOZ_ASSERT(NS_GetCurrentThread() == mReadThread);
 
     // Whether push encoded data back to onDataAvailable automatically.
     const bool pushBlob = (mTimeSlice > 0) ? true : false;
 
     // Pull encoded media data from MediaEncoder
     nsTArray<nsTArray<uint8_t> > encodedBuf;
-    nsString mimeType;
-    mEncoder->GetEncodedData(&encodedBuf, mimeType);
-
-    mRecorder->SetMimeType(mimeType);
+    mEncoder->GetEncodedData(&encodedBuf, mMimeType);
+    mRecorder->SetMimeType(mMimeType);
 
     // Append pulled data into cache buffer.
     for (uint32_t i = 0; i < encodedBuf.Length(); i++) {
       mEncodedBufferCache->AppendBuffer(encodedBuf[i]);
     }
 
     if (pushBlob) {
       if ((TimeStamp::Now() - mLastBlobTimeStamp).ToMilliseconds() > mTimeSlice) {
@@ -320,18 +318,20 @@ private:
     // At this stage, the API doesn't allow UA to choose the output mimeType format.
     mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(""), aTrackTypes);
 
     if (!mEncoder) {
       DoSessionEndTask(NS_ERROR_ABORT);
       return;
     }
 
-    // media stream is ready but has been issued stop command
-    if (mRecorder->mState == RecordingState::Inactive) {
+    // Media stream is ready but UA issues a stop method follow by start method.
+    // The Session::stop would clean the mTrackUnionStream. If the AfterTracksAdded
+    // comes after stop command, this function would crash.
+    if (!mTrackUnionStream) {
       DoSessionEndTask(NS_OK);
       return;
     }
     mTrackUnionStream->AddListener(mEncoder);
     // Create a thread to read encode media data from MediaEncoder.
     if (!mReadThread) {
       nsresult rv = NS_NewNamedThread("Media Encoder", getter_AddRefs(mReadThread));
       if (NS_FAILED(rv)) {
@@ -395,23 +395,27 @@ private:
   nsRefPtr<MediaInputPort> mInputPort;
 
   // Runnable thread for read data from MediaEncode.
   nsCOMPtr<nsIThread> mReadThread;
   // MediaEncoder pipeline.
   nsRefPtr<MediaEncoder> mEncoder;
   // A buffer to cache encoded meda data.
   nsAutoPtr<EncodedBufferCache> mEncodedBufferCache;
+  // Current session mimeType
+  nsString mMimeType;
   // Timestamp of the last fired dataavailable event.
   TimeStamp mLastBlobTimeStamp;
   // The interval of passing encoded data from EncodedBufferCache to onDataAvailable
   // handler. "mTimeSlice < 0" means Session object does not push encoded data to
   // onDataAvailable, instead, it passive wait the client side pull encoded data
   // by calling requestData API.
   const int32_t mTimeSlice;
+  // Indicate this session's stop has been called.
+  bool mStopIssued;
 };
 
 NS_IMPL_ISUPPORTS1(MediaRecorder::Session, nsIObserver)
 
 MediaRecorder::~MediaRecorder()
 {
   MOZ_ASSERT(mSession == nullptr);
 }
--- a/content/media/RtspMediaResource.cpp
+++ b/content/media/RtspMediaResource.cpp
@@ -435,29 +435,65 @@ RtspMediaResource::OnMediaDataAvailable(
   if (mRealTime) {
     time = 0;
   }
   mTrackBuffer[aTrackIdx]->WriteBuffer(data.BeginReading(), length, time,
                                        frameType);
   return NS_OK;
 }
 
+// Bug 962309 - Video RTSP support should be disabled in 1.3
+bool
+RtspMediaResource::IsVideoEnabled()
+{
+  return Preferences::GetBool("media.rtsp.video.enabled", false);
+}
+
+bool
+RtspMediaResource::IsVideo(uint8_t tracks, nsIStreamingProtocolMetaData *meta)
+{
+  bool isVideo = false;
+  for (int i = 0; i < tracks; ++i) {
+    nsCOMPtr<nsIStreamingProtocolMetaData> trackMeta;
+    mMediaStreamController->GetTrackMetaData(i, getter_AddRefs(trackMeta));
+    MOZ_ASSERT(trackMeta);
+    uint32_t w = 0, h = 0;
+    trackMeta->GetWidth(&w);
+    trackMeta->GetHeight(&h);
+    if (w > 0 || h > 0) {
+      isVideo = true;
+      break;
+    }
+  }
+  return isVideo;
+}
+
 nsresult
 RtspMediaResource::OnConnected(uint8_t aTrackIdx,
                                nsIStreamingProtocolMetaData *meta)
 {
   if (mIsConnected) {
     for (uint32_t i = 0 ; i < mTrackBuffer.Length(); ++i) {
       mTrackBuffer[i]->Start();
     }
     return NS_OK;
   }
 
   uint8_t tracks;
   mMediaStreamController->GetTotalTracks(&tracks);
+
+  // If the preference of RTSP video feature is not enabled and the streaming is
+  // video, we give up moving forward.
+  if (!IsVideoEnabled() && IsVideo(tracks, meta)) {
+    // Give up, report error to media element.
+    nsCOMPtr<nsIRunnable> event =
+      NS_NewRunnableMethod(mDecoder, &MediaDecoder::DecodeError);
+    NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
+    return NS_ERROR_FAILURE;
+  }
   uint64_t duration = 0;
   for (int i = 0; i < tracks; ++i) {
     nsCString rtspTrackId("RtspTrack");
     rtspTrackId.AppendInt(i);
     nsCOMPtr<nsIStreamingProtocolMetaData> trackMeta;
     mMediaStreamController->GetTrackMetaData(i, getter_AddRefs(trackMeta));
     MOZ_ASSERT(trackMeta);
     trackMeta->GetDuration(&duration);
--- a/content/media/RtspMediaResource.h
+++ b/content/media/RtspMediaResource.h
@@ -211,16 +211,18 @@ public:
 protected:
   // Main thread access only.
   // These are called on the main thread by Listener.
   NS_DECL_NSISTREAMINGPROTOCOLLISTENER
 
   nsRefPtr<Listener> mListener;
 
 private:
+  bool IsVideoEnabled();
+  bool IsVideo(uint8_t tracks, nsIStreamingProtocolMetaData *meta);
   // These two members are created at |RtspMediaResource::OnConnected|.
   nsCOMPtr<nsIStreamingProtocolController> mMediaStreamController;
   nsTArray<nsAutoPtr<RtspTrackBuffer>> mTrackBuffer;
 
   // A flag that indicates the |RtspMediaResource::OnConnected| has already been
   // called.
   bool mIsConnected;
   // live stream
--- a/content/media/encoder/ContainerWriter.h
+++ b/content/media/encoder/ContainerWriter.h
@@ -19,18 +19,18 @@ class ContainerWriter {
 public:
   ContainerWriter()
     : mInitialized(false)
     , mIsWritingComplete(false)
   {}
   virtual ~ContainerWriter() {}
   // Mapping to DOMLocalMediaStream::TrackTypeHints
   enum {
-    HAS_AUDIO = 1 << 0,
-    HAS_VIDEO = 1 << 1,
+    CREATE_AUDIO_TRACK = 1 << 0,
+    CREATE_VIDEO_TRACK = 1 << 1,
   };
   enum {
     END_OF_STREAM = 1 << 0
   };
 
   /**
    * Writes encoded track data from aBuffer to a packet, and insert this packet
    * into the internal stream of container writer. aDuration is the playback
--- a/content/media/encoder/EncodedFrameContainer.h
+++ b/content/media/encoder/EncodedFrameContainer.h
@@ -51,24 +51,35 @@ public:
     I_FRAME,      // intraframe
     P_FRAME,      // predicted frame
     B_FRAME,      // bidirectionally predicted frame
     AUDIO_FRAME,  // audio frame
     AAC_CSD,      // AAC codec specific data
     AVC_CSD,      // AVC codec specific data
     UNKNOW        // FrameType not set
   };
+  nsresult SwapInFrameData(nsTArray<uint8_t>& aData)
+  {
+    mFrameData.SwapElements(aData);
+    return NS_OK;
+  }
+  nsresult SwapOutFrameData(nsTArray<uint8_t>& aData)
+  {
+    if (mFrameType != UNKNOW) {
+      // Reset this frame type to UNKNOW once the data is swapped out.
+      mFrameData.SwapElements(aData);
+      mFrameType = UNKNOW;
+      return NS_OK;
+    }
+    return NS_ERROR_FAILURE;
+  }
   const nsTArray<uint8_t>& GetFrameData() const
   {
     return mFrameData;
   }
-  void SetFrameData(nsTArray<uint8_t> *aData)
-  {
-    mFrameData.SwapElements(*aData);
-  }
   uint64_t GetTimeStamp() const { return mTimeStamp; }
   void SetTimeStamp(uint64_t aTimeStamp) { mTimeStamp = aTimeStamp; }
 
   uint64_t GetDuration() const { return mDuration; }
   void SetDuration(uint64_t aDuration) { mDuration = aDuration; }
 
   FrameType GetFrameType() const { return mFrameType; }
   void SetFrameType(FrameType aFrameType) { mFrameType = aFrameType; }
--- a/content/media/encoder/MediaEncoder.cpp
+++ b/content/media/encoder/MediaEncoder.cpp
@@ -95,47 +95,47 @@ MediaEncoder::CreateEncoder(const nsAStr
   nsString mimeType;
   if (!aTrackTypes) {
     LOG(PR_LOG_ERROR, ("NO TrackTypes!!!"));
     return nullptr;
   }
 #ifdef MOZ_WEBM_ENCODER
   else if (MediaEncoder::IsWebMEncoderEnabled() &&
           (aMIMEType.EqualsLiteral(VIDEO_WEBM) ||
-          (aTrackTypes & ContainerWriter::HAS_VIDEO))) {
-    if (aTrackTypes & ContainerWriter::HAS_AUDIO) {
+          (aTrackTypes & ContainerWriter::CREATE_VIDEO_TRACK))) {
+    if (aTrackTypes & ContainerWriter::CREATE_AUDIO_TRACK) {
       audioEncoder = new VorbisTrackEncoder();
       NS_ENSURE_TRUE(audioEncoder, nullptr);
     }
     videoEncoder = new VP8TrackEncoder();
     writer = new WebMWriter(aTrackTypes);
     NS_ENSURE_TRUE(writer, nullptr);
     NS_ENSURE_TRUE(videoEncoder, nullptr);
     mimeType = NS_LITERAL_STRING(VIDEO_WEBM);
   }
 #endif //MOZ_WEBM_ENCODER
 #ifdef MOZ_OMX_ENCODER
   else if (MediaEncoder::IsOMXEncoderEnabled() &&
           (aMIMEType.EqualsLiteral(VIDEO_MP4) ||
-          (aTrackTypes & ContainerWriter::HAS_VIDEO))) {
-    if (aTrackTypes & ContainerWriter::HAS_AUDIO) {
+          (aTrackTypes & ContainerWriter::CREATE_VIDEO_TRACK))) {
+    if (aTrackTypes & ContainerWriter::CREATE_AUDIO_TRACK) {
       audioEncoder = new OmxAudioTrackEncoder();
       NS_ENSURE_TRUE(audioEncoder, nullptr);
     }
     videoEncoder = new OmxVideoTrackEncoder();
     writer = new ISOMediaWriter(aTrackTypes);
     NS_ENSURE_TRUE(writer, nullptr);
     NS_ENSURE_TRUE(videoEncoder, nullptr);
     mimeType = NS_LITERAL_STRING(VIDEO_MP4);
   }
 #endif // MOZ_OMX_ENCODER
 #ifdef MOZ_OGG
   else if (MediaDecoder::IsOggEnabled() && MediaDecoder::IsOpusEnabled() &&
            (aMIMEType.EqualsLiteral(AUDIO_OGG) ||
-           (aTrackTypes & ContainerWriter::HAS_AUDIO))) {
+           (aTrackTypes & ContainerWriter::CREATE_AUDIO_TRACK))) {
     writer = new OggWriter();
     audioEncoder = new OpusTrackEncoder();
     NS_ENSURE_TRUE(writer, nullptr);
     NS_ENSURE_TRUE(audioEncoder, nullptr);
     mimeType = NS_LITERAL_STRING(AUDIO_OGG);
   }
 #endif  // MOZ_OGG
   else {
--- a/content/media/encoder/MediaEncoder.h
+++ b/content/media/encoder/MediaEncoder.h
@@ -88,17 +88,17 @@ public :
   virtual void NotifyRemoved(MediaStreamGraph* aGraph);
 
   /**
    * Creates an encoder with a given MIME type. Returns null if we are unable
    * to create the encoder. For now, default aMIMEType to "audio/ogg" and use
    * Ogg+Opus if it is empty.
    */
   static already_AddRefed<MediaEncoder> CreateEncoder(const nsAString& aMIMEType,
-                                                      uint8_t aTrackTypes = ContainerWriter::HAS_AUDIO);
+                                                      uint8_t aTrackTypes = ContainerWriter::CREATE_AUDIO_TRACK);
   /**
    * Encodes the raw track data and returns the final container data. Assuming
    * it is called on a single worker thread. The buffer of container data is
    * allocated in ContainerWriter::GetContainerData(), and is appended to
    * aOutputBufs. aMIMEType is the valid mime-type of this returned container
    * data.
    */
   void GetEncodedData(nsTArray<nsTArray<uint8_t> >* aOutputBufs,
--- a/content/media/encoder/OmxTrackEncoder.cpp
+++ b/content/media/encoder/OmxTrackEncoder.cpp
@@ -116,30 +116,32 @@ OmxVideoTrackEncoder::GetEncodedTrack(En
     uint64_t totalDurationUs = mTotalFrameDuration * USECS_PER_S / mTrackRate;
     layers::Image* img = (!mLastFrame.GetImage() || mLastFrame.GetForceBlack())
                          ? nullptr : mLastFrame.GetImage();
     mEncoder->Encode(img, mFrameWidth, mFrameHeight, totalDurationUs,
                      OMXCodecWrapper::BUFFER_EOS);
   }
 
   // Dequeue an encoded frame from the output buffers of OMXCodecWrapper.
+  nsresult rv;
   nsTArray<uint8_t> buffer;
   int outFlags = 0;
   int64_t outTimeStampUs = 0;
   mEncoder->GetNextEncodedFrame(&buffer, &outTimeStampUs, &outFlags,
                                 GET_ENCODED_VIDEO_FRAME_TIMEOUT);
   if (!buffer.IsEmpty()) {
     nsRefPtr<EncodedFrame> videoData = new EncodedFrame();
     if (outFlags & OMXCodecWrapper::BUFFER_CODEC_CONFIG) {
       videoData->SetFrameType(EncodedFrame::AVC_CSD);
     } else {
       videoData->SetFrameType((outFlags & OMXCodecWrapper::BUFFER_SYNC_FRAME) ?
                               EncodedFrame::I_FRAME : EncodedFrame::P_FRAME);
     }
-    videoData->SetFrameData(&buffer);
+    rv = videoData->SwapInFrameData(buffer);
+    NS_ENSURE_SUCCESS(rv, rv);
     videoData->SetTimeStamp(outTimeStampUs);
     aData.AppendEncodedFrame(videoData);
   }
 
   if (outFlags & OMXCodecWrapper::BUFFER_EOS) {
     mEncodingComplete = true;
     OMX_LOG("Done encoding video.");
   }
@@ -208,17 +210,18 @@ OmxAudioTrackEncoder::AppendEncodedFrame
     } else if (outFlags & OMXCodecWrapper::BUFFER_EOS) { // last frame
       mEncodingComplete = true;
     }
 
     nsRefPtr<EncodedFrame> audiodata = new EncodedFrame();
     audiodata->SetFrameType(isCSD ?
       EncodedFrame::AAC_CSD : EncodedFrame::AUDIO_FRAME);
     audiodata->SetTimeStamp(outTimeUs);
-    audiodata->SetFrameData(&frameData);
+    rv = audiodata->SwapInFrameData(frameData);
+    NS_ENSURE_SUCCESS(rv, rv);
     aContainer.AppendEncodedFrame(audiodata);
   }
 
   return NS_OK;
 }
 
 nsresult
 OmxAudioTrackEncoder::GetEncodedTrack(EncodedFrameContainer& aData)
--- a/content/media/encoder/OpusTrackEncoder.cpp
+++ b/content/media/encoder/OpusTrackEncoder.cpp
@@ -369,14 +369,14 @@ OpusTrackEncoder::GetEncodedTrack(Encode
   }
   if (mEncodingComplete) {
     if (mResampler) {
       speex_resampler_destroy(mResampler);
       mResampler = nullptr;
     }
   }
 
-  audiodata->SetFrameData(&frameData);
+  audiodata->SwapInFrameData(frameData);
   aData.AppendEncodedFrame(audiodata);
   return result >= 0 ? NS_OK : NS_ERROR_FAILURE;
 }
 
 }
--- a/content/media/encoder/VP8TrackEncoder.cpp
+++ b/content/media/encoder/VP8TrackEncoder.cpp
@@ -159,16 +159,17 @@ VP8TrackEncoder::GetMetadata()
 }
 
 nsresult
 VP8TrackEncoder::GetEncodedPartitions(EncodedFrameContainer& aData)
 {
   vpx_codec_iter_t iter = nullptr;
   EncodedFrame::FrameType frameType = EncodedFrame::P_FRAME;
   nsTArray<uint8_t> frameData;
+  nsresult rv;
   const vpx_codec_cx_pkt_t *pkt = nullptr;
   while ((pkt = vpx_codec_get_cx_data(mVPXContext, &iter)) != nullptr) {
     switch (pkt->kind) {
       case VPX_CODEC_CX_FRAME_PKT: {
         // Copy the encoded data from libvpx to frameData
         frameData.AppendElements((uint8_t*)pkt->data.frame.buf,
                                  pkt->data.frame.sz);
         break;
@@ -197,17 +198,18 @@ VP8TrackEncoder::GetEncodedPartitions(En
       videoData->SetTimeStamp(
         (uint64_t)FramesToUsecs(mEncodedTimestamp, mTrackRate).value());
     }
     CheckedInt64 duration = FramesToUsecs(pkt->data.frame.duration, mTrackRate);
     if (duration.isValid()) {
       videoData->SetDuration(
         (uint64_t)FramesToUsecs(pkt->data.frame.duration, mTrackRate).value());
     }
-    videoData->SetFrameData(&frameData);
+    rv = videoData->SwapInFrameData(frameData);
+    NS_ENSURE_SUCCESS(rv, rv);
     VP8LOG("GetEncodedPartitions TimeStamp %lld Duration %lld\n",
            videoData->GetTimeStamp(), videoData->GetDuration());
     VP8LOG("frameType %d\n", videoData->GetFrameType());
     aData.AppendEncodedFrame(videoData);
   }
 
   return NS_OK;
 }
--- a/content/media/encoder/VorbisTrackEncoder.cpp
+++ b/content/media/encoder/VorbisTrackEncoder.cpp
@@ -142,17 +142,17 @@ VorbisTrackEncoder::GetEncodedFrames(Enc
   while (vorbis_analysis_blockout(&mVorbisDsp, &mVorbisBlock) == 1) {
     ogg_packet oggPacket;
     if (vorbis_analysis(&mVorbisBlock, &oggPacket) == 0) {
       VORBISLOG("vorbis_analysis_blockout block size %d", oggPacket.bytes);
       EncodedFrame* audiodata = new EncodedFrame();
       audiodata->SetFrameType(EncodedFrame::AUDIO_FRAME);
       nsTArray<uint8_t> frameData;
       frameData.AppendElements(oggPacket.packet, oggPacket.bytes);
-      audiodata->SetFrameData(&frameData);
+      audiodata->SwapInFrameData(frameData);
       aData.AppendEncodedFrame(audiodata);
     }
   }
 }
 
 nsresult
 VorbisTrackEncoder::GetEncodedTrack(EncodedFrameContainer& aData)
 {
--- a/content/media/encoder/fmp4_muxer/ISOMediaWriter.cpp
+++ b/content/media/encoder/fmp4_muxer/ISOMediaWriter.cpp
@@ -23,20 +23,20 @@ namespace mozilla {
 const static uint32_t FRAG_DURATION = 2000000;    // microsecond per unit
 
 ISOMediaWriter::ISOMediaWriter(uint32_t aType)
   : ContainerWriter()
   , mState(MUXING_HEAD)
   , mBlobReady(false)
   , mType(0)
 {
-  if (aType & HAS_AUDIO) {
+  if (aType & CREATE_AUDIO_TRACK) {
     mType |= Audio_Track;
   }
-  if (aType & HAS_VIDEO) {
+  if (aType & CREATE_VIDEO_TRACK) {
     mType |= Video_Track;
   }
   mControl = new ISOControl();
   MOZ_COUNT_CTOR(ISOMediaWriter);
 }
 
 ISOMediaWriter::~ISOMediaWriter()
 {
--- a/content/media/test/mochitest.ini
+++ b/content/media/test/mochitest.ini
@@ -251,16 +251,17 @@ support-files =
 [test_mozHasAudio.html]
 [test_source_media.html]
 [test_autoplay_contentEditable.html]
 [test_decoder_disable.html]
 [test_mediarecorder_record_no_timeslice.html]
 [test_mediarecorder_reload_crash.html]
 [test_mediarecorder_record_immediate_stop.html]
 [test_mediarecorder_record_session.html]
+[test_mediarecorder_record_startstopstart.html]
 [test_mediarecorder_unsupported_src.html]
 [test_playback.html]
 [test_seekLies.html]
 [test_media_sniffer.html]
 [test_streams_srcObject.html]
 [test_reset_src.html]
 [test_streams_autoplay.html]
 [test_streams_element_capture.html]
new file mode 100644
--- /dev/null
+++ b/content/media/test/test_mediarecorder_record_startstopstart.html
@@ -0,0 +1,76 @@
+ <!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test MediaRecorder crash on sequence start stop start method</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content" style="display: none">
+</div>
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+  var ac = new window.AudioContext();
+  var dest = ac.createMediaStreamDestination();
+  var recorder = new MediaRecorder(dest.stream);
+  var stopCount = 0;
+  var dataavailable = 0;
+  // mobile device may produce another format, but not landed.
+  // In audio only case, we should produce opus type.
+  var expectedMimeType = 'audio/ogg';
+  recorder.onstop = function (e) {
+    info('onstop fired');
+    is(recorder.stream, dest.stream,
+       'Media recorder stream = element stream post recording');
+    stopCount++;
+    if (stopCount == 2) {
+      is(2, dataavailable, 'Should has two dataavailable event');
+      SimpleTest.finish();
+    }
+  }
+  recorder.ondataavailable = function (evt) {
+    info('ondataavailable fired');
+    ok(evt instanceof BlobEvent,
+       'Events fired from ondataavailable should be BlobEvent');
+    is(evt.type, 'dataavailable',
+       'Event type should dataavailable');
+    // If script runs slower, it may generate header data in blob from encoder
+    if (evt.data.size > 0) {
+     info('blob size = ' + evt.data.size);
+     is(evt.data.type, expectedMimeType,
+       'Blob data received should have type = ' + expectedMimeType);
+    } else {
+      is(evt.data.type, '',
+         'Blob data received should have empty type');
+    }
+    dataavailable++;
+  }
+  recorder.onerror = function (e) {
+    ok(false, 'it should execute normally without exception');
+  }
+  recorder.onwarning = function() {
+    ok(false, 'onwarning unexpectedly fired');
+  };
+
+  recorder.start(2000);
+  is(recorder.state, 'recording', 'Media recorder should be recording');
+  recorder.stop();
+  is(recorder.state, 'inactive', 'check recording status is inactive');
+  recorder.start(10000); // This bug would crash on this line without this fix.
+  is(recorder.state, 'recording', 'check recording status is recording');
+  // Simulate delay stop, only delay stop no no stop can trigger crash.
+  setTimeout(function() {
+    recorder.stop();
+    is(recorder.state, 'inactive','check recording status is recording');
+  }, 1000);
+}
+
+startTest();
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
+
--- a/content/media/webm/WebMWriter.cpp
+++ b/content/media/webm/WebMWriter.cpp
@@ -39,25 +39,25 @@ nsresult
 WebMWriter::SetMetadata(TrackMetadataBase* aMetadata)
 {
   MOZ_ASSERT(aMetadata);
   if (aMetadata->GetKind() == TrackMetadataBase::METADATA_VP8) {
     VP8Metadata* meta = static_cast<VP8Metadata*>(aMetadata);
     MOZ_ASSERT(meta, "Cannot find vp8 encoder metadata");
     mEbmlComposer->SetVideoConfig(meta->mWidth, meta->mHeight,
                                   meta->mEncodedFrameRate);
-    mMetadataRequiredFlag = mMetadataRequiredFlag & ~ContainerWriter::HAS_VIDEO;
+    mMetadataRequiredFlag = mMetadataRequiredFlag & ~ContainerWriter::CREATE_VIDEO_TRACK;
   }
 
   if (aMetadata->GetKind() == TrackMetadataBase::METADATA_VORBIS) {
     VorbisMetadata* meta = static_cast<VorbisMetadata*>(aMetadata);
     MOZ_ASSERT(meta, "Cannot find vorbis encoder metadata");
     mEbmlComposer->SetAudioConfig(meta->mSamplingFrequency, meta->mChannels, meta->mBitDepth);
     mEbmlComposer->SetAudioCodecPrivateData(meta->mData);
-    mMetadataRequiredFlag = mMetadataRequiredFlag & ~ContainerWriter::HAS_AUDIO;
+    mMetadataRequiredFlag = mMetadataRequiredFlag & ~ContainerWriter::CREATE_AUDIO_TRACK;
   }
 
   if (!mMetadataRequiredFlag) {
     mEbmlComposer->GenerateHeader();
   }
   return NS_OK;
 }
 
--- a/dom/identity/DOMIdentity.jsm
+++ b/dom/identity/DOMIdentity.jsm
@@ -1,15 +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/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const PREF_FXA_ENABLED = "identity.fxaccounts.enabled";
+let _fxa_enabled = false;
+try {
+  if (Services.prefs.getPrefType(PREF_FXA_ENABLED) === Ci.nsIPrefBranch.PREF_BOOL) {
+    _fxa_enabled = Services.prefs.getBoolPref(PREF_FXA_ENABLED);
+  }
+} catch(noPref) {
+}
+const FXA_ENABLED = _fxa_enabled;
 
 // This is the parent process corresponding to nsDOMIdentity.
 this.EXPORTED_SYMBOLS = ["DOMIdentity"];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
@@ -17,16 +26,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
 #ifdef MOZ_B2G_VERSION
                                   "resource://gre/modules/identity/MinimalIdentity.jsm");
 #else
                                   "resource://gre/modules/identity/Identity.jsm");
 #endif
 
+XPCOMUtils.defineLazyModuleGetter(this, "FirefoxAccounts",
+                                  "resource://gre/modules/identity/FirefoxAccounts.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject",
+                                  "resource://gre/modules/identity/IdentityUtils.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this,
                                   "Logger",
                                   "resource://gre/modules/identity/LogUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                    "@mozilla.org/parentprocessmessagemanager;1",
                                    "nsIMessageListenerManager");
 
@@ -96,19 +111,27 @@ function RPWatchContext(aOptions, aTarge
   // id and origin are required
   if (! (this.id && this.origin)) {
     throw new Error("id and origin are required for RP watch context");
   }
 
   // default for no loggedInUser is undefined, not null
   this.loggedInUser = aOptions.loggedInUser;
 
-  // Maybe internal
+  // Maybe internal.  For hosted b2g identity shim.
   this._internal = aOptions._internal;
 
+  // By default, set the audience of the assertion to the origin of the RP. Bug
+  // 947374 will make it possible for certified apps and packaged apps on
+  // FirefoxOS to request a different audience from their origin.
+  //
+  // For BrowserID on b2g, this audience value is consumed by a hosted identity
+  // shim, set up by b2g/components/SignInToWebsite.jsm.
+  this.audience = this.origin;
+
   this._mm = aTargetMM;
 }
 
 RPWatchContext.prototype = {
   doLogin: function RPWatchContext_onlogin(aAssertion, aMaybeInternalParams) {
     log("doLogin: " + this.id);
     let message = new IDDOMMessage({id: this.id, assertion: aAssertion});
     if (aMaybeInternalParams) {
@@ -136,16 +159,82 @@ RPWatchContext.prototype = {
   },
 
   doError: function RPWatchContext_onerror(aMessage) {
     log("doError: " + aMessage);
   }
 };
 
 this.DOMIdentity = {
+  /*
+   * When relying parties (RPs) invoke the watch() method, they can request
+   * to use Firefox Accounts as their auth service or BrowserID (the default).
+   * For each RP, we create an RPWatchContext to store the parameters given to
+   * watch(), and to provide hooks to invoke the onlogin(), onlogout(), etc.
+   * callbacks held in the nsDOMIdentity state.
+   *
+   * The serviceContexts map associates the window ID of the RP with the
+   * context object.  The mmContexts map associates a message manager with a
+   * window ID.  We use the mmContexts map when child-process-shutdown is
+   * observed, and all we have is a message manager to identify the window in
+   * question.
+   */
+  _serviceContexts: new Map(),
+  _mmContexts: new Map(),
+
+  /*
+   * Create a new RPWatchContext, and update the context maps.
+   */
+  newContext: function(message, targetMM) {
+    let context = new RPWatchContext(message, targetMM);
+    this._serviceContexts.set(message.id, context);
+    this._mmContexts.set(targetMM, message.id);
+    return context;
+  },
+
+  /*
+   * Get the identity service used for an RP.
+   *
+   * @object message
+   *         A message received from an RP.  Will include the id of the window
+   *         whence the message originated.
+   *
+   * Returns FirefoxAccounts or IdentityService
+   */
+  getService: function(message) {
+    if (!this._serviceContexts.has(message.id)) {
+      throw new Error("getService called before newContext for " + message.id);
+    }
+
+    let context = this._serviceContexts.get(message.id);
+    if (context.wantIssuer == "firefox-accounts") {
+      if (FXA_ENABLED) {
+        return FirefoxAccounts;
+      }
+      log("WARNING: Firefox Accounts is not enabled; Defaulting to BrowserID");
+    }
+    return IdentityService;
+  },
+
+  /*
+   * Get the RPWatchContext object for a given message manager.
+   */
+  getContextForMM: function(targetMM) {
+    return this._serviceContexts.get(this._mmContexts.get(targetMM));
+  },
+
+  /*
+   * Delete the RPWatchContext object for a given message manager.  Removes the
+   * mapping both from _serviceContexts and _mmContexts.
+   */
+  deleteContextForMM: function(targetMM) {
+    this._serviceContexts.delete(this._mmContexts.get(targetMM));
+    this._mmContexts.delete(targetMM);
+  },
+
   // nsIMessageListener
   receiveMessage: function DOMIdentity_receiveMessage(aMessage) {
     let msg = aMessage.json;
 
     // Target is the frame message manager that called us and is
     // used to send replies back to the proper window.
     let targetMM = aMessage.target;
 
@@ -230,75 +319,69 @@ this.DOMIdentity = {
 
   _unsubscribeListeners: function DOMIdentity__unsubscribeListeners() {
     for (let message of this.messages) {
       ppmm.removeMessageListener(message, this);
     }
     ppmm = null;
   },
 
-  _resetFrameState: function(aContext) {
-    log("_resetFrameState: ", aContext.id);
-    if (!aContext._mm) {
-      throw new Error("ERROR: Trying to reset an invalid context");
-    }
-    let message = new IDDOMMessage({id: aContext.id});
-    aContext._mm.sendAsyncMessage("Identity:ResetState", message);
-  },
-
   _watch: function DOMIdentity__watch(message, targetMM) {
     log("DOMIdentity__watch: " + message.id);
-    // Pass an object with the watch members to Identity.jsm so it can call the
-    // callbacks.
-    let context = new RPWatchContext(message, targetMM);
-    IdentityService.RP.watch(context);
+    let context = this.newContext(message, targetMM);
+    this.getService(message).RP.watch(context);
   },
 
   _unwatch: function DOMIdentity_unwatch(message, targetMM) {
-    IdentityService.RP.unwatch(message.id, targetMM);
+    this.getService(message).RP.unwatch(message.id, targetMM);
   },
 
   _request: function DOMIdentity__request(message) {
-    IdentityService.RP.request(message.id, message);
+    this.getService(message).RP.request(message.id, message);
   },
 
   _logout: function DOMIdentity__logout(message) {
-    IdentityService.RP.logout(message.id, message.origin, message);
+    log("logout " + message + "\n");
+    this.getService(message).RP.logout(message.id, message.origin, message);
   },
 
   _childProcessShutdown: function DOMIdentity__childProcessShutdown(targetMM) {
-    IdentityService.RP.childProcessShutdown(targetMM);
+    this.getContextForMM(targetMM).RP.childProcessShutdown(targetMM);
+    this.deleteContextForMM(targetMM);
+
+    let options = makeMessageObject({messageManager: targetMM, id: null, origin: null});
+    Services.obs.notifyObservers({wrappedJSObject: options}, "identity-child-process-shutdown", null);
   },
 
   _beginProvisioning: function DOMIdentity__beginProvisioning(message, targetMM) {
     let context = new IDPProvisioningContext(message.id, message.origin,
                                              targetMM);
-    IdentityService.IDP.beginProvisioning(context);
+    this.getService(message).IDP.beginProvisioning(context);
   },
 
   _genKeyPair: function DOMIdentity__genKeyPair(message) {
-    IdentityService.IDP.genKeyPair(message.id);
+    this.getService(message).IDP.genKeyPair(message.id);
   },
 
   _registerCertificate: function DOMIdentity__registerCertificate(message) {
-    IdentityService.IDP.registerCertificate(message.id, message.cert);
+    this.getService(message).IDP.registerCertificate(message.id, message.cert);
   },
 
   _provisioningFailure: function DOMIdentity__provisioningFailure(message) {
-    IdentityService.IDP.raiseProvisioningFailure(message.id, message.reason);
+    this.getService(message).IDP.raiseProvisioningFailure(message.id, message.reason);
   },
 
   _beginAuthentication: function DOMIdentity__beginAuthentication(message, targetMM) {
     let context = new IDPAuthenticationContext(message.id, message.origin,
                                                targetMM);
-    IdentityService.IDP.beginAuthentication(context);
+    this.getService(message).IDP.beginAuthentication(context);
   },
 
   _completeAuthentication: function DOMIdentity__completeAuthentication(message) {
-    IdentityService.IDP.completeAuthentication(message.id);
+    this.getService(message).IDP.completeAuthentication(message.id);
   },
 
   _authenticationFailure: function DOMIdentity__authenticationFailure(message) {
-    IdentityService.IDP.cancelAuthentication(message.id);
+    this.getService(message).IDP.cancelAuthentication(message.id);
   }
 };
 
 // Object is initialized by nsIDService.js
--- a/dom/identity/nsDOMIdentity.js
+++ b/dom/identity/nsDOMIdentity.js
@@ -71,36 +71,50 @@ nsDOMIdentity.prototype = {
   },
 
   /**
    * Relying Party (RP) APIs
    */
 
   watch: function nsDOMIdentity_watch(aOptions) {
     if (this._rpWatcher) {
+      // For the initial release of Firefox Accounts, we support callers who
+      // invoke watch() either for Firefox Accounts, or Persona, but not both.
+      // In the future, we may wish to support the dual invocation (say, for
+      // packaged apps so they can sign users in who reject the app's request
+      // to sign in with their Firefox Accounts identity).
       throw new Error("navigator.id.watch was already called");
     }
 
     if (!aOptions || typeof(aOptions) !== "object") {
       throw new Error("options argument to watch is required");
     }
 
-    // Check for required callbacks
-    let requiredCallbacks = ["onlogin", "onlogout"];
-    for (let cbName of requiredCallbacks) {
-      if ((!(cbName in aOptions))
-          || typeof(aOptions[cbName]) !== "function") {
-           throw new Error(cbName + " callback is required.");
-         }
+    // The relying party (RP) provides callbacks on watch().
+    //
+    // In the future, BrowserID will probably only require an onlogin()
+    // callback [1], lifting the requirement that BrowserID handle logged-in
+    // state management for RPs.  See
+    // https://github.com/mozilla/id-specs/blob/greenfield/browserid/api-rp.md
+    //
+    // However, Firefox Accounts will almost certainly require RPs to provide
+    // onlogout(), onready(), and possibly an onerror() callback.
+    // XXX Bug 945278
+    //
+    // To accomodate the more and less lenient uses of the API, we will simply
+    // be strict about checking for onlogin here.
+    if (typeof(aOptions["onlogin"]) != "function") {
+      throw new Error("onlogin() callback is required.");
     }
 
-    // Optional callback "onready"
-    if (aOptions["onready"]
-        && typeof(aOptions['onready']) !== "function") {
-      throw new Error("onready must be a function");
+    // Optional callbacks
+    for (let cb of ["onready", "onlogout"]) {
+      if (aOptions[cb] && typeof(aOptions[cb]) != "function") {
+        throw new Error(cb + " must be a function");
+      }
     }
 
     let message = this.DOMIdentityMessage(aOptions);
 
     // loggedInUser vs loggedInEmail
     // https://developer.mozilla.org/en-US/docs/DOM/navigator.id.watch
     // This parameter, loggedInUser, was renamed from loggedInEmail in early
     // September, 2012. Both names will continue to work for the time being,
@@ -374,16 +388,17 @@ nsDOMIdentity.prototype = {
   // Private.
   _init: function nsDOMIdentity__init(aWindow) {
 
     this._initializeState();
 
     // Store window and origin URI.
     this._window = aWindow;
     this._origin = aWindow.document.nodePrincipal.origin;
+    this._appStatus = aWindow.document.nodePrincipal.appStatus;
 
     // Setup identifiers for current window.
     let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDOMWindowUtils);
 
     // We need to inherit the id from the internalIdentity service.
     // See comments below in that service's init.
     this._id = this._identityInternal._id;
@@ -531,16 +546,21 @@ nsDOMIdentity.prototype = {
     objectCopy(aOptions, message);
 
     // outer window id
     message.id = this._id;
 
     // window origin
     message.origin = this._origin;
 
+    // On b2g, an app's status can be NOT_INSTALLED, INSTALLED, PRIVILEGED, or
+    // CERTIFIED.  Compare the appStatus value to the constants enumerated in
+    // Ci.nsIPrincipal.APP_STATUS_*.
+    message.appStatus = this._appStatus;
+
     return message;
   },
 
   uninit: function DOMIdentity_uninit() {
     this._log("nsDOMIdentity uninit()");
     this._identityInternal._mm.sendAsyncMessage(
       "Identity:RP:Unwatch",
       { id: this._id }
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -899,32 +899,32 @@ nsMenuPopupFrame::AdjustPositionForAncho
   }
 
   // first, determine at which corner of the anchor the popup should appear
   nsPoint pnt;
   switch (popupAnchor) {
     case POPUPALIGNMENT_LEFTCENTER:
       pnt = nsPoint(anchorRect.x, anchorRect.y + anchorRect.height / 2);
       anchorRect.y = pnt.y;
-      anchorRect.height = 0;
+      anchorRect.height = 1;
       break;
     case POPUPALIGNMENT_RIGHTCENTER:
       pnt = nsPoint(anchorRect.XMost(), anchorRect.y + anchorRect.height / 2);
       anchorRect.y = pnt.y;
-      anchorRect.height = 0;
+      anchorRect.height = 1;
       break;
     case POPUPALIGNMENT_TOPCENTER:
       pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.y);
       anchorRect.x = pnt.x;
-      anchorRect.width = 0;
+      anchorRect.width = 1;
       break;
     case POPUPALIGNMENT_BOTTOMCENTER:
       pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.YMost());
       anchorRect.x = pnt.x;
-      anchorRect.width = 0;
+      anchorRect.width = 1;
       break;
     case POPUPALIGNMENT_TOPRIGHT:
       pnt = anchorRect.TopRight();
       break;
     case POPUPALIGNMENT_BOTTOMLEFT:
       pnt = anchorRect.BottomLeft();
       break;
     case POPUPALIGNMENT_BOTTOMRIGHT:
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1652,17 +1652,17 @@ abstract public class BrowserApp extends
         // Show the toolbar before hiding about:home so the
         // onMetricsChanged callback still works.
         if (isDynamicToolbarEnabled() && mLayerView != null) {
             mLayerView.getLayerMarginsAnimator().showMargins(true);
         }
 
         if (mHomePager == null) {
             final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
-            mHomePager = (HomePager) homePagerStub.inflate();
+            mHomePager = (HomePager) homePagerStub.inflate().findViewById(R.id.home_pager);
         }
 
         mHomePager.show(getSupportLoaderManager(),
                         getSupportFragmentManager(),
                         pageId, animator);
 
         // Hide the web content so it cannot be focused by screen readers.
         hideWebContentOnPropertyAnimationEnd(animator);
--- a/mobile/android/base/home/HomeBanner.java
+++ b/mobile/android/base/home/HomeBanner.java
@@ -1,15 +1,19 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
+import org.mozilla.gecko.animation.PropertyAnimator;
+import org.mozilla.gecko.animation.PropertyAnimator.Property;
+import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
+import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import org.json.JSONException;
@@ -18,121 +22,229 @@ import org.json.JSONObject;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.text.Html;
 import android.text.Spanned;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class HomeBanner extends LinearLayout
                         implements GeckoEventListener {
     private static final String LOGTAG = "GeckoHomeBanner";
 
+    final TextView mTextView;
+    final ImageView mIconView;
+    final ImageButton mCloseButton;
+
+    // Used for tracking scroll length
+    private float mTouchY = -1;
+
+    // Used to detect for upwards scroll to push banner all the way up
+    private boolean mSnapBannerToTop;
+
+    // Used so that we don't move the banner when scrolling between pages
+    private boolean mScrollingPages = false;
+
+    // User has dismissed the banner using the close button
+    private boolean mDismissed = false;
+
     public HomeBanner(Context context) {
         this(context, null);
     }
 
     public HomeBanner(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         LayoutInflater.from(context).inflate(R.layout.home_banner, this);
-    }
+        mTextView = (TextView) findViewById(R.id.text);
+        mIconView = (ImageView) findViewById(R.id.icon);
+        mCloseButton = (ImageButton) findViewById(R.id.close);
 
-    @Override
-    public void onAttachedToWindow() {
-        super.onAttachedToWindow();
-
+        mCloseButton.getDrawable().setAlpha(127);
         // Tapping on the close button will ensure that the banner is never
         // showed again on this session.
-        final ImageButton closeButton = (ImageButton) findViewById(R.id.close);
-
-        // The drawable should have 50% opacity.
-        closeButton.getDrawable().setAlpha(127);
-
-        closeButton.setOnClickListener(new View.OnClickListener() {
+        mCloseButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
-                HomeBanner.this.setVisibility(View.GONE);
+                animateDown();
+                mDismissed = true;
             }
         });
 
         setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 // Send the current message id back to JS.
-                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Click", (String) getTag()));
+                GeckoAppShell.sendEventToGecko(
+                    GeckoEvent.createBroadcastEvent("HomeBanner:Click",(String) getTag()));
             }
         });
+    }
 
+    @Override
+    public void onAttachedToWindow() {
         GeckoAppShell.getEventDispatcher().registerEventListener("HomeBanner:Data", this);
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null));
     }
 
     @Override
     public void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
+        GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this);
+    }
 
-        GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this);
-     }
+    public void showBanner() {
+        if (!mDismissed) {
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null));
+        }
+    }
 
-    public boolean isDismissed() {
-        return (getVisibility() == View.GONE);
+    public void hideBanner() {
+        animateDown();
+    }
+
+    public void setScrollingPages(boolean scrollingPages) {
+        mScrollingPages = scrollingPages;
     }
 
     @Override
-    public void handleMessage(String event, JSONObject message) {
-        try {
-            // Store the current message id to pass back to JS in the view's OnClickListener.
-            setTag(message.getString("id"));
-
-            // Display styled text from an HTML string.
-            final Spanned text = Html.fromHtml(message.getString("text"));
-            final TextView textView = (TextView) findViewById(R.id.text);
+    public void handleMessage(final String event, final JSONObject message) {
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    // Store the current message id to pass back to JS in the view's OnClickListener.
+                    setTag(message.getString("id"));
+                    setText(message.getString("text"));
+                    setIcon(message.optString("iconURI"));
+                    animateUp();
+                } catch (JSONException e) {
+                    Log.e(LOGTAG, "Exception handling " + event + " message", e);
+                }
+            }
+        });
+    }
 
-            // Update the banner message on the UI thread.
-            ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    textView.setText(text);
-                    setVisibility(View.VISIBLE);
-                }
-            });
-        } catch (JSONException e) {
-            Log.e(LOGTAG, "Exception handling " + event + " message", e);
-            return;
-        }
+    private void setText(String text) {
+        // Display styled text from an HTML string.
+        final Spanned html = Html.fromHtml(text);
 
-        final String iconURI = message.optString("iconURI");
-        final ImageView iconView = (ImageView) findViewById(R.id.icon);
+        // Update the banner message on the UI thread.
+        mTextView.setText(html);
+    }
 
+    private void setIcon(String iconURI) {
         if (TextUtils.isEmpty(iconURI)) {
             // Hide the image view if we don't have an icon to show.
-            iconView.setVisibility(View.GONE);
+            mIconView.setVisibility(View.GONE);
             return;
         }
 
         BitmapUtils.getDrawable(getContext(), iconURI, new BitmapUtils.BitmapLoader() {
             @Override
             public void onBitmapFound(final Drawable d) {
                 // Bail if getDrawable doesn't find anything.
                 if (d == null) {
-                    iconView.setVisibility(View.GONE);
+                    mIconView.setVisibility(View.GONE);
                     return;
                 }
 
-                // Update the banner icon on the UI thread.
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        iconView.setImageDrawable(d);
-                    }
-                });
+                // Update the banner icon
+                mIconView.setImageDrawable(d);
+            }
+        });
+    }
+
+    private void animateDown() {
+        // No need to animate if already translated.
+        if (getVisibility() == GONE && ViewHelper.getTranslationY(this) == getHeight()) {
+            return;
+        }
+
+        final PropertyAnimator animator = new PropertyAnimator(100);
+        animator.attach(this, Property.TRANSLATION_Y, getHeight());
+        animator.start();
+        animator.addPropertyAnimationListener(new PropertyAnimationListener() {
+            @Override
+            public void onPropertyAnimationStart() {}
+            public void onPropertyAnimationEnd() {
+                HomeBanner.this.setVisibility(GONE);
             }
         });
     }
+
+    private void animateUp() {
+        // No need to animate if already translated.
+        if (getVisibility() == VISIBLE && ViewHelper.getTranslationY(this) == 0) {
+            return;
+        }
+
+        setVisibility(View.VISIBLE);
+        final PropertyAnimator animator = new PropertyAnimator(100);
+        animator.attach(this, Property.TRANSLATION_Y, 0);
+        animator.start();
+    }
+
+    /**
+     * Touches to the HomePager are forwarded here to handle the hiding / showing of the banner
+     * on scroll.
+     */
+    public void handleHomeTouch(MotionEvent event) {
+        if (mDismissed || mScrollingPages) {
+            return;
+        }
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN: {
+                mTouchY = event.getRawY();
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                // There is a chance that we won't receive ACTION_DOWN, if the touch event
+                // actually started on the Grid instead of the List. Treat this as first event.
+                if (mTouchY == -1) {
+                    mTouchY = event.getRawY();
+                    return;
+                }
+
+                final float curY = event.getRawY();
+                final float delta = mTouchY - curY;
+                mSnapBannerToTop = delta <= 0.0f;
+
+                final float height = getHeight();
+                float newTranslationY = ViewHelper.getTranslationY(this) + delta;
+
+                // Clamp the values to be between 0 and height.
+                if (newTranslationY < 0.0f) {
+                    newTranslationY = 0.0f;
+                } else if (newTranslationY > height) {
+                    newTranslationY = height;
+                }
+
+                ViewHelper.setTranslationY(this, newTranslationY);
+                mTouchY = curY;
+                break;
+            }
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL: {
+                mTouchY = -1;
+                final float y = ViewHelper.getTranslationY(this);
+                final float height = getHeight();
+                if (y > 0.0f && y < height) {
+                    if (mSnapBannerToTop) {
+                        animateUp();
+                    } else {
+                        animateDown();
+                    }
+                }
+                break;
+            }
+        }
+    }
 }
--- a/mobile/android/base/home/HomeConfig.java
+++ b/mobile/android/base/home/HomeConfig.java
@@ -383,17 +383,18 @@ public final class HomeConfig {
             @Override
             public LayoutType[] newArray(final int size) {
                 return new LayoutType[size];
             }
         };
     }
 
     public static enum ViewType implements Parcelable {
-        LIST("list");
+        LIST("list"),
+        GRID("grid");
 
         private final String mId;
 
         ViewType(String id) {
             mId = id;
         }
 
         public static ViewType fromId(String id) {
--- a/mobile/android/base/home/HomePager.java
+++ b/mobile/android/base/home/HomePager.java
@@ -19,39 +19,44 @@ import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
 import android.support.v4.view.ViewPager;
 import android.view.ViewGroup.LayoutParams;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewGroup;
+import android.view.ViewParent;
 import android.view.View;
 
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
 
 public class HomePager extends ViewPager {
+    private static final String LOGTAG = "GeckoHomePager";
 
     private static final int LOADER_ID_CONFIG = 0;
 
     private final Context mContext;
     private volatile boolean mLoaded;
     private Decor mDecor;
     private View mTabStrip;
+    private HomeBanner mHomeBanner;
 
     private final OnAddPanelListener mAddPanelListener;
 
     private final HomeConfig mConfig;
     private ConfigLoaderCallbacks mConfigLoaderCallbacks;
 
     private String mInitialPanelId;
+    private int mDefaultPanelIndex;
 
     // Whether or not we need to restart the loader when we show the HomePager.
     private boolean mRestartLoader;
 
     // This is mostly used by UI tests to easily fetch
     // specific list views at runtime.
     static final String LIST_TAG_HISTORY = "history";
     static final String LIST_TAG_BOOKMARKS = "bookmarks";
@@ -233,27 +238,40 @@ public class HomePager extends ViewPager
             });
 
             ViewHelper.setAlpha(this, 0.0f);
 
             animator.attach(this,
                             PropertyAnimator.Property.ALPHA,
                             1.0f);
         }
+
+        // Setup banner and decor listeners
+        mHomeBanner = (HomeBanner) ((ViewGroup) getParent()).findViewById(R.id.home_banner);
+        setOnPageChangeListener(new HomePagerOnPageChangeListener());
     }
 
     /**
      * Hides the pager and removes all child fragments.
      */
     public void hide() {
         mLoaded = false;
         setVisibility(GONE);
         setAdapter(null);
     }
 
+    @Override
+    public void setVisibility(int visibility) {
+        // Ensure that no decorations are overlaying the mainlayout
+        if (mHomeBanner != null) {
+            mHomeBanner.setVisibility(visibility);
+        }
+        super.setVisibility(visibility);
+    }
+
     /**
      * Determines whether the pager is visible.
      *
      * Unlike getVisibility(), this method does not need to be called on the UI
      * thread.
      *
      * @return Whether the pager and its fragments are being displayed
      */
@@ -263,28 +281,45 @@ public class HomePager extends ViewPager
 
     @Override
     public void setCurrentItem(int item, boolean smoothScroll) {
         super.setCurrentItem(item, smoothScroll);
 
         if (mDecor != null) {
             mDecor.onPageSelected(item);
         }
+        if (mHomeBanner != null) {
+            if (item == mDefaultPanelIndex) {
+                mHomeBanner.showBanner();
+            } else {
+                mHomeBanner.hideBanner();
+            }
+        }
     }
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
             // Drop the soft keyboard by stealing focus from the URL bar.
             requestFocus();
         }
 
         return super.onInterceptTouchEvent(event);
     }
 
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        // Get touches to pages, pass to banner, and forward to pages.
+        if (mHomeBanner != null) {
+            mHomeBanner.handleHomeTouch(event);
+        }
+
+        return super.dispatchTouchEvent(event);
+    }
+
     private void updateUiFromPanelConfigs(List<PanelConfig> panelConfigs) {
         // We only care about the adapter if HomePager is currently
         // loaded, which means it's visible in the activity.
         if (!mLoaded) {
             return;
         }
 
         if (mDecor != null) {
@@ -298,16 +333,19 @@ public class HomePager extends ViewPager
         setAdapter(null);
 
         // Only keep enabled panels.
         final List<PanelConfig> enabledPanels = new ArrayList<PanelConfig>();
 
         for (PanelConfig panelConfig : panelConfigs) {
             if (!panelConfig.isDisabled()) {
                 enabledPanels.add(panelConfig);
+                if (panelConfig.isDefault()) {
+                    mDefaultPanelIndex = enabledPanels.size() - 1;
+                }
             }
         }
 
         // Update the adapter with the new panel configs
         adapter.update(enabledPanels);
 
         // Hide the tab strip if the new configuration contains no panels.
         final int count = enabledPanels.size();
@@ -343,9 +381,40 @@ public class HomePager extends ViewPager
         public void onLoadFinished(Loader<List<PanelConfig>> loader, List<PanelConfig> panelConfigs) {
             updateUiFromPanelConfigs(panelConfigs);
         }
 
         @Override
         public void onLoaderReset(Loader<List<PanelConfig>> loader) {
         }
     }
+
+    private class HomePagerOnPageChangeListener implements ViewPager.OnPageChangeListener {
+        @Override
+        public void onPageSelected(int position) {
+            if (mDecor != null) {
+                mDecor.onPageSelected(position);
+            }
+
+            if (mHomeBanner != null) {
+                if (position == mDefaultPanelIndex) {
+                    mHomeBanner.showBanner();
+                } else {
+                    mHomeBanner.hideBanner();
+                }
+            }
+        }
+
+        @Override
+        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+            if (mDecor != null) {
+                mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels);
+            }
+
+            if (mHomeBanner != null) {
+                mHomeBanner.setScrollingPages(positionOffsetPixels > 0);
+            }
+        }
+
+        @Override
+        public void onPageScrollStateChanged(int state) { }
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/PanelGridItemView.java
@@ -0,0 +1,52 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.home;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.mozilla.gecko.db.BrowserContract.URLColumns;
+import org.mozilla.gecko.favicons.Favicons;
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+public class PanelGridItemView extends FrameLayout {
+    private static final String LOGTAG = "GeckoPanelGridItemView";
+
+    private final ImageView mThumbnailView;
+
+    public PanelGridItemView(Context context) {
+        this(context, null);
+    }
+
+    public PanelGridItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PanelGridItemView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        LayoutInflater.from(context).inflate(R.layout.panel_grid_item_view, this);
+        mThumbnailView = (ImageView) findViewById(R.id.image);
+        mThumbnailView.setBackgroundColor(Color.rgb(255, 148, 0));
+    }
+
+    public void updateFromCursor(Cursor cursor) { }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/PanelGridView.java
@@ -0,0 +1,54 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.home;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.home.HomeConfig.ViewConfig;
+import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.widget.CursorAdapter;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.GridView;
+
+public class PanelGridView extends GridView implements DatasetBacked {
+    private static final String LOGTAG = "GeckoPanelGridView";
+
+    private final PanelGridViewAdapter mAdapter;
+
+    public PanelGridView(Context context, ViewConfig viewConfig) {
+        super(context, null, R.attr.homeGridViewStyle);
+        mAdapter = new PanelGridViewAdapter(context);
+        setAdapter(mAdapter);
+        setNumColumns(AUTO_FIT);
+    }
+
+    @Override
+    public void setDataset(Cursor cursor) {
+        mAdapter.swapCursor(cursor);
+    }
+
+    private class PanelGridViewAdapter extends CursorAdapter {
+
+        public PanelGridViewAdapter(Context context) {
+            super(context, null, 0);
+        }
+
+        @Override
+        public void bindView(View bindView, Context context, Cursor cursor) {
+            final PanelGridItemView item = (PanelGridItemView) bindView;
+            item.updateFromCursor(cursor);
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            return new PanelGridItemView(context);
+        }
+    }
+}
--- a/mobile/android/base/home/PanelLayout.java
+++ b/mobile/android/base/home/PanelLayout.java
@@ -139,16 +139,20 @@ abstract class PanelLayout extends Frame
 
         Log.d(LOGTAG, "Creating panel view: " + viewConfig.getType());
 
         switch(viewConfig.getType()) {
             case LIST:
                 view = new PanelListView(getContext(), viewConfig);
                 break;
 
+            case GRID:
+                view = new PanelGridView(getContext(), viewConfig);
+                break;
+
             default:
                 throw new IllegalStateException("Unrecognized view type in " + getClass().getSimpleName());
         }
 
         final ViewEntry entry = new ViewEntry(view, viewConfig);
         mViewEntries.add(entry);
 
         return view;
--- a/mobile/android/base/home/TopSitesPanel.java
+++ b/mobile/android/base/home/TopSitesPanel.java
@@ -82,25 +82,16 @@ public class TopSitesPanel extends HomeF
     private TopSitesGridAdapter mGridAdapter;
 
     // List of top sites
     private ListView mList;
 
     // Grid of top sites
     private TopSitesGridView mGrid;
 
-    // Banner to show snippets.
-    private HomeBanner mBanner;
-
-    // Raw Y value of the last event that happened on the list view.
-    private float mListTouchY = -1;
-
-    // Scrolling direction of the banner.
-    private boolean mSnapBannerToTop;
-
     // Callbacks used for the search and favicon cursor loaders
     private CursorLoaderCallbacks mCursorLoaderCallbacks;
 
     // Callback for thumbnail loader
     private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks;
 
     // Listener for editing pinned sites.
     private EditPinnedSiteListener mEditPinnedSiteListener;
@@ -202,25 +193,16 @@ public class TopSitesPanel extends HomeF
             }
         });
 
         mGrid.setOnUrlOpenListener(mUrlOpenListener);
         mGrid.setOnEditPinnedSiteListener(mEditPinnedSiteListener);
 
         registerForContextMenu(mList);
         registerForContextMenu(mGrid);
-
-        mBanner = (HomeBanner) view.findViewById(R.id.home_banner);
-        mList.setOnTouchListener(new OnTouchListener() {
-            @Override
-            public boolean onTouch(View v, MotionEvent event) {
-                TopSitesPanel.this.handleListTouchEvent(event);
-                return false;
-            }
-        });
     }
 
     @Override
     public void onDestroyView() {
         super.onDestroyView();
 
         // Discard any additional item clicks on the list
         // as the panel is getting destroyed (see bug 930160).
@@ -449,70 +431,16 @@ public class TopSitesPanel extends HomeF
                 @Override
                 public void run() {
                     BrowserDB.pinSite(context.getContentResolver(), url, title, position);
                 }
             });
         }
     }
 
-    private void handleListTouchEvent(MotionEvent event) {
-        // Ignore the event if the banner is hidden for this session.
-        if (mBanner.isDismissed()) {
-            return;
-        }
-
-        switch (event.getActionMasked()) {
-            case MotionEvent.ACTION_DOWN: {
-                mListTouchY = event.getRawY();
-                break;
-            }
-
-            case MotionEvent.ACTION_MOVE: {
-                // There is a chance that we won't receive ACTION_DOWN, if the touch event
-                // actually started on the Grid instead of the List. Treat this as first event.
-                if (mListTouchY == -1) {
-                    mListTouchY = event.getRawY();
-                    return;
-                }
-
-                final float curY = event.getRawY();
-                final float delta = mListTouchY - curY;
-                mSnapBannerToTop = (delta > 0.0f) ? false : true;
-
-                final float height = mBanner.getHeight();
-                float newTranslationY = ViewHelper.getTranslationY(mBanner) + delta;
-
-                // Clamp the values to be between 0 and height.
-                if (newTranslationY < 0.0f) {
-                    newTranslationY = 0.0f;
-                } else if (newTranslationY > height) {
-                    newTranslationY = height;
-                }
-
-                ViewHelper.setTranslationY(mBanner, newTranslationY);
-                mListTouchY = curY;
-                break;
-            }
-
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_CANCEL: {
-                mListTouchY = -1;
-                final float y = ViewHelper.getTranslationY(mBanner);
-                final float height = mBanner.getHeight();
-                if (y > 0.0f && y < height) {
-                    final PropertyAnimator animator = new PropertyAnimator(100);
-                    animator.attach(mBanner, Property.TRANSLATION_Y, mSnapBannerToTop ? 0 : height);
-                    animator.start();
-                }
-                break;
-            }
-        }
-    }
-
     private void updateUiFromCursor(Cursor c) {
         mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries);
     }
 
     private static class TopSitesLoader extends SimpleCursorLoader {
         // Max number of search results
         private static final int SEARCH_LIMIT = 30;
         private int mMaxGridEntries;
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -224,16 +224,18 @@ gbjar.sources += [
     'home/HomeConfigPrefsBackend.java',
     'home/HomeFragment.java',
     'home/HomeListView.java',
     'home/HomePager.java',
     'home/HomePagerTabStrip.java',
     'home/LastTabsPanel.java',
     'home/MostRecentPanel.java',
     'home/MultiTypeCursorAdapter.java',
+    'home/PanelGridItemView.java',
+    'home/PanelGridView.java',
     'home/PanelLayout.java',
     'home/PanelListRow.java',
     'home/PanelListView.java',
     'home/PanelManager.java',
     'home/PinSiteDialog.java',
     'home/ReadingListPanel.java',
     'home/SearchEngine.java',
     'home/SearchEngineRow.java',
--- a/mobile/android/base/resources/layout-large-v11/home_pager.xml
+++ b/mobile/android/base/resources/layout-large-v11/home_pager.xml
@@ -9,15 +9,16 @@
 <org.mozilla.gecko.home.HomePager xmlns:android="http://schemas.android.com/apk/res/android"
                                   xmlns:gecko="http://schemas.android.com/apk/res-auto"
                                   android:id="@+id/home_pager"
                                   android:layout_width="fill_parent"
                                   android:layout_height="fill_parent"
                                   android:background="@android:color/white"
                                   android:visibility="gone">
 
-    <org.mozilla.gecko.home.TabMenuStrip android:layout_width="fill_parent"
+    <org.mozilla.gecko.home.TabMenuStrip android:id="@+id/tablet_menu_strip"
+                                         android:layout_width="fill_parent"
                                          android:layout_height="32dip"
                                          android:background="@color/background_light"
                                          android:layout_gravity="top"
                                          gecko:strip="@drawable/home_tab_menu_strip"/>
 
 </org.mozilla.gecko.home.HomePager>
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -31,16 +31,26 @@
                          android:layout_width="fill_parent"
                          android:layout_height="fill_parent">
 
                 <ViewStub android:id="@+id/home_pager_stub"
                           android:layout="@layout/home_pager"
                           android:layout_width="fill_parent"
                           android:layout_height="fill_parent"/>
 
+                <org.mozilla.gecko.home.HomeBanner android:id="@+id/home_banner"
+                                                   style="@style/Widget.HomeBanner"
+                                                   android:layout_width="fill_parent"
+                                                   android:layout_height="@dimen/home_banner_height"
+                                                   android:background="@drawable/home_banner"
+                                                   android:layout_gravity="bottom"
+                                                   android:gravity="center_vertical"
+                                                   android:visibility="gone"
+                                                   android:clickable="true"
+                                                   android:focusable="true"/>
 
             </FrameLayout>
 
         </RelativeLayout>
 
         <org.mozilla.gecko.FindInPageBar android:id="@+id/find_in_page"
                                          android:layout_width="fill_parent"
                                          android:layout_height="wrap_content"
--- a/mobile/android/base/resources/layout/home_pager.xml
+++ b/mobile/android/base/resources/layout/home_pager.xml
@@ -9,17 +9,18 @@
 <org.mozilla.gecko.home.HomePager xmlns:android="http://schemas.android.com/apk/res/android"
                                   xmlns:gecko="http://schemas.android.com/apk/res-auto"
                                   android:id="@+id/home_pager"
                                   android:layout_width="fill_parent"
                                   android:layout_height="fill_parent"
                                   android:background="@android:color/white"
                                   android:visibility="gone">
 
-    <org.mozilla.gecko.home.HomePagerTabStrip android:layout_width="fill_parent"
+    <org.mozilla.gecko.home.HomePagerTabStrip android:id="@+id/phone_menu_strip"
+                                              android:layout_width="fill_parent"
                                               android:layout_height="32dip"
                                               android:layout_gravity="top"
                                               android:gravity="bottom"
                                               android:background="@color/background_light"
                                               gecko:tabIndicatorColor="@color/text_color_highlight"
                                               android:textAppearance="@style/TextAppearance.Widget.HomePagerTabStrip"/>
 
 </org.mozilla.gecko.home.HomePager>
--- a/mobile/android/base/resources/layout/home_top_sites_panel.xml
+++ b/mobile/android/base/resources/layout/home_top_sites_panel.xml
@@ -1,28 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-             android:layout_width="fill_parent"
-             android:layout_height="fill_parent"
-             android:orientation="vertical">
-
-    <org.mozilla.gecko.home.HomeListView
-            android:id="@+id/list"
-            style="@style/Widget.TopSitesListView"
-            android:layout_width="fill_parent"
-            android:layout_height="fill_parent"/>
-
-    <org.mozilla.gecko.home.HomeBanner android:id="@+id/home_banner"
-                                       style="@style/Widget.HomeBanner"
-                                       android:layout_width="fill_parent"
-                                       android:layout_height="@dimen/home_banner_height"
-                                       android:background="@drawable/home_banner"
-                                       android:layout_gravity="bottom"
-                                       android:gravity="center_vertical"
-                                       android:visibility="gone"
-                                       android:clickable="true"
-                                       android:focusable="true"/>
-
-</FrameLayout>
+<org.mozilla.gecko.home.HomeListView xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/list"
+        style="@style/Widget.TopSitesListView"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/panel_grid_item_view.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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+    <ImageView android:id="@+id/image"
+               android:layout_width="fill_parent"
+               android:layout_height="80dp"
+               android:layout_marginRight="5dp" />
+
+</merge>
--- a/mobile/android/base/resources/values-large-land-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-land-v11/styles.xml
@@ -23,24 +23,26 @@
     <style name="Widget.BookmarksListView" parent="Widget.HomeListView">
         <item name="android:paddingTop">30dp</item>
         <item name="android:paddingLeft">120dp</item>
         <item name="android:paddingRight">120dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
         <item name="topDivider">true</item>
     </style>
 
-    <style name="Widget.TopSitesGridView" parent="Widget.GridView">
+    <style name="Widget.HomeGridView" parent="Widget.GridView">
         <item name="android:paddingLeft">55dp</item>
         <item name="android:paddingRight">55dp</item>
         <item name="android:paddingBottom">30dp</item>
         <item name="android:horizontalSpacing">20dp</item>
         <item name="android:verticalSpacing">20dp</item>
     </style>
 
+    <style name="Widget.TopSitesGridView" parent="Widget.HomeGridView" />
+
     <style name="Widget.Home.HistoryListView">
         <item name="android:paddingLeft">50dp</item>
         <item name="android:paddingRight">100dp</item>
         <item name="android:paddingTop">30dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
         <item name="topDivider">true</item>
     </style>
 
--- a/mobile/android/base/resources/values-large-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-v11/styles.xml
@@ -72,24 +72,26 @@
     <style name="Widget.BookmarksListView" parent="Widget.HomeListView">
         <item name="android:paddingTop">30dp</item>
         <item name="android:paddingLeft">32dp</item>
         <item name="android:paddingRight">32dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
         <item name="topDivider">true</item>
     </style>
 
-    <style name="Widget.TopSitesGridView" parent="Widget.GridView">
+    <style name="Widget.HomeGridView" parent="Widget.GridView">
         <item name="android:paddingLeft">5dp</item>
         <item name="android:paddingRight">5dp</item>
         <item name="android:paddingBottom">30dp</item>
         <item name="android:horizontalSpacing">10dp</item>
         <item name="android:verticalSpacing">10dp</item>
     </style>
 
+    <style name="Widget.TopSitesGridView" parent="Widget.HomeGridView" />
+
     <style name="Widget.TopSitesListView" parent="Widget.BookmarksListView">
         <item name="topDivider">false</item>
     </style>
 
     <style name="Widget.ReadingListView" parent="Widget.BookmarksListView"/>
 
     <style name="Widget.Home.HistoryPanelTitle" parent="Widget.Home.HistoryTabIndicator">
         <item name="android:layout_marginLeft">32dp</item>
--- a/mobile/android/base/resources/values-v11/themes.xml
+++ b/mobile/android/base/resources/values-v11/themes.xml
@@ -42,16 +42,17 @@
         <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
         <item name="menuItemActionViewStyle">@style/Widget.MenuItemActionView</item>
         <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
         <item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
         <item name="menuItemShareActionButtonStyle">@style/Widget.MenuItemSecondaryActionBar</item>
         <item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
         <item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
         <item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
+        <item name="homeGridViewStyle">@style/Widget.HomeGridView</item>
         <item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
         <item name="homeListViewStyle">@style/Widget.HomeListView</item>
         <item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
         <item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
         <item name="android:actionModeStyle">@style/GeckoActionBar</item>
         <item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item>
         <item name="android:actionModeCutDrawable">@drawable/ab_cut</item>
         <item name="android:actionModeCopyDrawable">@drawable/ab_copy</item>
--- a/mobile/android/base/resources/values-xlarge-land-v11/styles.xml
+++ b/mobile/android/base/resources/values-xlarge-land-v11/styles.xml
@@ -1,23 +1,25 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
 
-    <style name="Widget.TopSitesGridView" parent="Widget.GridView">
+    <style name="Widget.HomeGridView" parent="Widget.GridView">
         <item name="android:paddingLeft">55dp</item>
         <item name="android:paddingRight">55dp</item>
         <item name="android:paddingBottom">30dp</item>
         <item name="android:horizontalSpacing">56dp</item>
         <item name="android:verticalSpacing">20dp</item>
     </style>
 
+    <style name="Widget.TopSitesGridView" parent="Widget.HomeGridView" />
+
     <style name="Widget.Home.HistoryListView">
         <item name="android:paddingLeft">50dp</item>
         <item name="android:paddingRight">100dp</item>
         <item name="android:paddingTop">30dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
         <item name="topDivider">true</item>
     </style>
 
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -30,16 +30,19 @@
         <attr name="menuItemShareActionButtonStyle" format="reference"/>
 
         <!-- Default style for the BookmarksListView -->
         <attr name="bookmarksListViewStyle" format="reference" />
 
         <!-- Default style for the TopSitesGridItemView -->
         <attr name="topSitesGridItemViewStyle" format="reference" />
 
+        <!-- Default style for the HomeGridView -->
+        <attr name="homeGridViewStyle" format="reference" />
+
         <!-- Default style for the TopSitesGridView -->
         <attr name="topSitesGridViewStyle" format="reference" />
 
         <!-- Default style for the TopSitesThumbnailView -->
         <attr name="topSitesThumbnailViewStyle" format="reference" />
 
         <!-- Default style for the HomeListView -->
         <attr name="homeListViewStyle" format="reference" />
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -125,22 +125,24 @@
     </style>
 
     <style name="Widget.BookmarkFolderView" parent="Widget.TwoLineRow.PrimaryText">
         <item name="android:paddingLeft">10dip</item>
         <item name="android:drawablePadding">10dip</item>
         <item name="android:drawableLeft">@drawable/bookmark_folder</item>
     </style>
 
-    <style name="Widget.TopSitesGridView" parent="Widget.GridView">
+    <style name="Widget.HomeGridView" parent="Widget.GridView">
         <item name="android:padding">7dp</item>
         <item name="android:horizontalSpacing">0dp</item>
         <item name="android:verticalSpacing">7dp</item>
     </style>
 
+    <style name="Widget.TopSitesGridView" parent="Widget.HomeGridView" />
+
     <style name="Widget.TopSitesGridItemView">
       <item name="android:layout_width">fill_parent</item>
       <item name="android:layout_height">fill_parent</item>
       <item name="android:padding">5dip</item>
       <item name="android:orientation">vertical</item>
     </style>
 
     <style name="Widget.BookmarkItemView" parent="Widget.TwoLineRow"/>
--- a/mobile/android/base/resources/values/themes.xml
+++ b/mobile/android/base/resources/values/themes.xml
@@ -75,16 +75,17 @@
         <item name="android:dropDownItemStyle">@style/Widget.DropDownItem</item>
         <item name="android:editTextStyle">@style/Widget.EditText</item>
         <item name="android:gridViewStyle">@style/Widget.GridView</item>
         <item name="android:textViewStyle">@style/Widget.TextView</item>
         <item name="android:spinnerStyle">@style/Widget.Spinner</item>
         <item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
         <item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
         <item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
+        <item name="homeGridViewStyle">@style/Widget.HomeGridView</item>
         <item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
         <item name="homeListViewStyle">@style/Widget.HomeListView</item>
         <item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
         <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
         <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
         <item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
     </style>
 
--- a/mobile/android/base/tests/StringHelper.java
+++ b/mobile/android/base/tests/StringHelper.java
@@ -106,16 +106,20 @@ public class StringHelper {
     public static final String TABS_LABEL = "Tabs";
 
     // Display
     public static final String TEXT_SIZE_LABEL = "Text size";
     public static final String TITLE_BAR_LABEL = "Title bar";
     public static final String TEXT_REFLOW_LABEL = "Text reflow";
     public static final String CHARACTER_ENCODING_LABEL = "Character encoding";
     public static final String PLUGINS_LABEL = "Plugins";
+ 
+    // Title bar
+    public static final String SHOW_PAGE_TITLE_LABEL = "Show page title";
+    public static final String SHOW_PAGE_ADDRESS_LABEL = "Show page address";
 
     // Privacy
     public static final String TRACKING_LABEL = "Tracking";
     public static final String COOKIES_LABEL = "Cookies";
     public static final String REMEMBER_PASSWORDS_LABEL = "Remember passwords";
     public static final String MASTER_PASWSWORD_LABEL = "Use master password";
     public static final String CLEAR_PRIVATE_DATA_LABEL = "Clear private data";
 
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -68,16 +68,17 @@ skip-if = processor == "x86"
 [testSessionOOMSave]
 [testSessionOOMRestore]
 [testSettingsMenuItems]
 # [testShareLink] # see bug 915897
 [testSystemPages]
 # disabled on x86 only; bug 907383
 skip-if = processor == "x86"
 # [testThumbnails] # see bug 813107
+[testTitleBar]
 # [testVkbOverlap] # see bug 907274
 
 # Using JavascriptTest
 [testBrowserDiscovery]
 [testDeviceSearchEngine]
 [testJNI]
 # [testMozPay] # see bug 945675
 [testOrderedBroadcast]
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testTitleBar.java
@@ -0,0 +1,58 @@
+package org.mozilla.gecko.tests;
+import android.view.View;
+import org.mozilla.gecko.*;
+import java.util.ArrayList;
+
+/**
+ * This patch tests the option that shows the full URL and title in the URL Bar
+ */
+
+public class testTitleBar extends PixelTest {
+
+    @Override
+    protected int getTestType() {
+        return TEST_MOCHITEST;
+    }
+    public void testTitleBar() {
+        blockForGeckoReady();
+        checkOption();
+    }
+
+    public void checkOption() {
+
+        String blank1 = getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+        String title = StringHelper.ROBOCOP_BLANK_PAGE_01_TITLE;
+
+        // Loading a page
+        inputAndLoadUrl(blank1);
+        verifyPageTitle(title);
+
+        // Verifing the full URL is displayed in the URL Bar
+        selectOption(StringHelper.SHOW_PAGE_ADDRESS_LABEL);
+        inputAndLoadUrl(blank1);
+        verifyUrl(blank1);
+
+        // Verifing the title is displayed in the URL Bar
+        selectOption(StringHelper.SHOW_PAGE_TITLE_LABEL);
+        inputAndLoadUrl(blank1);
+        verifyPageTitle(title);
+    }
+
+    // Entering settings, changing the options: show title/page address option and verifing the device type because for phone there is an extra back action to exit the settings menu
+    public void selectOption(String option) {
+        selectSettingsItem(StringHelper.DISPLAY_SECTION_LABEL, StringHelper.TITLE_BAR_LABEL);
+        mAsserter.ok(waitForText(StringHelper.SHOW_PAGE_TITLE_LABEL), "Waiting for the pop-up to open", "Pop up with the options was openend");
+        mSolo.clickOnText(option);
+        mAsserter.ok(waitForText(StringHelper.CHARACTER_ENCODING_LABEL), "Waiting to press the option", "The pop-up is dismissed once clicked");
+        if (mDevice.type.equals("phone")) {
+            mActions.sendSpecialKey(Actions.SpecialKey.BACK);
+            mAsserter.ok(waitForText(StringHelper.CUSTOMIZE_SECTION_LABEL), "Waiting to perform one back", "One back performed");
+            mActions.sendSpecialKey(Actions.SpecialKey.BACK);
+            mAsserter.ok(waitForText(StringHelper.ROBOCOP_BLANK_PAGE_01_URL), "Waiting to exit settings", "Exit settings done");
+        }
+        else {
+            mActions.sendSpecialKey(Actions.SpecialKey.BACK);
+            mAsserter.ok(waitForText(StringHelper.ROBOCOP_BLANK_PAGE_01_URL), "Waiting to exit settings", "Exit settings done");
+        }
+    }
+}
--- a/services/common/tests/unit/test_tokenserverclient.js
+++ b/services/common/tests/unit/test_tokenserverclient.js
@@ -194,24 +194,27 @@ add_test(function test_403_no_urls() {
     do_check_eq(error.cause, "malformed-response");
     do_check_null(result);
 
     server.stop(run_next_test);
 
   });
 });
 
-add_test(function test_send_conditions_accepted() {
+add_test(function test_send_extra_headers() {
   _("Ensures that the condition acceptance header is sent when asked.");
 
   let duration = 300;
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
-      do_check_true(request.hasHeader("x-conditions-accepted"));
-      do_check_eq(request.getHeader("x-conditions-accepted"), "1");
+      do_check_true(request.hasHeader("x-foo"));
+      do_check_eq(request.getHeader("x-foo"), "42");
+
+      do_check_true(request.hasHeader("x-bar"));
+      do_check_eq(request.getHeader("x-bar"), "17");
 
       response.setStatusLine(request.httpVersion, 200, "OK");
       response.setHeader("Content-Type", "application/json");
 
       let body = JSON.stringify({
         id:           "id",
         key:          "key",
         api_endpoint: "http://example.com/",
@@ -228,17 +231,21 @@ add_test(function test_send_conditions_a
   function onResponse(error, token) {
     do_check_null(error);
 
     // Other tests validate other things.
 
     server.stop(run_next_test);
   }
 
-  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse, true);
+  let extra = {
+    "X-Foo": 42,
+    "X-Bar": 17
+  };
+  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse, extra);
 });
 
 add_test(function test_error_404_empty() {
   _("Ensure that 404 responses without proper response are handled properly.");
 
   let server = httpd_setup();
 
   let client = new TokenServerClient();
@@ -330,36 +337,36 @@ add_test(function test_400_response() {
     do_check_eq("TokenServerClientServerError", error.name);
     do_check_neq(null, error.response);
     do_check_eq(error.cause, "malformed-request");
 
     server.stop(run_next_test);
   });
 });
 
-add_test(function test_401_response() {
-  _("Ensure HTTP 401 is converted to invalid-credentials.");
+add_test(function test_401_with_error_cause() {
+  _("Ensure 401 cause is specified in body.status");
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       response.setStatusLine(request.httpVersion, 401, "Unauthorized");
       response.setHeader("Content-Type", "application/json; charset=utf-8");
 
-      let body = "{}"; // Actual content may not be used.
+      let body = JSON.stringify({status: "no-soup-for-you"});
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/1.0/foo/1.0";
   client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
     do_check_neq(null, error);
     do_check_eq("TokenServerClientServerError", error.name);
     do_check_neq(null, error.response);
-    do_check_eq(error.cause, "invalid-credentials");
+    do_check_eq(error.cause, "no-soup-for-you");
 
     server.stop(run_next_test);
   });
 });
 
 add_test(function test_unhandled_media_type() {
   _("Ensure that unhandled media types throw an error.");
 
--- a/services/common/tokenserverclient.js
+++ b/services/common/tokenserverclient.js
@@ -201,18 +201,17 @@ TokenServerClient.prototype = {
    * @param  assertion
    *         (string) BrowserID assertion to exchange token for.
    * @param  cb
    *         (function) Callback to be invoked with result of operation.
    * @param  conditionsAccepted
    *         (bool) Whether to send acceptance to service conditions.
    */
   getTokenFromBrowserIDAssertion:
-    function getTokenFromBrowserIDAssertion(url, assertion, cb,
-                                            conditionsAccepted=false) {
+    function getTokenFromBrowserIDAssertion(url, assertion, cb, addHeaders={}) {
     if (!url) {
       throw new TokenServerClientError("url argument is not valid.");
     }
 
     if (!assertion) {
       throw new TokenServerClientError("assertion argument is not valid.");
     }
 
@@ -221,19 +220,18 @@ TokenServerClient.prototype = {
     }
 
     this._log.debug("Beginning BID assertion exchange: " + url);
 
     let req = new RESTRequest(url);
     req.setHeader("Accept", "application/json");
     req.setHeader("Authorization", "BrowserID " + assertion);
 
-    if (conditionsAccepted) {
-      // Value is irrelevant.
-      req.setHeader("X-Conditions-Accepted", "1");
+    for (let header in addHeaders) {
+      req.setHeader(header, addHeaders[header]);
     }
 
     let client = this;
     req.get(function onResponse(error) {
       if (error) {
         cb(new TokenServerClientNetworkError(error), null);
         return;
       }
@@ -321,18 +319,20 @@ TokenServerClient.prototype = {
 
       let error = new TokenServerClientServerError();
       error.response = response;
 
       if (response.status == 400) {
         error.message = "Malformed request.";
         error.cause = "malformed-request";
       } else if (response.status == 401) {
+        // Cause can be invalid-credentials, invalid-timestamp, or
+        // invalid-generation.
         error.message = "Authentication failed.";
-        error.cause = "invalid-credentials";
+        error.cause = result.status;
       }
 
       // 403 should represent a "condition acceptance needed" response.
       //
       // The extra validation of "urls" is important. We don't want to signal
       // conditions required unless we are absolutely sure that is what the
       // server is asking for.
       else if (response.status == 403) {
--- a/services/common/utils.js
+++ b/services/common/utils.js
@@ -68,17 +68,17 @@ this.CommonUtils = {
    * @param pad
    *        (bool) Whether to include padding characters (=). Defaults
    *        to true for historical reasons.
    */
   encodeBase64URL: function encodeBase64URL(bytes, pad=true) {
     let s = btoa(bytes).replace("+", "-", "g").replace("/", "_", "g");
 
     if (!pad) {
-      s = s.replace("=", "");
+      s = s.replace("=", "", "g");
     }
 
     return s;
   },
 
   /**
    * Create a nsIURI instance from a string.
    */
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -5,16 +5,17 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["BrowserIDManager"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-common/tokenserverclient.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-common/tokenserverclient.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://gre/modules/Promise.jsm");
@@ -37,17 +38,16 @@ function deriveKeyBundle(kB) {
   let out = CryptoUtils.hkdf(kB, undefined,
                              "identity.mozilla.com/picl/v1/oldsync", 2*32);
   let bundle = new BulkKeyBundle();
   // [encryptionKey, hmacKey]
   bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)];
   return bundle;
 }
 
-
 this.BrowserIDManager = function BrowserIDManager() {
   this._fxaService = fxAccounts;
   this._tokenServerClient = new TokenServerClient();
   // will be a promise that resolves when we are ready to authenticate
   this.whenReadyToAuthenticate = null;
   this._log = Log.repository.getLogger("Sync.BrowserIDManager");
   this._log.addAppender(new Log.DumpAppender());
   this._log.Level = Log.Level[Svc.Prefs.get("log.logger.identity")] || Log.Level.Error;
@@ -158,16 +158,35 @@ this.BrowserIDManager.prototype = {
       // and resets _shouldHaveSyncKeyBundle.
       this.username = "";
       this._account = null;
       Weave.Service.logout();
       break;
     }
   },
 
+   /**
+   * Compute the sha256 of the message bytes.  Return bytes.
+   */
+  _sha256: function(message) {
+    let hasher = Cc["@mozilla.org/security/hash;1"]
+                    .createInstance(Ci.nsICryptoHash);
+    hasher.init(hasher.SHA256);
+    return CryptoUtils.digestBytes(message, hasher);
+  },
+
+  /**
+   * Compute the X-Client-State header given the byte string kB.
+   *
+   * Return string: hex(first16Bytes(sha256(kBbytes)))
+   */
+  _computeXClientState: function(kBbytes) {
+    return CommonUtils.bytesAsHex(this._sha256(kBbytes).slice(0, 16), false);
+  },
+
   /**
    * Provide override point for testing token expiration.
    */
   _now: function() {
     return Date.now();
   },
 
   get account() {
@@ -383,29 +402,34 @@ this.BrowserIDManager.prototype = {
     }
   },
 
   // This is a helper to fetch a sync token for the given user data.
   _fetchTokenForUser: function(userData) {
     let tokenServerURI = Svc.Prefs.get("tokenServerURI");
     let log = this._log;
     let client = this._tokenServerClient;
+
+    // Both Jelly and FxAccounts give us kB as hex
+    let kBbytes = CommonUtils.hexToBytes(userData.kB);
+    let headers = {"X-Client-State": this._computeXClientState(kBbytes)};
     log.info("Fetching Sync token from: " + tokenServerURI);
 
     function getToken(tokenServerURI, assertion) {
       let deferred = Promise.defer();
       let cb = function (err, token) {
         if (err) {
           log.info("TokenServerClient.getTokenFromBrowserIDAssertion() failed with: " + err.message);
           return deferred.reject(err);
         } else {
           return deferred.resolve(token);
         }
       };
-      client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb);
+
+      client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
       return deferred.promise;
     }
 
     let audience = Services.io.newURI(tokenServerURI, null, null).prePath;
     // wait until the account email is verified and we know that
     // getAssertion() will return a real assertion (not null).
     return this._fxaService.whenVerified(userData)
       .then(() => this._fxaService.getAssertion(audience))
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -1,15 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://gre/modules/FxAccounts.jsm");
 Cu.import("resource://services-sync/browserid_identity.js");
 Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
 
 let identityConfig = makeIdentityConfig();
 let browseridManager = new BrowserIDManager();
 configureFxAccountIdentity(browseridManager, identityConfig);
 
 function run_test() {
   initTestLogging("Trace");
@@ -97,8 +99,47 @@ add_test(function test_userChangeAndLogO
     do_check_true(!!output);
     do_check_eq(bidUser.account, identityConfig.fxaccount.user.email);
     do_check_true(bidUser.hasValidToken());
     identityConfig.fxaccount.user.email = "something@new";
     do_check_false(bidUser.hasValidToken());
     run_next_test();
   }
 );
+
+add_test(function test_sha256() {
+  // Test vectors from http://www.bichlmeier.info/sha256test.html
+  let vectors = [
+    ["",
+     "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
+    ["abc",
+     "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"],
+    ["message digest",
+     "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"],
+    ["secure hash algorithm",
+     "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"],
+    ["SHA256 is considered to be safe",
+     "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"],
+    ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+     "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"],
+    ["For this sample, this 63-byte string will be used as input data",
+     "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"],
+    ["This is exactly 64 bytes long, not counting the terminating byte",
+     "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"]
+  ];
+  let bidUser = new BrowserIDManager();
+  for (let [input,output] of vectors) {
+    do_check_eq(CommonUtils.bytesAsHex(bidUser._sha256(input)), output);
+  }
+  run_next_test();
+});
+
+add_test(function test_computeXClientStateHeader() {
+  let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
+  let kB = CommonUtils.hexToBytes(kBhex);
+
+  let bidUser = new BrowserIDManager();
+  let header = bidUser._computeXClientState(kB);
+
+  do_check_eq(header, "6ae94683571c7a7c54dab4700aa3995f");
+  run_next_test();
+});
+
--- a/toolkit/components/telemetry/TelemetryStartup.js
+++ b/toolkit/components/telemetry/TelemetryStartup.js
@@ -2,18 +2,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cu = Components.utils;
 
-Cu.import("resource://gre/modules/TelemetryPing.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm")
+Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this)
 
 /**
  * TelemetryStartup is needed to forward the "profile-after-change" notification
  * to TelemetryPing.jsm.
  */
 function TelemetryStartup() {
 }
 
--- a/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm
+++ b/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm
@@ -3,18 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 let Cr = Components.results;
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
 
 this.EXPORTED_SYMBOLS = ["ThirdPartyCookieProbe"];
 
 const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
 
 /**
  * A probe implementing the measurements detailed at
  * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry
--- a/toolkit/components/telemetry/UITelemetry.jsm
+++ b/toolkit/components/telemetry/UITelemetry.jsm
@@ -12,17 +12,17 @@ const PREF_ENABLED = PREF_BRANCH + "enab
 #else
 const PREF_ENABLED = PREF_BRANCH + "enabled";
 #endif
 
 this.EXPORTED_SYMBOLS = [
   "UITelemetry",
 ];
 
-Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
 
 /**
  * UITelemetry is a helper JSM used to record UI specific telemetry events.
  *
  * It implements nsIUITelemetryObserver, defined in nsIAndroidBridge.idl.
  */
 this.UITelemetry = {
   _enabled: undefined,
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js
@@ -3,17 +3,17 @@
 */
 /* A testcase to make sure reading late writes stacks works.  */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
-Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
 
 const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
 
 // Constants from prio.h for nsIFileOutputStream.init
 const PR_WRONLY = 0x2;
 const PR_CREATE_FILE = 0x8;
 const PR_TRUNCATE = 0x20;
 const RW_OWNER = 0600;
@@ -126,9 +126,9 @@ function actual_test() {
     return function(obj, idx, array) {
       return unevalCanonicalStack == obj;
     }
   }
   do_check_eq(uneval_STACKS.filter(stackChecker(first_stack)).length, 1);
   do_check_eq(uneval_STACKS.filter(stackChecker(second_stack)).length, 1);
 
   do_test_finished();
-}
\ No newline at end of file
+}
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js
@@ -3,17 +3,17 @@
 */
 /* A testcase to make sure reading the failed profile lock count works.  */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
-Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
 
 const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
 
 const LOCK_FILE_NAME = "Telemetry.FailedProfileLocks.txt";
 const N_FAILED_LOCKS = 10;
 
 // Constants from prio.h for nsIFileOutputStream.init
 const PR_WRONLY = 0x2;
@@ -52,9 +52,9 @@ function run_test() {
   do_test_pending();
   Telemetry.asyncFetchTelemetryData(actual_test);
 }
 
 function actual_test() {
   do_check_eq(Telemetry.failedProfileLockCount, N_FAILED_LOCKS);
   do_check_false(construct_file().exists());
   do_test_finished();
-}
\ No newline at end of file
+}
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryPing_idle.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryPing_idle.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Check that TelemetryPing notifies correctly on idle-daily.
 
 const Cu = Components.utils;
 
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/TelemetryPing.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
 
 function run_test() {
   do_test_pending();
 
   Services.obs.addObserver(function observeTelemetry() {
     Services.obs.removeObserver(observeTelemetry, "gather-telemetry");
     do_test_finished();
   }, "gather-telemetry", false);
--- a/toolkit/components/telemetry/tests/unit/test_ThirdPartyCookieProbe.js
+++ b/toolkit/components/telemetry/tests/unit/test_ThirdPartyCookieProbe.js
@@ -1,20 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let Cu = Components.utils;
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm");
-Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
-Cu.import("resource://gre/modules/TelemetryPing.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm", this);
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", this);
+Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
 
 let TOPIC_ACCEPTED = "third-party-cookie-accepted";
 let TOPIC_REJECTED = "third-party-cookie-rejected";
 
 let FLUSH_MILLISECONDS = 1000 * 60 * 60 * 24 / 2; /*Half a day, for testing purposes*/
 
 const NUMBER_OF_REJECTS = 30;
 const NUMBER_OF_ACCEPTS = 17;
--- a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
+++ b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
@@ -2,17 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const INT_MAX = 0x7FFFFFFF;
 
 const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
-Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
 
 function test_expired_histogram() {
   var histogram_id = "FOOBAR";
   var test_expired_id = "TELEMETRY_TEST_EXPIRED";
   var clone_id = "ExpiredClone";
   var dummy = Telemetry.newHistogram(histogram_id, "28.0a1", 1, 2, 3, Telemetry.HISTOGRAM_EXPONENTIAL);
   var dummy_clone = Telemetry.histogramFrom(clone_id, test_expired_id);
   var rh = Telemetry.registeredHistograms([]);
--- a/toolkit/content/tests/chrome/chrome.ini
+++ b/toolkit/content/tests/chrome/chrome.ini
@@ -122,16 +122,18 @@ skip-if = os == "win" # Intermittent fai
 [test_popup_scaled.xul]
 [test_popup_tree.xul]
 [test_popuphidden.xul]
 [test_popupincontent.xul]
 [test_popupremoving.xul]
 [test_popupremoving_frame.xul]
 [test_position.xul]
 [test_preferences.xul]
+[test_preferences_beforeaccept.xul]
+support-files = window_preferences_beforeaccept.xul
 [test_progressmeter.xul]
 [test_props.xul]
 [test_radio.xul]
 [test_richlist_direction.xul]
 [test_righttoleft.xul]
 [test_scale.xul]
 [test_scaledrag.xul]
 [test_screenPersistence.xul]
--- a/toolkit/content/tests/chrome/test_arrowpanel.xul
+++ b/toolkit/content/tests/chrome/test_arrowpanel.xul
@@ -226,50 +226,55 @@ function checkPanelPosition(panel)
     expectedAnchorEdge = expectedAnchorEdge.substring(7);
     iscentered = true;
   }
 
   switch (expectedAnchorEdge) {
     case "top":
       adj = vwinpos + parseInt(getComputedStyle(panel, "").marginTop);
       if (iscentered)
-        adj += anchorRect.height / 2;
-      is(Math.round(panelRect.top), Math.round(anchorRect.top * zoomFactor +  + adj), "anchored on top");
+        adj += Math.round(anchorRect.height) / 2;
+      isWithinHalfPixel(panelRect.top, anchorRect.top * zoomFactor + adj, "anchored on top");
       break;
     case "bottom":
       adj = vwinpos + parseInt(getComputedStyle(panel, "").marginBottom);
       if (iscentered)
-        adj += anchorRect.height / 2;
-      is(Math.round(panelRect.bottom), Math.round(anchorRect.bottom * zoomFactor +  - adj), "anchored on bottom");
+        adj += Math.round(anchorRect.height) / 2;
+      isWithinHalfPixel(panelRect.bottom, anchorRect.bottom * zoomFactor - adj, "anchored on bottom");
       break;
     case "left":
       adj = hwinpos + parseInt(getComputedStyle(panel, "").marginLeft);
       if (iscentered)
-        adj += anchorRect.width / 2;
-      is(Math.round(panelRect.left), Math.round((anchorRect.left * zoomFactor + adj)), "anchored on left ");
+        adj += Math.round(anchorRect.width) / 2;
+      isWithinHalfPixel(panelRect.left, anchorRect.left * zoomFactor + adj, "anchored on left ");
       break;
     case "right":
       adj = hwinpos + parseInt(getComputedStyle(panel, "").marginRight);
       if (iscentered)
-        adj += anchorRect.width / 2;
-      is(Math.round(panelRect.right), Math.round(anchorRect.right * zoomFactor +  - adj), "anchored on right");
+        adj += Math.round(anchorRect.width) / 2;
+      isWithinHalfPixel(panelRect.right, anchorRect.right * zoomFactor - adj, "anchored on right");
       break;
   }
 
   is(anchor, expectedAnchor, "anchor");
 
   var arrow = document.getAnonymousElementByAttribute(panel, "anonid", "arrow");
   is(arrow.getAttribute("side"), expectedSide, "panel arrow side");
   is(arrow.hidden, false, "panel hidden");
   is(arrow.parentNode.pack, expectedPack, "panel arrow pack");
   is(panel.alignmentPosition, expectedAlignment, "panel alignmentPosition");
 
   panel.hidePopup();
 }
 
+function isWithinHalfPixel(a, b)
+{
+  return Math.abs(a - b) <= 0.5;
+}
+
 function checkBigPanel(panel)
 {
   ok(panel.firstChild.getBoundingClientRect().height < 2800, "big panel height");
   panel.hidePopup();
 }
 
 SimpleTest.waitForFocus(startTest);
 
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/test_preferences_beforeaccept.xul
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Preferences Window beforeaccept Tests"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/MochiKit/packed.js"/>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <script type="application/javascript">
+  <![CDATA[
+    SimpleTest.waitForExplicitFinish();
+
+    // No instant-apply for this test
+    SpecialPowers.setBoolPref("browser.preferences.instantApply", false);
+
+    var prefWindow = openDialog("window_preferences_beforeaccept.xul", "", "", windowOnload);
+
+    SimpleTest.registerCleanupFunction(() => {
+      SpecialPowers.clearUserPref("browser.preferences.instantApply");
+      SpecialPowers.clearUserPref("tests.beforeaccept.dialogShown");
+      SpecialPowers.clearUserPref("tests.beforeaccept.called");
+    });
+
+    function windowOnload() {
+      var dialogShown = prefWindow.document.getElementById("tests.beforeaccept.dialogShown");
+      var called = prefWindow.document.getElementById("tests.beforeaccept.called");
+      is(dialogShown.value, true, "dialog opened, shown pref set");
+      is(dialogShown.valueFromPreferences, null, "shown pref not committed");
+      is(called.value, null, "beforeaccept not yet called");
+      is(called.valueFromPreferences, null, "beforeaccept not yet called, pref not committed");
+
+      // try to accept the dialog, should fail the first time
+      prefWindow.document.documentElement.acceptDialog();
+      is(prefWindow.closed, false, "window not closed");
+      is(dialogShown.value, true, "shown pref still set");
+      is(dialogShown.valueFromPreferences, null, "shown pref still not committed");
+      is(called.value, true, "beforeaccept called");
+      is(called.valueFromPreferences, null, "called pref not committed");
+
+      // try again, this one should succeed
+      prefWindow.document.documentElement.acceptDialog();
+      is(prefWindow.closed, true, "window now closed");
+      is(dialogShown.valueFromPreferences, true, "shown pref committed");
+      is(called.valueFromPreferences, true, "called pref committed");
+
+      SimpleTest.finish();
+    }
+  ]]>
+  </script>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+    <p id="display"></p>
+    <div id="content" style="display: none"></div>
+    <pre id="test"></pre>
+  </body>
+
+</window>
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/window_preferences_beforeaccept.xul
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<!--
+  XUL Widget Test for preferences window with beforeaccept
+-->
+<prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+            title="preferences window"
+            width="300" height="300"
+            windowtype="test:preferences"
+            buttons="accept,cancel"
+            onbeforeaccept="return beforeAccept();"
+            onload="onDialogLoad();"
+>
+  <script type="application/javascript">
+  <![CDATA[
+    function onDialogLoad() {
+      var pref = document.getElementById("tests.beforeaccept.dialogShown");
+      pref.value = true;
+
+      // call the onload handler we were passed
+      window.arguments[0]();
+    }
+
+    function beforeAccept() {
+      var beforeAcceptPref = document.getElementById("tests.beforeaccept.called");
+      var oldValue = beforeAcceptPref.value;
+      beforeAcceptPref.value = true;
+
+      return !!oldValue;
+    }
+  ]]>
+  </script>
+
+  <prefpane id="sample_pane" label="Sample Prefpane">
+    <preferences id="sample_preferences">
+      <preference id="tests.beforeaccept.called"
+                  name="tests.beforeaccept.called"
+                  type="bool"/>
+      <preference id="tests.beforeaccept.dialogShown"
+                  name="tests.beforeaccept.dialogShown"
+                  type="bool"/>
+    </preferences>
+  </prefpane>
+  <label>Test Prefpane</label>
+</prefwindow>
--- a/toolkit/content/widgets/preferences.xml
+++ b/toolkit/content/widgets/preferences.xml
@@ -1026,19 +1026,20 @@
           return win;
         ]]>
         </body>
       </method>
     </implementation>
     <handlers>
       <handler event="dialogaccept">
       <![CDATA[
-        if (!this._fireEvent("beforeaccept", this)) 
-          return;
-        
+        if (!this._fireEvent("beforeaccept", this)){
+          return false;
+        }
+
         if (this.type == "child" && window.opener) {
           var psvc = Components.classes["@mozilla.org/preferences-service;1"]
                                .getService(Components.interfaces.nsIPrefBranch);
           var instantApply = psvc.getBoolPref("browser.preferences.instantApply");
           if (instantApply) {
             var panes = this.preferencePanes;
             for (var i = 0; i < panes.length; ++i)
               panes[i].writePreferences(true);
@@ -1091,16 +1092,18 @@
           var panes = this.preferencePanes;
           for (var i = 0; i < panes.length; ++i)
             panes[i].writePreferences(false);
 
           var psvc = Components.classes["@mozilla.org/preferences-service;1"]
                                .getService(Components.interfaces.nsIPrefService);
           psvc.savePrefFile(null);
         }
+
+        return true;
       ]]>
       </handler>
       <handler event="command">
         if (event.originalTarget.hasAttribute("pane")) {
           var pane = document.getElementById(event.originalTarget.getAttribute("pane"));
           this.showPane(pane);
         }
       </handler>
--- a/toolkit/devtools/output-parser.js
+++ b/toolkit/devtools/output-parser.js
@@ -6,17 +6,17 @@
 
 const {Cc, Ci, Cu} = require("chrome");
 const {colorUtils} = require("devtools/css-color");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const MAX_ITERATIONS = 100;
-const REGEX_QUOTES = /^".*?"|^".*/;
+const REGEX_QUOTES = /^".*?"|^".*|^'.*?'|^'.*/;
 const REGEX_URL = /^url\(["']?(.+?)(?::(\d+))?["']?\)/;
 const REGEX_WHITESPACE = /^\s+/;
 const REGEX_FIRST_WORD_OR_CHAR = /^\w+|^./;
 const REGEX_CSS_PROPERTY_VALUE = /(^[^;]+)/;
 
 /**
  * This regex matches:
  *  - #F00
@@ -268,38 +268,50 @@ OutputParser.prototype = {
   _cssPropertySupportsValue: function(name, value) {
     let win = Services.appShell.hiddenDOMWindow;
     let doc = win.document;
 
     name = name.replace(/-\w{1}/g, function(match) {
       return match.charAt(1).toUpperCase();
     });
 
+    value = value.replace("!important", "");
+
     let div = doc.createElement("div");
     div.style[name] = value;
 
     return !!div.style[name];
   },
 
   /**
+   * Tests if a given colorObject output by CssColor is valid for parsing.
+   * Valid means it's really a color, not any of the CssColor SPECIAL_VALUES
+   * except transparent
+   */
+  _isValidColor: function(colorObj) {
+    return colorObj.valid &&
+      (!colorObj.specialValue || colorObj.specialValue === "transparent");
+  },
+
+  /**
    * Append a color to the output.
    *
    * @param  {String} color
    *         Color to append
    * @param  {Object} [options]
    *         Options object. For valid options and default values see
    *         _mergeOptions().
    * @returns {Boolean}
    *          true if the color passed in was valid, false otherwise. Special
    *          values such as transparent also return false.
    */
   _appendColor: function(color, options={}) {
     let colorObj = new colorUtils.CssColor(color);
 
-    if (colorObj.valid && !colorObj.specialValue) {
+    if (this._isValidColor(colorObj)) {
       if (options.colorSwatchClass) {
         this._appendNode("span", {
           class: options.colorSwatchClass,
           style: "background-color:" + color
         });
       }
       if (options.defaultColorType) {
         color = colorObj.toString();
@@ -454,10 +466,10 @@ OutputParser.prototype = {
     if (typeof overrides.baseURI === "string") {
       overrides.baseURI = Services.io.newURI(overrides.baseURI, null, null);
     }
 
     for (let item in overrides) {
       defaults[item] = overrides[item];
     }
     return defaults;
-  },
+  }
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/FirefoxAccounts.jsm
@@ -0,0 +1,229 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["FirefoxAccounts"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
+                                  "resource://gre/modules/identity/IdentityUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject",
+                                  "resource://gre/modules/identity/IdentityUtils.jsm");
+
+// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
+// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
+// default.
+const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
+try {
+  this.LOG_LEVEL =
+    Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
+    && Services.prefs.getCharPref(PREF_LOG_LEVEL);
+} catch (e) {
+  this.LOG_LEVEL = Log.Level.Error;
+}
+
+let log = Log.repository.getLogger("Identity.FxAccounts");
+log.level = LOG_LEVEL;
+log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
+
+#ifdef MOZ_B2G
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsManager",
+                                  "resource://gre/modules/FxAccountsManager.jsm",
+                                  "FxAccountsManager");
+#else
+log.warn("The FxAccountsManager is only functional in B2G at this time.");
+var FxAccountsManager = null;
+#endif
+
+function FxAccountsService() {
+  Services.obs.addObserver(this, "quit-application-granted", false);
+
+  // Maintain interface parity with Identity.jsm and MinimalIdentity.jsm
+  this.RP = this;
+
+  this._rpFlows = new Map();
+
+  // Enable us to mock FxAccountsManager service in testing
+  this.fxAccountsManager = FxAccountsManager;
+}
+
+FxAccountsService.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+  observe: function observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "quit-application-granted":
+        Services.obs.removeObserver(this, "quit-application-granted");
+        break;
+    }
+  },
+
+  /**
+   * Register a listener for a given windowID as a result of a call to
+   * navigator.id.watch().
+   *
+   * @param aCaller
+   *        (Object)  an object that represents the caller document, and
+   *                  is expected to have properties:
+   *                  - id (unique, e.g. uuid)
+   *                  - origin (string)
+   *
+   *                  and a bunch of callbacks
+   *                  - doReady()
+   *                  - doLogin()
+   *                  - doLogout()
+   *                  - doError()
+   *                  - doCancel()
+   *
+   */
+  watch: function watch(aRpCaller) {
+    this._rpFlows.set(aRpCaller.id, aRpCaller);
+    log.debug("Current rp flows: " + this._rpFlows.size);
+
+    // Nothing to do but call ready()
+    let runnable = {
+      run: () => {
+        this.doReady(aRpCaller.id);
+      }
+    };
+    Services.tm.currentThread.dispatch(runnable,
+                                       Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
+  unwatch: function(aRpCaller, aTargetMM) {
+    // nothing to do
+  },
+
+  /**
+   * Initiate a login with user interaction as a result of a call to
+   * navigator.id.request().
+   *
+   * @param aRPId
+   *        (integer)  the id of the doc object obtained in .watch()
+   *
+   * @param aOptions
+   *        (Object)  options including privacyPolicy, termsOfService
+   */
+  request: function request(aRPId, aOptions) {
+    aOptions = aOptions || {};
+    let rp = this._rpFlows.get(aRPId);
+    if (!rp) {
+      log.error("request() called before watch()");
+      return;
+    }
+
+    let options = makeMessageObject(rp);
+    objectCopy(aOptions, options);
+
+    log.debug("get assertion for " + rp.audience);
+
+    this.fxAccountsManager.getAssertion(rp.audience).then(
+      data => {
+        log.debug("got assertion: " + JSON.stringify(data));
+        this.doLogin(aRPId, data);
+      },
+      error => {
+        log.error("get assertion failed: " + JSON.stringify(error));
+      }
+    );
+  },
+
+  /**
+   * Invoked when a user wishes to logout of a site (for instance, when clicking
+   * on an in-content logout button).
+   *
+   * @param aRpCallerId
+   *        (integer)  the id of the doc object obtained in .watch()
+   *
+   */
+  logout: function logout(aRpCallerId) {
+    // XXX Bug 945363 - Resolve the SSO story for FXA and implement
+    // logout accordingly.
+    //
+    // For now, it makes no sense to logout from a specific RP in
+    // Firefox Accounts, so just directly call the logout callback.
+    if (!this._rpFlows.has(aRpCallerId)) {
+      log.error("logout() called before watch()");
+      return;
+    }
+
+    // Call logout() on the next tick
+    let runnable = {
+      run: () => {
+        this.doLogout(aRpCallerId);
+      }
+    };
+    Services.tm.currentThread.dispatch(runnable,
+                                       Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
+  childProcessShutdown: function childProcessShutdown(messageManager) {
+    for (let [key,] of this._rpFlows) {
+      if (this._rpFlows.get(key)._mm === messageManager) {
+        this._rpFlows.delete(key);
+      }
+    }
+  },
+
+  doLogin: function doLogin(aRpCallerId, aAssertion) {
+    let rp = this._rpFlows.get(aRpCallerId);
+    if (!rp) {
+      log.warn("doLogin found no rp to go with callerId " + aRpCallerId + "\n");
+      return;
+    }
+
+    rp.doLogin(aAssertion);
+  },
+
+  doLogout: function doLogout(aRpCallerId) {
+    let rp = this._rpFlows.get(aRpCallerId);
+    if (!rp) {
+      log.warn("doLogout found no rp to go with callerId " + aRpCallerId + "\n");
+      return;
+    }
+
+    rp.doLogout();
+  },
+
+  doReady: function doReady(aRpCallerId) {
+    let rp = this._rpFlows.get(aRpCallerId);
+    if (!rp) {
+      log.warn("doReady found no rp to go with callerId " + aRpCallerId + "\n");
+      return;
+    }
+
+    rp.doReady();
+  },
+
+  doCancel: function doCancel(aRpCallerId) {
+    let rp = this._rpFlows.get(aRpCallerId);
+    if (!rp) {
+      log.warn("doCancel found no rp to go with callerId " + aRpCallerId + "\n");
+      return;
+    }
+
+    rp.doCancel();
+  },
+
+  doError: function doError(aRpCallerId, aError) {
+    let rp = this._rpFlows.get(aRpCallerId);
+    if (!rp) {
+      log.warn("doCancel found no rp to go with callerId " + aRpCallerId + "\n");
+      return;
+    }
+
+    rp.doError(aError);
+  }
+};
+
+this.FirefoxAccounts = new FxAccountsService();
+
--- a/toolkit/identity/IdentityUtils.jsm
+++ b/toolkit/identity/IdentityUtils.jsm
@@ -7,17 +7,18 @@
 // functions common to Identity.jsm and MinimalIdentity.jsm
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "checkDeprecated",
   "checkRenamed",
   "getRandomId",
-  "objectCopy"
+  "objectCopy",
+  "makeMessageObject",
 ];
 
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
@@ -68,8 +69,43 @@ this.objectCopy = function objectCopy(so
   let desc;
   Object.getOwnPropertyNames(source).forEach(function(name) {
     if (name[0] !== '_') {
       desc = Object.getOwnPropertyDescriptor(source, name);
       Object.defineProperty(target, name, desc);
     }
   });
 };
+
+this.makeMessageObject = function makeMessageObject(aRpCaller) {
+  let options = {};
+
+  options.id = aRpCaller.id;
+  options.origin = aRpCaller.origin;
+
+  // Backwards compatibility with Persona beta:
+  // loggedInUser can be undefined, null, or a string
+  options.loggedInUser = aRpCaller.loggedInUser;
+
+  // Special flag for internal calls for Persona in b2g
+  options._internal = aRpCaller._internal;
+
+  Object.keys(aRpCaller).forEach(function(option) {
+    // Duplicate the callerobject, scrubbing out functions and other
+    // internal variables (like _mm, the message manager object)
+    if (!Object.hasOwnProperty(this, option)
+        && option[0] !== '_'
+        && typeof aRpCaller[option] !== 'function') {
+      options[option] = aRpCaller[option];
+    }
+  });
+
+  // check validity of message structure
+  if ((typeof options.id === 'undefined') ||
+      (typeof options.origin === 'undefined')) {
+    let err = "id and origin required in relying-party message: " + JSON.stringify(options);
+    reportError(err);
+    throw new Error(err);
+  }
+
+  return options;
+}
+
--- a/toolkit/identity/MinimalIdentity.jsm
+++ b/toolkit/identity/MinimalIdentity.jsm
@@ -24,63 +24,28 @@ const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/identity/LogUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
                                   "resource://gre/modules/identity/IdentityUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this,
-                                  "jwcrypto",
-                                  "resource://gre/modules/identity/jwcrypto.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject",
+                                  "resource://gre/modules/identity/IdentityUtils.jsm");
 
 function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["minimal core"].concat(aMessageArgs));
 }
 function reportError(...aMessageArgs) {
   Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs));
 }
 
-function makeMessageObject(aRpCaller) {
-  let options = {};
-
-  options.id = aRpCaller.id;
-  options.origin = aRpCaller.origin;
-
-  // loggedInUser can be undefined, null, or a string
-  options.loggedInUser = aRpCaller.loggedInUser;
-
-  // Special flag for internal calls
-  options._internal = aRpCaller._internal;
-
-  Object.keys(aRpCaller).forEach(function(option) {
-    // Duplicate the callerobject, scrubbing out functions and other
-    // internal variables (like _mm, the message manager object)
-    if (!Object.hasOwnProperty(this, option)
-        && option[0] !== '_'
-        && typeof aRpCaller[option] !== 'function') {
-      options[option] = aRpCaller[option];
-    }
-  });
-
-  // check validity of message structure
-  if ((typeof options.id === 'undefined') ||
-      (typeof options.origin === 'undefined')) {
-    let err = "id and origin required in relying-party message: " + JSON.stringify(options);
-    reportError(err);
-    throw new Error(err);
-  }
-
-  return options;
-}
-
 function IDService() {
   Services.obs.addObserver(this, "quit-application-granted", false);
-  // Services.obs.addObserver(this, "identity-auth-complete", false);
 
   // simplify, it's one object
   this.RP = this;
   this.IDP = this;
 
   // keep track of flows
   this._rpFlows = {};
   this._authFlows = {};
@@ -207,18 +172,16 @@ IDService.prototype = {
       return;
     }
 
     let options = makeMessageObject(rp);
     Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-logout", null);
   },
 
   childProcessShutdown: function childProcessShutdown(messageManager) {
-    let options = makeMessageObject({messageManager: messageManager, id: null, origin: null});
-    Services.obs.notifyObservers({wrappedJSObject: options}, "identity-child-process-shutdown", null);
     Object.keys(this._rpFlows).forEach(function(key) {
       if (this._rpFlows[key]._mm === messageManager) {
         log("child process shutdown for rp", key, "- deleting flow");
         delete this._rpFlows[key];
       }
     }, this);
   },
 
@@ -268,207 +231,12 @@ IDService.prototype = {
   doCancel: function doCancel(aRpCallerId) {
     let rp = this._rpFlows[aRpCallerId];
     if (!rp) {
       log("WARNING: doCancel found no rp to go with callerId " + aRpCallerId);
       return;
     }
 
     rp.doCancel();
-  },
-
-
-  /*
-   * XXX Bug 804229: Implement Identity Provider Functions
-   *
-   * Stubs for Identity Provider functions follow
-   */
-
-  /**
-   * the provisioning iframe sandbox has called navigator.id.beginProvisioning()
-   *
-   * @param aCaller
-   *        (object)  the iframe sandbox caller with all callbacks and
-   *                  other information.  Callbacks include:
-   *                  - doBeginProvisioningCallback(id, duration_s)
-   *                  - doGenKeyPairCallback(pk)
-   */
-  beginProvisioning: function beginProvisioning(aCaller) {
-  },
-
-  /**
-   * the provisioning iframe sandbox has called
-   * navigator.id.raiseProvisioningFailure()
-   *
-   * @param aProvId
-   *        (int)  the identifier of the provisioning flow tied to that sandbox
-   * @param aReason
-   */
-  raiseProvisioningFailure: function raiseProvisioningFailure(aProvId, aReason) {
-    reportError("Provisioning failure", aReason);
-  },
-
-  /**
-   * When navigator.id.genKeyPair is called from provisioning iframe sandbox.
-   * Generates a keypair for the current user being provisioned.
-   *
-   * @param aProvId
-   *        (int)  the identifier of the provisioning caller tied to that sandbox
-   *
-   * It is an error to call genKeypair without receiving the callback for
-   * the beginProvisioning() call first.
-   */
-  genKeyPair: function genKeyPair(aProvId) {
-  },
-
-  /**
-   * When navigator.id.registerCertificate is called from provisioning iframe
-   * sandbox.
-   *
-   * Sets the certificate for the user for which a certificate was requested
-   * via a preceding call to beginProvisioning (and genKeypair).
-   *
-   * @param aProvId
-   *        (integer) the identifier of the provisioning caller tied to that
-   *                  sandbox
-   *
-   * @param aCert
-   *        (String)  A JWT representing the signed certificate for the user
-   *                  being provisioned, provided by the IdP.
-   */
-  registerCertificate: function registerCertificate(aProvId, aCert) {
-  },
-
-  /**
-   * The authentication frame has called navigator.id.beginAuthentication
-   *
-   * IMPORTANT: the aCaller is *always* non-null, even if this is called from
-   * a regular content page. We have to make sure, on every DOM call, that
-   * aCaller is an expected authentication-flow identifier. If not, we throw
-   * an error or something.
-   *
-   * @param aCaller
-   *        (object)  the authentication caller
-   *
-   */
-  beginAuthentication: function beginAuthentication(aCaller) {
-  },
-
-  /**
-   * The auth frame has called navigator.id.completeAuthentication
-   *
-   * @param aAuthId
-   *        (int)  the identifier of the authentication caller tied to that sandbox
-   *
-   */
-  completeAuthentication: function completeAuthentication(aAuthId) {
-  },
-
-  /**
-   * The auth frame has called navigator.id.cancelAuthentication
-   *
-   * @param aAuthId
-   *        (int)  the identifier of the authentication caller
-   *
-   */
-  cancelAuthentication: function cancelAuthentication(aAuthId) {
-  },
-
-  // methods for chrome and add-ons
-
-  /**
-   * Discover the IdP for an identity
-   *
-   * @param aIdentity
-   *        (string) the email we're logging in with
-   *
-   * @param aCallback
-   *        (function) callback to invoke on completion
-   *                   with first-positional parameter the error.
-   */
-  _discoverIdentityProvider: function _discoverIdentityProvider(aIdentity, aCallback) {
-    // XXX bug 767610 - validate email address call
-    // When that is available, we can remove this custom parser
-    var parsedEmail = this.parseEmail(aIdentity);
-    if (parsedEmail === null) {
-      return aCallback("Could not parse email: " + aIdentity);
-    }
-    log("_discoverIdentityProvider: identity:", aIdentity, "domain:", parsedEmail.domain);
-
-    this._fetchWellKnownFile(parsedEmail.domain, function fetchedWellKnown(err, idpParams) {
-      // idpParams includes the pk, authorization url, and
-      // provisioning url.
-
-      // XXX bug 769861 follow any authority delegations
-      // if no well-known at any point in the delegation
-      // fall back to browserid.org as IdP
-      return aCallback(err, idpParams);
-    });
-  },
-
-  /**
-   * Fetch the well-known file from the domain.
-   *
-   * @param aDomain
-   *
-   * @param aScheme
-   *        (string) (optional) Protocol to use.  Default is https.
-   *                 This is necessary because we are unable to test
-   *                 https.
-   *
-   * @param aCallback
-   *
-   */
-  _fetchWellKnownFile: function _fetchWellKnownFile(aDomain, aCallback, aScheme='https') {
-    // XXX bug 769854 make tests https and remove aScheme option
-    let url = aScheme + '://' + aDomain + "/.well-known/browserid";
-    log("_fetchWellKnownFile:", url);
-
-    // this appears to be a more successful way to get at xmlhttprequest (which supposedly will close with a window
-    let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
-                .createInstance(Ci.nsIXMLHttpRequest);
-
-    // XXX bug 769865 gracefully handle being off-line
-    // XXX bug 769866 decide on how to handle redirects
-    req.open("GET", url, true);
-    req.responseType = "json";
-    req.mozBackgroundRequest = true;
-    req.onload = function _fetchWellKnownFile_onload() {
-      if (req.status < 200 || req.status >= 400) {
-        log("_fetchWellKnownFile", url, ": server returned status:", req.status);
-        return aCallback("Error");
-      }
-      try {
-        let idpParams = req.response;
-
-        // Verify that the IdP returned a valid configuration
-        if (! (idpParams.provisioning &&
-            idpParams.authentication &&
-            idpParams['public-key'])) {
-          let errStr= "Invalid well-known file from: " + aDomain;
-          log("_fetchWellKnownFile:", errStr);
-          return aCallback(errStr);
-        }
-
-        let callbackObj = {
-          domain: aDomain,
-          idpParams: idpParams,
-        };
-        log("_fetchWellKnownFile result: ", callbackObj);
-        // Yay.  Valid IdP configuration for the domain.
-        return aCallback(null, callbackObj);
-
-      } catch (err) {
-        reportError("_fetchWellKnownFile", "Bad configuration from", aDomain, err);
-        return aCallback(err.toString());
-      }
-    };
-    req.onerror = function _fetchWellKnownFile_onerror() {
-      log("_fetchWellKnownFile", "ERROR:", req.status, req.statusText);
-      log("ERROR: _fetchWellKnownFile:", err);
-      return aCallback("Error");
-    };
-    req.send(null);
-  },
-
+  }
 };
 
 this.IdentityService = new IDService();
--- a/toolkit/identity/moz.build
+++ b/toolkit/identity/moz.build
@@ -25,11 +25,15 @@ EXTRA_JS_MODULES += [
     'IdentityUtils.jsm',
     'jwcrypto.jsm',
     'LogUtils.jsm',
     'MinimalIdentity.jsm',
     'RelyingParty.jsm',
     'Sandbox.jsm',
 ]
 
+EXTRA_PP_JS_MODULES += [
+    'FirefoxAccounts.jsm',
+]
+
 FAIL_ON_WARNINGS = True
 
 FINAL_LIBRARY = 'xul'
--- a/toolkit/identity/tests/unit/head_identity.js
+++ b/toolkit/identity/tests/unit/head_identity.js
@@ -29,21 +29,23 @@ XPCOMUtils.defineLazyModuleGetter(this,
                                   "Logger",
                                   "resource://gre/modules/identity/LogUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "uuidGenerator",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
+const TEST_MESSAGE_MANAGER = "Mr McFeeley";
 const TEST_URL = "https://myfavoritebacon.com";
 const TEST_URL2 = "https://myfavoritebaconinacan.com";
 const TEST_USER = "user@mozilla.com";
 const TEST_PRIVKEY = "fake-privkey";
 const TEST_CERT = "fake-cert";
+const TEST_ASSERTION = "face-assertion";
 const TEST_IDPPARAMS = {
   domain: "myfavoriteflan.com",
   authentication: "/foo/authenticate.html",
   provisioning: "/foo/provision.html"
 };
 
 // The following are utility functions for Identity testing
 
@@ -69,22 +71,46 @@ function uuid() {
 // create a mock "doc" object, which the Identity Service
 // uses as a pointer back into the doc object
 function mock_doc(aIdentity, aOrigin, aDoFunc) {
   let mockedDoc = {};
   mockedDoc.id = uuid();
   mockedDoc.loggedInUser = aIdentity;
   mockedDoc.origin = aOrigin;
   mockedDoc['do'] = aDoFunc;
+  mockedDoc._mm = TEST_MESSAGE_MANAGER;
   mockedDoc.doReady = partial(aDoFunc, 'ready');
   mockedDoc.doLogin = partial(aDoFunc, 'login');
   mockedDoc.doLogout = partial(aDoFunc, 'logout');
   mockedDoc.doError = partial(aDoFunc, 'error');
   mockedDoc.doCancel = partial(aDoFunc, 'cancel');
   mockedDoc.doCoffee = partial(aDoFunc, 'coffee');
+  mockedDoc.childProcessShutdown = partial(aDoFunc, 'child-process-shutdown');
+
+  mockedDoc.RP = mockedDoc;
+
+  return mockedDoc;
+}
+
+function mock_fxa_rp(aIdentity, aOrigin, aDoFunc) {
+  let mockedDoc = {};
+  mockedDoc.id = uuid();
+  mockedDoc.emailHint = aIdentity;
+  mockedDoc.origin = aOrigin;
+  mockedDoc.wantIssuer = "firefox-accounts";
+  mockedDoc._mm = TEST_MESSAGE_MANAGER;
+
+  mockedDoc.doReady = partial(aDoFunc, "ready");
+  mockedDoc.doLogin = partial(aDoFunc, "login");
+  mockedDoc.doLogout = partial(aDoFunc, "logout");
+  mockedDoc.doError = partial(aDoFunc, 'error');
+  mockedDoc.doCancel = partial(aDoFunc, 'cancel');
+  mockedDoc.childProcessShutdown = partial(aDoFunc, 'child-process-shutdown');
+
+  mockedDoc.RP = mockedDoc;
 
   return mockedDoc;
 }
 
 // mimicking callback funtionality for ease of testing
 // this observer auto-removes itself after the observe function
 // is called, so this is meant to observe only ONE event.
 function makeObserver(aObserveTopic, aObserveFunc) {
@@ -176,9 +202,29 @@ function setup_provisioning(identity, af
     if (callerCallbacks && callerCallbacks.genKeyPairCallback)
       callerCallbacks.genKeyPairCallback(pk);
   };
 
   afterSetupCallback(caller);
 }
 
 // Switch debug messages on by default
+let initialPrefDebugValue = false;
+try {
+  initialPrefDebugValue = Services.prefs.getBoolPref("toolkit.identity.debug");
+} catch(noPref) {}
 Services.prefs.setBoolPref("toolkit.identity.debug", true);
+
+// Switch on firefox accounts
+let initialPrefFXAValue = false;
+try {
+  initialPrefFXAValue = Services.prefs.getBoolPref("identity.fxaccounts.enabled");
+} catch(noPref) {}
+Services.prefs.setBoolPref("identity.fxaccounts.enabled", true);
+
+// after execution, restore prefs
+do_register_cleanup(function() {
+  log("restoring prefs to their initial values");
+  Services.prefs.setBoolPref("toolkit.identity.debug", initialPrefDebugValue);
+  Services.prefs.setBoolPref("identity.fxaccounts.enabled", initialPrefFXAValue);
+});
+
+
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_firefox_accounts.js
@@ -0,0 +1,168 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/DOMIdentity.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FirefoxAccounts",
+                                  "resource://gre/modules/identity/FirefoxAccounts.jsm");
+
+// Make the profile dir available; this is necessary so that
+// services/fxaccounts/FxAccounts.jsm can read and write its signed-in user
+// data.
+do_get_profile();
+
+function MockFXAManager() {}
+MockFXAManager.prototype = {
+  getAssertion: function(audience) {
+    let deferred = Promise.defer();
+    deferred.resolve(TEST_ASSERTION);
+    return deferred.promise;
+  }
+}
+
+let originalManager = FirefoxAccounts.fxAccountsManager;
+FirefoxAccounts.fxAccountsManager = new MockFXAManager();
+do_register_cleanup(() => {
+  print("restoring fxaccountsmanager");
+  FirefoxAccounts.fxAccountsManager = originalManager;
+});
+
+function test_overall() {
+  do_check_neq(FirefoxAccounts, null);
+  run_next_test();
+}
+
+function test_mock() {
+  do_test_pending();
+
+  FirefoxAccounts.fxAccountsManager.getAssertion().then(assertion => {
+    do_check_eq(assertion, TEST_ASSERTION);
+    do_test_finished();
+    run_next_test();
+  });
+}
+
+function test_watch() {
+  do_test_pending();
+
+  let mockedRP = mock_fxa_rp(null, TEST_URL, function(method) {
+    do_check_eq(method, "ready");
+    do_test_finished();
+    run_next_test();
+  });
+
+  FirefoxAccounts.RP.watch(mockedRP);
+}
+
+function test_request() {
+  do_test_pending();
+
+  let received = [];
+
+  let mockedRP = mock_fxa_rp(null, TEST_URL, function(method) {
+    // We will received "ready" as a result of watch(), then "login"
+    // as a result of request()
+    received.push(method);
+
+    if (received.length == 2) {
+      do_check_eq(received[0], "ready");
+      do_check_eq(received[1], "login");
+      do_test_finished();
+      run_next_test();
+    }
+
+    // Second, call request()
+    if (method == "ready") {
+      FirefoxAccounts.RP.request(mockedRP.id);
+    }
+  });
+
+  // First, call watch()
+  FirefoxAccounts.RP.watch(mockedRP);
+}
+
+function test_logout() {
+  do_test_pending();
+
+  let received = [];
+
+  let mockedRP = mock_fxa_rp(null, TEST_URL, function(method) {
+    // We will receive "ready" as a result of watch(), and "logout"
+    // as a result of logout()
+    received.push(method);
+
+    if (received.length == 2) {
+      do_check_eq(received[0], "ready");
+      do_check_eq(received[1], "logout");
+      do_test_finished();
+      run_next_test();
+    }
+
+    if (method == "ready") {
+      // Second, call logout()
+      FirefoxAccounts.RP.logout(mockedRP.id);
+    }
+  });
+
+  // First, call watch()
+  FirefoxAccounts.RP.watch(mockedRP);
+}
+
+function test_child_process_shutdown() {
+  do_test_pending();
+  let rpCount = FirefoxAccounts.RP._rpFlows.size;
+
+  makeObserver("identity-child-process-shutdown", (aTopic, aSubject, aData) => {
+    // Last of all, the shutdown observer message will be fired.
+    // This takes place after the RP has a chance to delete flows
+    // and clean up.
+    do_check_eq(FirefoxAccounts.RP._rpFlows.size, rpCount);
+    do_test_finished();
+    run_next_test();
+  });
+
+  let mockedRP = mock_fxa_rp(null, TEST_URL, (method) => {
+    // We should enter this function for 'ready' and 'child-process-shutdown'.
+    // After we have a chance to do our thing, the shutdown observer message
+    // will fire and be caught by the function above.
+    do_check_eq(FirefoxAccounts.RP._rpFlows.size, rpCount + 1);
+    switch (method) {
+      case "ready":
+        DOMIdentity._childProcessShutdown("my message manager");
+        break;
+
+      case "child-process-shutdown":
+        // We have to call this explicitly because there's no real
+        // dom window here.
+        FirefoxAccounts.RP.childProcessShutdown(mockedRP._mm);
+        break;
+
+      default:
+        break;
+    }
+  });
+
+  mockedRP._mm = "my message manager";
+  FirefoxAccounts.RP.watch(mockedRP);
+
+  // fake a dom window context
+  DOMIdentity.newContext(mockedRP, mockedRP._mm);
+}
+
+let TESTS = [
+  test_overall,
+  test_mock,
+  test_watch,
+  test_request,
+  test_logout,
+  test_child_process_shutdown,
+];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+  run_next_test();
+}
--- a/toolkit/identity/tests/unit/test_minimalidentity.js
+++ b/toolkit/identity/tests/unit/test_minimalidentity.js
@@ -1,15 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "MinimalIDService",
                                   "resource://gre/modules/identity/MinimalIdentity.jsm",
                                   "IdentityService");
 
 Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/DOMIdentity.jsm");
 
 function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["test_minimalidentity"].concat(aMessageArgs));
 }
 
 function test_overall() {
   do_check_neq(MinimalIDService, null);
   run_next_test();
@@ -159,26 +163,61 @@ function test_unwatchBeforeWatch() {
 
   let mockedDoc = mock_doc(null, TEST_URL);
 
   MinimalIDService.RP.unwatch(mockedDoc.id, {});
   do_test_finished();
   run_next_test();
 }
 
+/*
+ * Test that the RP flow is cleaned up on child process shutdown
+ */
+
+function test_childProcessShutdown() {
+  do_test_pending();
+  let UNIQUE_MESSAGE_MANAGER = "i am a beautiful snowflake";
+  let initialRPCount = Object.keys(MinimalIDService.RP._rpFlows).length;
+
+  let mockedDoc = mock_doc(null, TEST_URL, (action, params) => {
+    if (action == "child-process-shutdown") {
+      // since there's no actual dom window connection, we have to
+      // do this bit manually here.
+      MinimalIDService.RP.childProcessShutdown(UNIQUE_MESSAGE_MANAGER);
+    }
+  });
+  mockedDoc._mm = UNIQUE_MESSAGE_MANAGER;
+
+  makeObserver("identity-controller-watch", function (aSubject, aTopic, aData) {
+    DOMIdentity._childProcessShutdown(UNIQUE_MESSAGE_MANAGER);
+  });
+
+  makeObserver("identity-child-process-shutdown", (aTopic, aSubject, aData) => {
+    do_check_eq(Object.keys(MinimalIDService.RP._rpFlows).length, initialRPCount);
+    do_test_finished();
+    run_next_test();
+  });
+
+  // fake a dom window context
+  DOMIdentity.newContext(mockedDoc, UNIQUE_MESSAGE_MANAGER);
+
+  MinimalIDService.RP.watch(mockedDoc);
+}
+
 let TESTS = [
   test_overall,
   test_mock_doc,
   test_watch,
   test_request,
   test_request_forceAuthentication,
   test_request_forceIssuer,
   test_logout,
   test_logoutBeforeWatch,
   test_requestBeforeWatch,
   test_unwatchBeforeWatch,
+  test_childProcessShutdown,
 ];
 
 TESTS.forEach(add_test);
 
 function run_test() {
   run_next_test();
 }
--- a/toolkit/identity/tests/unit/xpcshell.ini
+++ b/toolkit/identity/tests/unit/xpcshell.ini
@@ -3,16 +3,17 @@ head = head_identity.js
 tail = tail_identity.js
 support-files =
   data/idp_1/.well-known/browserid
   data/idp_invalid_1/.well-known/browserid
 
 # Test load modules first so syntax failures are caught early.
 [test_load_modules.js]
 [test_minimalidentity.js]
+[test_firefox_accounts.js]
 
 [test_identity_utils.js]
 [test_log_utils.js]
 [test_authentication.js]
 # Identity modules aren't packaged on Android.
 skip-if = os == "android"
 [test_crypto_service.js]
 skip-if = os == "android"
--- a/toolkit/modules/CharsetMenu.jsm
+++ b/toolkit/modules/CharsetMenu.jsm
@@ -226,16 +226,16 @@ let CharsetMenu = {
     return charset;
   },
   _getCharsetAccessKey: function(charset) {
     if (charset == "gbk") {
       // Localization key has been revised
       charset = "gbk.bis";
     }
     try {
-      accesskey = gBundle.GetStringFromName(charset + ".key");
+      return gBundle.GetStringFromName(charset + ".key");
     } catch (ex) {}
     return "";
   },
 };
 
 Object.freeze(CharsetMenu);
 
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -70,16 +70,18 @@ const PREF_XPI_UNPACK                 = 
 const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
 const PREF_INSTALL_DISTRO_ADDONS      = "extensions.installDistroAddons";
 const PREF_BRANCH_INSTALLED_ADDON     = "extensions.installedDistroAddon.";
 const PREF_SHOWN_SELECTION_UI         = "extensions.shownSelectionUI";
 
 const PREF_EM_MIN_COMPAT_APP_VERSION      = "extensions.minCompatibleAppVersion";
 const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
 
+const PREF_CHECKCOMAT_THEMEOVERRIDE   = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion";
+
 const URI_EXTENSION_SELECT_DIALOG     = "chrome://mozapps/content/extensions/selectAddons.xul";
 const URI_EXTENSION_UPDATE_DIALOG     = "chrome://mozapps/content/extensions/update.xul";
 const URI_EXTENSION_STRINGS           = "chrome://mozapps/locale/extensions/extensions.properties";
 
 const STRING_TYPE_NAME                = "type.%ID%.name";
 
 const DIR_EXTENSIONS                  = "extensions";
 const DIR_STAGE                       = "staged";
@@ -598,18 +600,32 @@ function isUsableAddon(aAddon) {
   if (!aAddon.isPlatformCompatible)
     return false;
 
   if (AddonManager.checkCompatibility) {
     if (!aAddon.isCompatible)
       return false;
   }
   else {
-    if (!aAddon.matchingTargetApplication)
+    let app = aAddon.matchingTargetApplication;
+    if (!app)
       return false;
+
+    // XXX Temporary solution to let applications opt-in to make themes safer
+    //     following significant UI changes even if checkCompatibility=false has
+    //     been set, until we get bug 962001.
+    if (aAddon.type == "theme" && app.id == Services.appinfo.ID) {
+      try {
+        let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE);
+        if (minCompatVersion &&
+            Services.vc.compare(minCompatVersion, app.maxVersion) > 0) {
+          return false;
+        }
+      } catch (e) {}
+    }
   }
 
   return true;
 }
 
 function isAddonDisabled(aAddon) {
   return aAddon.appDisabled || aAddon.softDisabled || aAddon.userDisabled;
 }
--- a/toolkit/mozapps/extensions/moz.build
+++ b/toolkit/mozapps/extensions/moz.build
@@ -45,15 +45,15 @@ EXTRA_PP_JS_MODULES += [
     'XPIProviderUtils.js',
 ]
 
 if CONFIG['MOZ_UPDATE_CHANNEL'] not in ('aurora', 'beta', 'release', 'esr'):
     DEFINES['MOZ_COMPATIBILITY_NIGHTLY'] = 1
 
 # This is used in multiple places, so is defined here to avoid it getting
 # out of sync.
-DEFINES['MOZ_EXTENSIONS_DB_SCHEMA'] = 15
+DEFINES['MOZ_EXTENSIONS_DB_SCHEMA'] = 16
 
 # Additional debugging info is exposed in debug builds
 if CONFIG['MOZ_DEBUG']:
     DEFINES['MOZ_EM_DEBUG'] = 1
 
 JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_checkCompatibility_themeOverride.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that the (temporary)
+// extensions.checkCompatibility.temporaryThemeOverride_minAppVersion
+// preference works.
+
+var ADDONS = [{
+  id: "addon1@tests.mozilla.org",
+  type: 4,
+  internalName: "theme1/1.0",
+  version: "1.0",
+  name: "Test 1",
+  targetApplications: [{
+    id: "xpcshell@tests.mozilla.org",
+    minVersion: "1.0",
+    maxVersion: "1.0"
+  }]
+}, {
+  id: "addon2@tests.mozilla.org",
+  type: 4,
+  internalName: "theme2/1.0",
+  version: "1.0",
+  name: "Test 2",
+  targetApplications: [{
+    id: "xpcshell@tests.mozilla.org",
+    minVersion: "2.0",
+    maxVersion: "2.0"
+  }]
+}];
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+
+function run_test() {
+  do_test_pending();
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3.0", "1");
+
+  for (let a of ADDONS) {
+    writeInstallRDFForExtension(a, profileDir);
+  }
+
+  startupManager();
+
+  run_test_1();
+}
+
+function run_test_1() {
+  AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+                               "addon2@tests.mozilla.org"],
+                               function([a1, a2]) {
+
+    do_check_neq(a1, null);
+    do_check_false(a1.isActive);
+    do_check_false(a1.isCompatible);
+    do_check_true(a1.appDisabled);
+
+    do_check_neq(a2, null);
+    do_check_false(a2.isActive);
+    do_check_false(a2.isCompatible);
+    do_check_true(a1.appDisabled);
+
+    do_execute_soon(run_test_2);
+  });
+}
+
+function run_test_2() {
+  Services.prefs.setCharPref("extensions.checkCompatibility.temporaryThemeOverride_minAppVersion", "2.0");
+  if (isNightlyChannel())
+    Services.prefs.setBoolPref("extensions.checkCompatibility.nightly", false);
+  else
+    Services.prefs.setBoolPref("extensions.checkCompatibility.3.0", false);
+  restartManager();
+
+  AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+                               "addon2@tests.mozilla.org"],
+                               function([a1, a2]) {
+
+    do_check_neq(a1, null);
+    do_check_false(a1.isActive);
+    do_check_false(a1.isCompatible);
+    do_check_true(a1.appDisabled);
+
+    do_check_neq(a2, null);
+    do_check_false(a2.isActive);
+    do_check_false(a2.isCompatible);
+    do_check_false(a2.appDisabled);
+
+    do_execute_soon(do_test_finished);
+  });
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -139,16 +139,17 @@ fail-if = os == "android"
 [test_bug659772.js]
 [test_bug675371.js]
 [test_bug740612.js]
 [test_bug753900.js]
 [test_bug757663.js]
 [test_bug953156.js]
 [test_cacheflush.js]
 [test_checkcompatibility.js]
+[test_checkCompatibility_themeOverride.js]
 [test_childprocess.js]
 [test_ChromeManifestParser.js]
 [test_compatoverrides.js]
 [test_corrupt.js]
 [test_corrupt_strictcompat.js]
 [test_corruptfile.js]
 [test_default_providers_pref.js]
 [test_dictionary.js]
--- a/toolkit/webapps/WebappsInstaller.jsm
+++ b/toolkit/webapps/WebappsInstaller.jsm
@@ -224,18 +224,18 @@ NativeApp.prototype = {
    * If the retrieving fails, it uses the default chrome icon.
    */
   getIcon: function() {
     try {
       // If the icon is in the zip package, we should modify the url
       // to point to the zip file (we can't use the app protocol yet
       // because the app isn't installed yet).
       if (this.iconURI.scheme == "app") {
-        let zipFile = getFile(this.tmpInstallDir, "application.zip");
-        let zipUrl = Services.io.newFileURI(zipFile).spec;
+        let zipUrl = OS.Path.toFileURI(OS.Path.join(this.tmpInstallDir,
+                                                    "application.zip"));
 
         let filePath = this.iconURI.QueryInterface(Ci.nsIURL).filePath;
 
         this.iconURI = Services.io.newURI("jar:" + zipUrl + "!" + filePath,
                                           null, null);
       }
 
 
@@ -335,17 +335,17 @@ WinNativeApp.prototype = {
   size: null,
 
   /**
    * Install the app in the system
    *
    */
   install: function(aZipPath) {
     return Task.spawn(function() {
-      this._getInstallDir();
+      yield this._getInstallDir();
 
       try {
         yield this._createDirectoryStructure();
         yield this._copyPrebuiltFiles();
         this._createConfigFiles();
 
         if (aZipPath) {
           yield OS.File.move(aZipPath, OS.Path.join(this.tmpInstallDir,
@@ -395,19 +395,19 @@ WinNativeApp.prototype = {
                      .getService(Ci.nsIINIParserFactory)
                      .createINIParser(shortcutLogsINIfile);
       this.shortcutName = parser.getString("STARTMENU", "Shortcut0");
     } else {
       this.installDir = OS.Path.join(APP_DATA_DIR, this.uniqueName);
 
       // Check in both directories to see if a shortcut with the same name
       // already exists.
-      this.shortcutName = getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ],
-                                               this.appNameAsFilename,
-                                               ".lnk");
+      this.shortcutName = yield getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ],
+                                                     this.appNameAsFilename,
+                                                     ".lnk");
     }
   },
 
   /**
    * Remove the current installation
    */
   _removeInstallation: function(keepProfile) {
     let uninstallKey;
@@ -665,17 +665,17 @@ function MacNativeApp(aData) {
   this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns");
 }
 
 MacNativeApp.prototype = {
   __proto__: NativeApp.prototype,
 
   install: function(aZipPath) {
     return Task.spawn(function() {
-      this._getInstallDir();
+      yield this._getInstallDir();
 
       try {
         yield this._createDirectoryStructure();
         this._copyPrebuiltFiles();
         this._createConfigFiles();
 
         if (aZipPath) {
           yield OS.File.move(aZipPath, OS.Path.join(this.tmpInstallDir,
@@ -707,23 +707,25 @@ MacNativeApp.prototype = {
       this.installDir = installPath;
 
       if (this.uniqueName != oldUniqueName) {
         // Bug 919799: If the app is still in the registry, migrate its data to
         // the new format.
         throw("Updates for apps installed with the old naming scheme unsupported");
       }
     } else {
-      let destinationName = getAvailableFileName([ LOCAL_APP_DIR ],
-                                                 this.appNameAsFilename,
-                                                ".app");
-      if (!destinationName) {
-        throw("No available filename");
+      let localAppDir = getFile(LOCAL_APP_DIR);
+      if (!localAppDir.isWritable()) {
+        throw("Not enough privileges to install apps");
       }
 
+      let destinationName = yield getAvailableFileName([ LOCAL_APP_DIR ],
+                                                       this.appNameAsFilename,
+                                                       ".app");
+
       this.installDir = OS.Path.join(LOCAL_APP_DIR, destinationName);
     }
   },
 
   _removeInstallation: function(keepProfile) {
     let filesToRemove = [this.installDir];
 
     if (!keepProfile) {
@@ -1123,68 +1125,47 @@ function stripStringForFilename(aPossibl
  * directories where we want to write
  * @param aName   string with the filename (minus the extension) desired
  * @param aExtension string with the file extension, including the dot
 
  * @return file name or null if folder is unwritable or unique name
  *         was not available
  */
 function getAvailableFileName(aPathSet, aName, aExtension) {
-  let fileSet = [];
-  let name = aName + aExtension;
-  let isUnique = true;
+  return Task.spawn(function*() {
+    let name = aName + aExtension;
 
-  // Check if the plain name is a unique name in all the directories.
-  for (let path of aPathSet) {
-    let folder = getFile(path);
+    function checkUnique(aName) {
+      return Task.spawn(function*() {
+        for (let path of aPathSet) {
+          if (yield OS.File.exists(OS.Path.join(path, aName))) {
+            return false;
+          }
+        }
 
-    folder.followLinks = false;
-    if (!folder.isDirectory() || !folder.isWritable()) {
-      return null;
+        return true;
+      });
     }
 
-    let file = folder.clone();
-    file.append(name);
-    // Avoid exists() call if we already know this file name is not unique in
-    // one of the directories.
-    if (isUnique && file.exists()) {
-      isUnique = false;
+    if (yield checkUnique(name)) {
+      return name;
     }
 
-    fileSet.push(file);
-  }
-
-  if (isUnique) {
-    return name;
-  }
+    // If we're here, the plain name wasn't enough. Let's try modifying the name
+    // by adding "(" + num + ")".
+    for (let i = 2; i < 100; i++) {
+      name = aName + " (" + i + ")" + aExtension;
 
-
-  function checkUnique(aName) {
-    for (let file of fileSet) {
-      file.leafName = aName;
-
-      if (file.exists()) {
-        return false;
+      if (yield checkUnique(name)) {
+        return name;
       }
     }
 
-    return true;
-  }
-
-  // If we're here, the plain name wasn't enough. Let's try modifying the name
-  // by adding "(" + num + ")".
-  for (let i = 2; i < 100; i++) {
-    name = aName + " (" + i + ")" + aExtension;
-
-    if (checkUnique(name)) {
-      return name;
-    }
-  }
-
-  return null;
+    throw "No available filename";
+  });
 }
 
 /**
  * Attempts to remove files or directories.
  *
  * @param aPaths An array with paths to files to remove
  */
 function removeFiles(aPaths) {