Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 28 Oct 2015 10:51:36 +0100
changeset 305134 1d67c4e4858f3a6ea5bf39b31fe11bef3cd001ac
parent 305133 9f16e62baa3acf4684dcd545df69b0b010f803d1 (current diff)
parent 305068 fc706d376f0658e560a59c3dd520437b18e8c4a4 (diff)
child 305135 bda42988725cb4ed31f56abc9ff7717d91adf64a
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone44.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 mozilla-central to mozilla-inbound
devtools/client/memory/modules/census-view.js
devtools/client/memory/modules/moz.build
devtools/client/memory/test/mochitest/chrome.ini
devtools/client/memory/test/mochitest/head.js
devtools/client/memory/test/mochitest/test_census-view-01.html
mobile/android/tests/background/junit3/src/common/TestAndroidLogWriters.java
mobile/android/tests/background/junit3/src/common/TestBrowserContractHelpers.java
mobile/android/tests/background/junit3/src/common/TestDateUtils.java
mobile/android/tests/background/junit3/src/common/TestUtils.java
mobile/android/tests/background/junit3/src/common/TestWaitHelper.java
mobile/android/tests/background/junit3/src/db/AndroidBrowserRepositoryTestCase.java
mobile/android/tests/background/junit3/src/db/TestAndroidBrowserBookmarksRepository.java
mobile/android/tests/background/junit3/src/db/TestAndroidBrowserHistoryDataExtender.java
mobile/android/tests/background/junit3/src/db/TestAndroidBrowserHistoryRepository.java
mobile/android/tests/background/junit3/src/db/TestBookmarks.java
mobile/android/tests/background/junit3/src/db/TestCachedSQLiteOpenHelper.java
mobile/android/tests/background/junit3/src/db/TestClientsDatabase.java
mobile/android/tests/background/junit3/src/db/TestClientsDatabaseAccessor.java
mobile/android/tests/background/junit3/src/db/TestFennecTabsRepositorySession.java
mobile/android/tests/background/junit3/src/db/TestFennecTabsStorage.java
mobile/android/tests/background/junit3/src/db/TestFormHistoryRepositorySession.java
mobile/android/tests/background/junit3/src/db/TestPasswordsRepository.java
mobile/android/tests/background/junit3/src/fxa/TestAccountLoader.java
mobile/android/tests/background/junit3/src/fxa/TestBrowserIDKeyPairGeneration.java
mobile/android/tests/background/junit3/src/fxa/TestFirefoxAccounts.java
mobile/android/tests/background/junit3/src/fxa/authenticator/TestAccountPickler.java
mobile/android/tests/background/junit3/src/healthreport/MockDatabaseEnvironment.java
mobile/android/tests/background/junit3/src/healthreport/MockHealthReportDatabaseStorage.java
mobile/android/tests/background/junit3/src/healthreport/MockHealthReportSQLiteOpenHelper.java
mobile/android/tests/background/junit3/src/healthreport/MockProfileInformationCache.java
mobile/android/tests/background/junit3/src/healthreport/TestEnvironmentBuilder.java
mobile/android/tests/background/junit3/src/healthreport/TestEnvironmentV1HashAppender.java
mobile/android/tests/background/junit3/src/healthreport/TestHealthReportBroadcastService.java
mobile/android/tests/background/junit3/src/healthreport/TestHealthReportDatabaseStorage.java
mobile/android/tests/background/junit3/src/healthreport/TestHealthReportGenerator.java
mobile/android/tests/background/junit3/src/healthreport/TestHealthReportProvider.java
mobile/android/tests/background/junit3/src/healthreport/TestHealthReportSQLiteOpenHelper.java
mobile/android/tests/background/junit3/src/healthreport/TestProfileInformationCache.java
mobile/android/tests/background/junit3/src/healthreport/prune/TestHealthReportPruneService.java
mobile/android/tests/background/junit3/src/healthreport/prune/TestPrunePolicyDatabaseStorage.java
mobile/android/tests/background/junit3/src/healthreport/upload/TestAndroidSubmissionClient.java
mobile/android/tests/background/junit3/src/healthreport/upload/TestHealthReportUploadService.java
mobile/android/tests/background/junit3/src/helpers/AndroidSyncTestCase.java
mobile/android/tests/background/junit3/src/helpers/BackgroundServiceTestCase.java
mobile/android/tests/background/junit3/src/helpers/DBHelpers.java
mobile/android/tests/background/junit3/src/helpers/DBProviderTestCase.java
mobile/android/tests/background/junit3/src/helpers/FakeProfileTestCase.java
mobile/android/tests/background/junit3/src/nativecode/test/TestNativeCrypto.java
mobile/android/tests/background/junit3/src/sync/AndroidSyncTestCaseWithAccounts.java
mobile/android/tests/background/junit3/src/sync/TestAccountPickler.java
mobile/android/tests/background/junit3/src/sync/TestClientsStage.java
mobile/android/tests/background/junit3/src/sync/TestConfigurationMigrator.java
mobile/android/tests/background/junit3/src/sync/TestResetting.java
mobile/android/tests/background/junit3/src/sync/TestSendTabData.java
mobile/android/tests/background/junit3/src/sync/TestStoreTracking.java
mobile/android/tests/background/junit3/src/sync/TestSyncAccounts.java
mobile/android/tests/background/junit3/src/sync/TestSyncAuthenticatorService.java
mobile/android/tests/background/junit3/src/sync/TestSyncConfiguration.java
mobile/android/tests/background/junit3/src/sync/TestTabsRecord.java
mobile/android/tests/background/junit3/src/sync/TestUpgradeRequired.java
mobile/android/tests/background/junit3/src/sync/TestWebURLFinder.java
mobile/android/tests/background/junit3/src/sync/helpers/BookmarkHelpers.java
mobile/android/tests/background/junit3/src/sync/helpers/DefaultBeginDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/DefaultCleanDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/DefaultDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/DefaultFetchDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/DefaultFinishDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/DefaultGuidsSinceDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/DefaultSessionCreationDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/DefaultStoreDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectBeginDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectBeginFailDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectFetchDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectFetchSinceDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectFinishDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectFinishFailDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectGuidsSinceDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectInvalidRequestFetchDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectInvalidTypeStoreDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectManyStoredDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectNoGUIDsSinceDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectNoStoreDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectStoreCompletedDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/ExpectStoredDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/HistoryHelpers.java
mobile/android/tests/background/junit3/src/sync/helpers/PasswordHelpers.java
mobile/android/tests/background/junit3/src/sync/helpers/SessionTestHelper.java
mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessBeginDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessCreationDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessFetchDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessFinishDelegate.java
mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessStoreDelegate.java
mobile/android/tests/background/junit3/src/telemetry/TestTelemetryRecorder.java
mobile/android/tests/background/junit3/src/testhelpers/BaseMockServerSyncStage.java
mobile/android/tests/background/junit3/src/testhelpers/CommandHelpers.java
mobile/android/tests/background/junit3/src/testhelpers/DefaultGlobalSessionCallback.java
mobile/android/tests/background/junit3/src/testhelpers/JPakeNumGeneratorFixed.java
mobile/android/tests/background/junit3/src/testhelpers/MockAbstractNonRepositorySyncStage.java
mobile/android/tests/background/junit3/src/testhelpers/MockClientsDataDelegate.java
mobile/android/tests/background/junit3/src/testhelpers/MockClientsDatabaseAccessor.java
mobile/android/tests/background/junit3/src/testhelpers/MockGlobalSession.java
mobile/android/tests/background/junit3/src/testhelpers/MockPrefsGlobalSession.java
mobile/android/tests/background/junit3/src/testhelpers/MockRecord.java
mobile/android/tests/background/junit3/src/testhelpers/MockServerSyncStage.java
mobile/android/tests/background/junit3/src/testhelpers/MockSharedPreferences.java
mobile/android/tests/background/junit3/src/testhelpers/StubDelegate.java
mobile/android/tests/background/junit3/src/testhelpers/WBORepository.java
mobile/android/tests/background/junit3/src/testhelpers/WaitHelper.java
--- a/b2g/config/aries/sources.xml
+++ b/b2g/config/aries/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="1b0db93fb6b870b03467aff50d6419771ba0d88c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="4ace9aaee0e048dfda11bb787646c59982a3dc80"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c72c9278ddc2f442d193474993d36e7f2cfb08c4"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
   <!-- Stock Android things -->
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="660169a3d7e034a892359e39135e8c2785a6ad6f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1837d370a964a9719160c79155a07980f2ea4bdf"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
   <project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1837d370a964a9719160c79155a07980f2ea4bdf"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1837d370a964a9719160c79155a07980f2ea4bdf"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="1b0db93fb6b870b03467aff50d6419771ba0d88c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="4ace9aaee0e048dfda11bb787646c59982a3dc80"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c72c9278ddc2f442d193474993d36e7f2cfb08c4"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
   <!-- Stock Android things -->
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "a26eadc5e1133d5112b6cbc10badbb7670a1090f", 
+        "git_revision": "2e89362de40a6c9c36525d36317fa1ae8e67e143", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "a99ff14b3258f49f5902775a5e3b849f3455714a", 
+    "revision": "4a596a387a18b019fc2763b3509940d52154d72c", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4-kk/sources.xml
+++ b/b2g/config/nexus-4-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -13,17 +13,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="660169a3d7e034a892359e39135e8c2785a6ad6f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1837d370a964a9719160c79155a07980f2ea4bdf"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
   <project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- a/browser/base/content/contentSearchUI.js
+++ b/browser/base/content/contentSearchUI.js
@@ -571,16 +571,22 @@ ContentSearchUIController.prototype = {
       this.input.setAttribute("aria-expanded", "true");
       this._originalDefaultEngine = {
         name: this.defaultEngine.name,
         icon: this.defaultEngine.icon,
       };
     }
   },
 
+  _onMsgSuggestionsCancelled: function () {
+    if (!this._table.hidden) {
+      this._hideSuggestions();
+    }
+  },
+
   _onMsgState: function (state) {
     this.engines = state.engines;
     // No point updating the default engine (and the header) if there's no change.
     if (this.defaultEngine &&
         this.defaultEngine.name == state.currentEngine.name &&
         this.defaultEngine.icon == state.currentEngine.icon) {
       return;
     }
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -416,23 +416,27 @@ loop.panel = (function(_, mozL10n) {
     },
 
     render: function() {
       var roomClasses = React.addons.classSet({
         "room-entry": true,
         "room-active": this._isActive()
       });
 
+      var roomTitle = this.props.room.decryptedContext.roomName ||
+        this.props.room.decryptedContext.urls[0].description ||
+        this.props.room.decryptedContext.urls[0].location;
+
       return (
         React.createElement("div", {className: roomClasses, 
           onClick: this.handleClickEntry, 
           onMouseLeave: this._handleMouseOut, 
           ref: "roomEntry"}, 
           React.createElement("h2", null, 
-            this.props.room.decryptedContext.roomName
+            roomTitle
           ), 
           React.createElement(RoomEntryContextItem, {
             mozLoop: this.props.mozLoop, 
             roomUrls: this.props.room.decryptedContext.urls}), 
           React.createElement(RoomEntryContextButtons, {
             dispatcher: this.props.dispatcher, 
             eventPosY: this.state.eventPosY, 
             handleClickEntry: this.handleClickEntry, 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -416,23 +416,27 @@ loop.panel = (function(_, mozL10n) {
     },
 
     render: function() {
       var roomClasses = React.addons.classSet({
         "room-entry": true,
         "room-active": this._isActive()
       });
 
+      var roomTitle = this.props.room.decryptedContext.roomName ||
+        this.props.room.decryptedContext.urls[0].description ||
+        this.props.room.decryptedContext.urls[0].location;
+
       return (
         <div className={roomClasses}
           onClick={this.handleClickEntry}
           onMouseLeave={this._handleMouseOut}
           ref="roomEntry">
           <h2>
-            {this.props.room.decryptedContext.roomName}
+            {roomTitle}
           </h2>
           <RoomEntryContextItem
             mozLoop={this.props.mozLoop}
             roomUrls={this.props.room.decryptedContext.urls} />
           <RoomEntryContextButtons
             dispatcher={this.props.dispatcher}
             eventPosY={this.state.eventPosY}
             handleClickEntry={this.handleClickEntry}
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -750,18 +750,21 @@ loop.roomViews = (function(mozL10n) {
       // Handle timestamp and window closing only when the call has terminated.
       if (prevState.roomState === ROOM_STATES.ENDED &&
           this.state.roomState === ROOM_STATES.ENDED) {
         this.props.onCallTerminated();
       }
     },
 
     render: function() {
-      if (this.state.roomName) {
-        this.setTitle(this.state.roomName);
+      if (this.state.roomName || this.state.roomContextUrls) {
+        var roomTitle = this.state.roomName ||
+                        this.state.roomContextUrls[0].description ||
+                        this.state.roomContextUrls[0].location;
+        this.setTitle(roomTitle);
       }
 
       var screenShareData = {
         state: this.state.screenSharingState || SCREEN_SHARE_STATES.INACTIVE,
         visible: true
       };
 
       var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -750,18 +750,21 @@ loop.roomViews = (function(mozL10n) {
       // Handle timestamp and window closing only when the call has terminated.
       if (prevState.roomState === ROOM_STATES.ENDED &&
           this.state.roomState === ROOM_STATES.ENDED) {
         this.props.onCallTerminated();
       }
     },
 
     render: function() {
-      if (this.state.roomName) {
-        this.setTitle(this.state.roomName);
+      if (this.state.roomName || this.state.roomContextUrls) {
+        var roomTitle = this.state.roomName ||
+                        this.state.roomContextUrls[0].description ||
+                        this.state.roomContextUrls[0].location;
+        this.setTitle(roomTitle);
       }
 
       var screenShareData = {
         state: this.state.screenSharingState || SCREEN_SHARE_STATES.INACTIVE,
         visible: true
       };
 
       var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
--- a/browser/components/loop/content/shared/js/textChatStore.js
+++ b/browser/components/loop/content/shared/js/textChatStore.js
@@ -157,19 +157,24 @@ loop.store.TextChatStore = (function() {
      * so it can be added to the list.
      *
      * @param  {sharedActions.UpdateRoomInfo} actionData
      */
     updateRoomInfo: function(actionData) {
       // XXX When we add special messages to desktop, we'll need to not post
       // multiple changes of room name, only the first. Bug 1171940 should fix this.
       if (actionData.roomName) {
+        var roomName = actionData.roomName;
+        if (!roomName && actionData.roomContextUrls && actionData.roomContextUrls.length) {
+          roomName = actionData.roomContextUrls[0].description ||
+                     actionData.roomContextUrls[0].url;
+        }
         this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
           contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
-          message: actionData.roomName
+          message: roomName
         });
       }
 
       // Append the context if we have any.
       if (("roomContextUrls" in actionData) && actionData.roomContextUrls &&
           actionData.roomContextUrls.length) {
         // We only support the first url at the moment.
         var urlData = actionData.roomContextUrls[0];
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -97,25 +97,28 @@ loop.standaloneRoomViews = (function(moz
 
     _renderFailureText: function() {
       return (
         React.createElement("p", {className: "failure"}, mozL10n.get("rooms_already_joined"))
       );
     },
 
     render: function() {
+      var roomName = this.state.roomName ||
+                     this.state.roomContextUrls[0].description ||
+                     this.state.roomContextUrls[0].location;
       // The extra scroller div here is for providing a scroll view for shorter
       // screens, as the common.css specifies overflow:hidden for the body which
       // we need in some places.
       return (
         React.createElement("div", {className: "handle-user-agent-view-scroller"}, 
           React.createElement("div", {className: "handle-user-agent-view"}, 
             React.createElement("div", {className: "info-panel"}, 
               React.createElement("p", {className: "loop-logo-text", title: mozL10n.get("clientShortname2")}), 
-              React.createElement("p", {className: "roomName"}, this.state.roomName), 
+              React.createElement("p", {className: "roomName"}, roomName), 
               React.createElement("p", {className: "loop-logo"}), 
               
                 this.state.failureReason ?
                   this._renderFailureText() :
                   this._renderJoinButton()
               
             ), 
             React.createElement(ToSView, {dispatcher: this.props.dispatcher}), 
@@ -439,26 +442,25 @@ loop.standaloneRoomViews = (function(moz
      * user media access.
      *
      * @param  {Object} nextProps (Unused)
      * @param  {Object} nextState Next state object.
      */
     componentWillUpdate: function(nextProps, nextState) {
       if (this.state.roomState !== ROOM_STATES.READY &&
           nextState.roomState === ROOM_STATES.READY) {
-        var roomName = nextState.roomName || this.state.roomName;
+        var roomName = nextState.roomName ||
+                       this.state.roomName ||
+                       this.state.roomContextUrls[0].description ||
+                       this.state.roomContextUrls[0].location;
 
-        if (roomName) {
-          this.setTitle(mozL10n.get("standalone_title_with_room_name", {
-            roomName: roomName,
-            clientShortname: mozL10n.get("clientShortname2")
-          }));
-        } else {
-          this.setTitle(mozL10n.get("clientShortname2"));
-        }
+        this.setTitle(mozL10n.get("standalone_title_with_room_name", {
+          roomName: roomName,
+          clientShortname: mozL10n.get("clientShortname2")
+        }));
       }
 
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
         this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
           publisherConfig: this.getDefaultPublisherConfig({ publishVideo: true })
         }));
       }
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -97,25 +97,28 @@ loop.standaloneRoomViews = (function(moz
 
     _renderFailureText: function() {
       return (
         <p className="failure">{mozL10n.get("rooms_already_joined")}</p>
       );
     },
 
     render: function() {
+      var roomName = this.state.roomName ||
+                     this.state.roomContextUrls[0].description ||
+                     this.state.roomContextUrls[0].location;
       // The extra scroller div here is for providing a scroll view for shorter
       // screens, as the common.css specifies overflow:hidden for the body which
       // we need in some places.
       return (
         <div className="handle-user-agent-view-scroller">
           <div className="handle-user-agent-view">
             <div className="info-panel">
               <p className="loop-logo-text" title={mozL10n.get("clientShortname2")}></p>
-              <p className="roomName">{this.state.roomName}</p>
+              <p className="roomName">{roomName}</p>
               <p className="loop-logo" />
               {
                 this.state.failureReason ?
                   this._renderFailureText() :
                   this._renderJoinButton()
               }
             </div>
             <ToSView dispatcher={this.props.dispatcher} />
@@ -439,26 +442,25 @@ loop.standaloneRoomViews = (function(moz
      * user media access.
      *
      * @param  {Object} nextProps (Unused)
      * @param  {Object} nextState Next state object.
      */
     componentWillUpdate: function(nextProps, nextState) {
       if (this.state.roomState !== ROOM_STATES.READY &&
           nextState.roomState === ROOM_STATES.READY) {
-        var roomName = nextState.roomName || this.state.roomName;
+        var roomName = nextState.roomName ||
+                       this.state.roomName ||
+                       this.state.roomContextUrls[0].description ||
+                       this.state.roomContextUrls[0].location;
 
-        if (roomName) {
-          this.setTitle(mozL10n.get("standalone_title_with_room_name", {
-            roomName: roomName,
-            clientShortname: mozL10n.get("clientShortname2")
-          }));
-        } else {
-          this.setTitle(mozL10n.get("clientShortname2"));
-        }
+        this.setTitle(mozL10n.get("standalone_title_with_room_name", {
+          roomName: roomName,
+          clientShortname: mozL10n.get("clientShortname2")
+        }));
       }
 
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
         this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
           publisherConfig: this.getDefaultPublisherConfig({ publishVideo: true })
         }));
       }
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -738,16 +738,78 @@ describe("loop.panel", function() {
 
         roomEntry.setProps({ room: updatedRoom });
 
         expect(
           roomEntry.getDOMNode().textContent)
         .eql("New room name");
       });
     });
+
+    describe("Room name priority", function() {
+      var roomEntry;
+      beforeEach(function() {
+        roomEntry = mountRoomEntry({
+          dispatcher: dispatcher,
+          room: new loop.store.Room(roomData)
+        });
+      });
+
+      function setDecryptedContext(newDecryptedContext) {
+        return new loop.store.Room(_.extend({}, roomData, {
+          decryptedContext: newDecryptedContext,
+          ctime: new Date().getTime()
+        }));
+      }
+
+      it("should use room name by default", function() {
+        var updatedRoom = setDecryptedContext({
+          roomName: "Room name",
+          urls: [
+            {
+              description: "Website title",
+              location: "https://fakeurl.com"
+            }
+          ]
+        });
+
+        roomEntry.setProps({ room: updatedRoom });
+
+        expect(roomEntry.getDOMNode().textContent).eql("Room name");
+      });
+
+      it("should use context title when there's no room title", function() {
+        var updatedRoom = setDecryptedContext({
+          urls: [
+            {
+              description: "Website title",
+              location: "https://fakeurl.com"
+            }
+          ]
+        });
+
+        roomEntry.setProps({ room: updatedRoom });
+
+        expect(roomEntry.getDOMNode().textContent).eql("Website title");
+      });
+
+      it("should use website url when there's no room title nor website", function() {
+        var updatedRoom = setDecryptedContext({
+          urls: [
+            {
+              location: "https://fakeurl.com"
+            }
+          ]
+        });
+
+        roomEntry.setProps({ room: updatedRoom });
+
+        expect(roomEntry.getDOMNode().textContent).eql("https://fakeurl.com");
+      });
+    });
   });
 
   describe("loop.panel.RoomList", function() {
     var roomStore, dispatcher, fakeEmail, dispatch, roomData;
 
     beforeEach(function() {
       fakeEmail = "fakeEmail@example.com";
       dispatcher = new loop.Dispatcher();
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -644,16 +644,58 @@ describe("loop.roomViews", function() {
           videoMuted: false
         });
 
         view = mountTestComponent();
 
         expect(view.getDOMNode().querySelector(".local video")).not.eql(null);
       });
 
+      describe("Room name priority", function() {
+        var roomEntry;
+        beforeEach(function() {
+          activeRoomStore.setStoreState({
+            participants: [{}],
+            roomState: ROOM_STATES.JOINED,
+            roomName: "fakeName",
+            roomContextUrls: [
+              {
+                description: "Website title",
+                location: "https://fakeurl.com"
+              }
+            ]
+          });
+        });
+
+        it("should use room name by default", function() {
+          view = mountTestComponent();
+          expect(fakeWindow.document.title).to.equal("fakeName");
+        });
+
+        it("should use context title when there's no room title", function() {
+          activeRoomStore.setStoreState({ roomName: null });
+
+          view = mountTestComponent();
+          expect(fakeWindow.document.title).to.equal("Website title");
+        });
+
+        it("should use website url when there's no room title nor website", function() {
+          activeRoomStore.setStoreState({
+            roomName: null,
+            roomContextUrls: [
+                {
+                  location: "https://fakeurl.com"
+                }
+              ]
+          });
+          view = mountTestComponent();
+          expect(fakeWindow.document.title).to.equal("https://fakeurl.com");
+        });
+      });
+
     });
 
     describe("Edit Context", function() {
       it("should show the form when the edit button is clicked", function() {
         view = mountTestComponent();
         var node = view.getDOMNode();
 
         expect(node.querySelector(".room-context")).to.eql(null);
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -135,82 +135,141 @@ describe("loop.standaloneRoomViews", fun
         React.createElement(
           loop.standaloneRoomViews.StandaloneHandleUserAgentView, {
             dispatcher: dispatcher
           }));
     }
 
     it("should display a join room button if the state is not ROOM_JOINED", function() {
       activeRoomStore.setStoreState({
-        roomState: ROOM_STATES.READY
+        roomState: ROOM_STATES.READY,
+        roomName: "fakeName"
       });
 
       view = mountTestComponent();
       var button = view.getDOMNode().querySelector(".info-panel > button");
 
       expect(button.textContent).eql("rooms_room_join_label");
     });
 
     it("should dispatch a JoinRoom action when the join room button is clicked", function() {
       activeRoomStore.setStoreState({
-        roomState: ROOM_STATES.READY
+        roomState: ROOM_STATES.READY,
+        roomName: "fakeName"
       });
 
       view = mountTestComponent();
       var button = view.getDOMNode().querySelector(".info-panel > button");
 
       TestUtils.Simulate.click(button);
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.JoinRoom());
     });
 
     it("should display a enjoy your conversation button if the state is ROOM_JOINED", function() {
       activeRoomStore.setStoreState({
-        roomState: ROOM_STATES.JOINED
+        roomState: ROOM_STATES.JOINED,
+        roomName: "fakeName"
       });
 
       view = mountTestComponent();
       var button = view.getDOMNode().querySelector(".info-panel > button");
 
       expect(button.textContent).eql("rooms_room_joined_own_conversation_label");
     });
 
     it("should disable the enjoy your conversation button if the state is ROOM_JOINED", function() {
       activeRoomStore.setStoreState({
-        roomState: ROOM_STATES.JOINED
+        roomState: ROOM_STATES.JOINED,
+        roomName: "fakeName"
       });
 
       view = mountTestComponent();
       var button = view.getDOMNode().querySelector(".info-panel > button");
 
       expect(button.classList.contains("disabled")).eql(true);
     });
 
     it("should not display a join button if there is a failure reason", function() {
       activeRoomStore.setStoreState({
-        failureReason: FAILURE_DETAILS.ROOM_ALREADY_OPEN
+        failureReason: FAILURE_DETAILS.ROOM_ALREADY_OPEN,
+        roomName: "fakeName"
       });
 
       view = mountTestComponent();
       var button = view.getDOMNode().querySelector(".info-panel > button");
 
       expect(button).eql(null);
     });
 
     it("should display a room already joined message if opening failed", function() {
       activeRoomStore.setStoreState({
-        failureReason: FAILURE_DETAILS.ROOM_ALREADY_OPEN
+        failureReason: FAILURE_DETAILS.ROOM_ALREADY_OPEN,
+        roomName: "fakeName"
       });
 
       view = mountTestComponent();
       var text = view.getDOMNode().querySelector(".failure");
 
       expect(text.textContent).eql("rooms_already_joined");
     });
+
+    describe("Room name priority", function() {
+      it("should use room name", function() {
+        activeRoomStore.setStoreState({
+          roomState: ROOM_STATES.JOINED,
+          roomName: "fakeName"
+        });
+
+        view = mountTestComponent();
+        var text = view.getDOMNode().querySelector(".roomName");
+
+        expect(
+          text.textContent)
+        .eql("fakeName");
+      });
+
+      it("should use context title when there's no room title", function() {
+        activeRoomStore.setStoreState({
+          roomState: ROOM_STATES.JOINED,
+          roomContextUrls: [
+            {
+              description: "Website title",
+              location: "https://fakeurl.com"
+            }
+          ]
+        });
+
+        view = mountTestComponent();
+        var text = view.getDOMNode().querySelector(".roomName");
+
+        expect(
+          text.textContent)
+        .eql("Website title");
+      });
+
+      it("should use website url when there's no room title nor website", function() {
+        activeRoomStore.setStoreState({
+          roomState: ROOM_STATES.JOINED,
+          roomContextUrls: [
+            {
+              location: "https://fakeurl.com"
+            }
+          ]
+        });
+
+        view = mountTestComponent();
+        var text = view.getDOMNode().querySelector(".roomName");
+
+        expect(
+          text.textContent)
+        .eql("https://fakeurl.com");
+      });
+    });
   });
 
   describe("StandaloneRoomHeader", function() {
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneOverlayWrapper, {
             dispatcher: dispatcher
@@ -236,17 +295,20 @@ describe("loop.standaloneRoomViews", fun
         dispatcher: dispatcher
       }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomFailureView, props));
     }
 
     beforeEach(function() {
-      activeRoomStore.setStoreState({ roomState: ROOM_STATES.FAILED });
+      activeRoomStore.setStoreState({
+        roomState: ROOM_STATES.FAILED,
+        roomName: "fakeName"
+      });
     });
 
     it("should display a status error message if not reason is supplied", function() {
       view = mountTestComponent();
 
       expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
         .eql("status_error");
     });
@@ -360,32 +422,27 @@ describe("loop.standaloneRoomViews", fun
 
     function expectActionDispatched() {
       sinon.assert.calledOnce(dispatch);
       sinon.assert.calledWithExactly(dispatch,
         sinon.match.instanceOf(sharedActions.SetupStreamElements));
     }
 
     describe("#componentWillUpdate", function() {
+      beforeEach(function() {
+        activeRoomStore.setStoreState({ roomName: "fakeName" });
+      });
       it("should set document.title to roomName and brand name when the READY state is dispatched", function() {
-        activeRoomStore.setStoreState({ roomName: "fakeName", roomState: ROOM_STATES.INIT });
+        activeRoomStore.setStoreState({ roomState: ROOM_STATES.INIT });
         view = mountTestComponent();
         activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
 
         expect(fakeWindow.document.title).to.equal("fakeName — clientShortname2");
       });
 
-      it("should set document.title brand name when there is no context available", function() {
-        activeRoomStore.setStoreState({ roomState: ROOM_STATES.INIT });
-        view = mountTestComponent();
-        activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
-
-        expect(fakeWindow.document.title).to.equal("clientShortname2");
-      });
-
       it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
         "is entered", function() {
           activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
           view = mountTestComponent();
 
           activeRoomStore.setStoreState({ roomState: ROOM_STATES.MEDIA_WAIT });
 
           expectActionDispatched(view);
@@ -451,17 +508,17 @@ describe("loop.standaloneRoomViews", fun
         });
     });
 
     describe("#componentWillReceiveProps", function() {
       beforeEach(function() {
         view = mountTestComponent();
 
         // Pretend the user waited a little bit
-        activeRoomStore.setStoreState({ roomState: ROOM_STATES.JOINING });
+        activeRoomStore.setStoreState({ roomState: ROOM_STATES.JOINING, roomName: "fakeName" });
         clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY - 1);
       });
 
       describe("Support multiple joins", function() {
         it("should send the first `TileShown` after waiting in JOINING state",
           function() {
             clock.tick(1);
 
@@ -536,17 +593,17 @@ describe("loop.standaloneRoomViews", fun
           enabled: true
         }));
       });
     });
 
     describe("#render", function() {
       beforeEach(function() {
         view = mountTestComponent();
-        activeRoomStore.setStoreState({ roomState: ROOM_STATES.JOINING });
+        activeRoomStore.setStoreState({ roomState: ROOM_STATES.JOINING, roomName: "fakeName" });
       });
 
       describe("Empty room message", function() {
         it("should not display an message immediately in the JOINED state",
           function() {
             activeRoomStore.setStoreState({ roomState: ROOM_STATES.JOINED });
 
             expect(view.getDOMNode().querySelector(".empty-room-message"))
@@ -971,16 +1028,20 @@ describe("loop.standaloneRoomViews", fun
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomControllerView, {
         dispatcher: dispatcher,
         isFirefox: true
       }));
     }
 
+    beforeEach(function() {
+      activeRoomStore.setStoreState({ roomName: "fakeName" });
+    });
+
     it("should not display anything if it is not known if Firefox can handle the room", function() {
       activeRoomStore.setStoreState({
         userAgentHandlesRoom: undefined
       });
 
       view = mountTestComponent();
 
       expect(view.getDOMNode()).eql(null);
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -1327,18 +1327,73 @@
             )
           )
 
         )
       );
     }
   });
 
+  var Failure = React.createClass({displayName: "Failure",
+    propTypes: {
+      errorDetected: React.PropTypes.bool.isRequired,
+      errorLine1: React.PropTypes.string,
+      errorLine2: React.PropTypes.string,
+      summary: React.PropTypes.string.isRequired
+    },
+
+    render: function() {
+      // if no errors, return blank
+      return !this.props.errorDetected ? null :
+      (React.createElement("li", {className: "test fail"}, 
+          React.createElement("h2", null, 
+            this.props.summary
+          ), 
+          React.createElement("pre", {className: "error"}, 
+            this.props.errorLine1 +
+             this.props.errorLine2 ? "\n" + this.props.errorLine2 : ""
+          )
+        )
+      );
+    }
+  });
+
+  var Result = React.createClass({displayName: "Result",
+    propTypes: {
+      error: React.PropTypes.object,
+      warnings: React.PropTypes.array
+    },
+
+    render: function() {
+      var warningsDetected = this.props.warnings.length !== 0;
+      var totalFailures = warningsDetected + !!this.props.error;
+
+      return (
+        React.createElement("div", {className: "error-summary"}, 
+          React.createElement("div", {className: "failures"}, 
+            React.createElement("a", null, "failures: "), 
+            React.createElement("em", null, totalFailures)
+          ), 
+          React.createElement("ul", null, 
+            React.createElement(Failure, {errorDetected: warningsDetected, 
+                     errorLine1: "Got: " + this.props.warnings.length, 
+                     summary: "Unexpected warnings detected rendering UI-Showcase"}), 
+            React.createElement(Failure, {errorDetected: !!this.props.error, 
+                     errorLine1: this.props.error, 
+                     errorLine2: this.props.error ? this.props.error.stack : null, 
+                     summary: "Errors rendering UI-Showcase"})
+          ), 
+          React.createElement("p", {id: "complete"}, "Completed")
+        )
+      );
+    }
+  });
+
   window.addEventListener("DOMContentLoaded", function() {
-    var uncaughtError;
+    var uncaughtError = null;
     var consoleWarn = console.warn;
     var caughtWarnings = [];
     console.warn = function() {
       var args = Array.slice(arguments);
       caughtWarnings.push(args);
       consoleWarn.apply(console, args);
     };
 
@@ -1357,60 +1412,15 @@
     setTimeout(function waitForQueuedFrames() {
       if (window.queuedFrames.length !== 0) {
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
-      // This simulates the mocha layout for errors which means we can run
-      // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 0;
-      var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
-      var resultsElement = document.querySelector("#results");
-      var divFailuresNode = document.createElement("div");
-      var pCompleteNode = document.createElement("p");
-      var emNode = document.createElement("em");
-
-      if (uncaughtError || warningsMismatch) {
-        var liTestFail = document.createElement("li");
-        var h2Node = document.createElement("h2");
-        var preErrorNode = document.createElement("pre");
-
-        divFailuresNode.className = "failures";
-        emNode.innerHTML = ((uncaughtError && warningsMismatch) ? 2 : 1).toString();
-        divFailuresNode.appendChild(emNode);
-        resultsElement.appendChild(divFailuresNode);
-
-        if (warningsMismatch) {
-          liTestFail.className = "test";
-          liTestFail.className += " fail";
-          h2Node.innerHTML = "Unexpected number of warnings detected in UI-Showcase";
-          preErrorNode.className = "error";
-          preErrorNode.innerHTML = "Got: " + caughtWarnings.length + "\n" + "Expected: " + expectedWarningsCount;
-          liTestFail.appendChild(h2Node);
-          liTestFail.appendChild(preErrorNode);
-          resultsElement.appendChild(liTestFail);
-        }
-        if (uncaughtError) {
-          liTestFail.className = "test";
-          liTestFail.className += " fail";
-          h2Node.innerHTML = "Errors rendering UI-Showcase";
-          preErrorNode.className = "error";
-          preErrorNode.innerHTML = uncaughtError + "\n" + uncaughtError.stack;
-          liTestFail.appendChild(h2Node);
-          liTestFail.appendChild(preErrorNode);
-          resultsElement.appendChild(liTestFail);
-        }
-      } else {
-        divFailuresNode.className = "failures";
-        emNode.innerHTML = "0";
-        divFailuresNode.appendChild(emNode);
-        resultsElement.appendChild(divFailuresNode);
-      }
-      pCompleteNode.id = "complete";
-      pCompleteNode.innerHTML = "Completed";
-      resultsElement.appendChild(pCompleteNode);
+      React.render(React.createElement(Result, {error: uncaughtError, 
+                           warnings: caughtWarnings}),
+                   document.querySelector("#results"));
     }, 1000);
   });
 
 })();
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -1327,18 +1327,73 @@
             </FramedExample>
           </Section>
 
         </ShowCase>
       );
     }
   });
 
+  var Failure = React.createClass({
+    propTypes: {
+      errorDetected: React.PropTypes.bool.isRequired,
+      errorLine1: React.PropTypes.string,
+      errorLine2: React.PropTypes.string,
+      summary: React.PropTypes.string.isRequired
+    },
+
+    render: function() {
+      // if no errors, return blank
+      return !this.props.errorDetected ? null :
+      (<li className = "test fail">
+          <h2>
+            {this.props.summary}
+          </h2>
+          <pre className="error">
+            {this.props.errorLine1 +
+             this.props.errorLine2 ? "\n" + this.props.errorLine2 : ""}
+          </pre>
+        </li>
+      );
+    }
+  });
+
+  var Result = React.createClass({
+    propTypes: {
+      error: React.PropTypes.object,
+      warnings: React.PropTypes.array
+    },
+
+    render: function() {
+      var warningsDetected = this.props.warnings.length !== 0;
+      var totalFailures = warningsDetected + !!this.props.error;
+
+      return (
+        <div className = "error-summary">
+          <div className = "failures">
+            <a>failures: </a>
+            <em>{totalFailures}</em>
+          </div>
+          <ul>
+            <Failure errorDetected={warningsDetected}
+                     errorLine1={"Got: " + this.props.warnings.length}
+                     summary="Unexpected warnings detected rendering UI-Showcase" />
+            <Failure errorDetected={!!this.props.error}
+                     errorLine1={this.props.error}
+                     errorLine2={this.props.error ? this.props.error.stack : null}
+                     summary="Errors rendering UI-Showcase" />
+          </ul>
+          <p id="complete">Completed</p>
+        </div>
+      );
+    }
+  });
+
   window.addEventListener("DOMContentLoaded", function() {
-    var uncaughtError;
+    var uncaughtError = null;
     var consoleWarn = console.warn;
     var caughtWarnings = [];
     console.warn = function() {
       var args = Array.slice(arguments);
       caughtWarnings.push(args);
       consoleWarn.apply(console, args);
     };
 
@@ -1357,60 +1412,15 @@
     setTimeout(function waitForQueuedFrames() {
       if (window.queuedFrames.length !== 0) {
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
-      // This simulates the mocha layout for errors which means we can run
-      // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 0;
-      var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
-      var resultsElement = document.querySelector("#results");
-      var divFailuresNode = document.createElement("div");
-      var pCompleteNode = document.createElement("p");
-      var emNode = document.createElement("em");
-
-      if (uncaughtError || warningsMismatch) {
-        var liTestFail = document.createElement("li");
-        var h2Node = document.createElement("h2");
-        var preErrorNode = document.createElement("pre");
-
-        divFailuresNode.className = "failures";
-        emNode.innerHTML = ((uncaughtError && warningsMismatch) ? 2 : 1).toString();
-        divFailuresNode.appendChild(emNode);
-        resultsElement.appendChild(divFailuresNode);
-
-        if (warningsMismatch) {
-          liTestFail.className = "test";
-          liTestFail.className += " fail";
-          h2Node.innerHTML = "Unexpected number of warnings detected in UI-Showcase";
-          preErrorNode.className = "error";
-          preErrorNode.innerHTML = "Got: " + caughtWarnings.length + "\n" + "Expected: " + expectedWarningsCount;
-          liTestFail.appendChild(h2Node);
-          liTestFail.appendChild(preErrorNode);
-          resultsElement.appendChild(liTestFail);
-        }
-        if (uncaughtError) {
-          liTestFail.className = "test";
-          liTestFail.className += " fail";
-          h2Node.innerHTML = "Errors rendering UI-Showcase";
-          preErrorNode.className = "error";
-          preErrorNode.innerHTML = uncaughtError + "\n" + uncaughtError.stack;
-          liTestFail.appendChild(h2Node);
-          liTestFail.appendChild(preErrorNode);
-          resultsElement.appendChild(liTestFail);
-        }
-      } else {
-        divFailuresNode.className = "failures";
-        emNode.innerHTML = "0";
-        divFailuresNode.appendChild(emNode);
-        resultsElement.appendChild(divFailuresNode);
-      }
-      pCompleteNode.id = "complete";
-      pCompleteNode.innerHTML = "Completed";
-      resultsElement.appendChild(pCompleteNode);
+      React.render(<Result error={uncaughtError}
+                           warnings={caughtWarnings} />,
+                   document.querySelector("#results"));
     }, 1000);
   });
 
 })();
--- a/browser/modules/ContentSearch.jsm
+++ b/browser/modules/ContentSearch.jsm
@@ -51,16 +51,18 @@ const MAX_SUGGESTIONS = 6;
  *   ManageEngines
  *     Opens the search engine management window.
  *     data: null
  *   RemoveFormHistoryEntry
  *     Removes an entry from the search form history.
  *     data: the entry, a string
  *   Search
  *     Performs a search.
+ *     Any GetSuggestions messages in the queue from the same target will be
+ *     cancelled.
  *     data: { engineName, searchString, healthReportKey, searchPurpose }
  *   SetCurrentEngine
  *     Sets the current engine.
  *     data: the name of the engine
  *   SpeculativeConnect
  *     Speculatively connects to an engine.
  *     data: the name of the engine
  *
@@ -76,16 +78,20 @@ const MAX_SUGGESTIONS = 6;
  *     Sent in reply to GetState.
  *     data: see _currentStateObj
  *   Strings
  *     Sent in reply to GetStrings
  *     data: Object containing string names and values for the current locale.
  *   Suggestions
  *     Sent in reply to GetSuggestions.
  *     data: see _onMessageGetSuggestions
+ *   SuggestionsCancelled
+ *     Sent in reply to GetSuggestions when pending GetSuggestions events are
+ *     cancelled.
+ *     data: null
  */
 
 this.ContentSearch = {
 
   // Inbound events are queued and processed in FIFO order instead of handling
   // them immediately, which would result in non-FIFO responses due to the
   // asynchrononicity added by converting image data URIs to ArrayBuffers.
   _eventQueue: [],
@@ -93,16 +99,20 @@ this.ContentSearch = {
 
   // This is used to handle search suggestions.  It maps xul:browsers to objects
   // { controller, previousFormHistoryResult }.  See _onMessageGetSuggestions.
   _suggestionMap: new WeakMap(),
 
   // Resolved when we finish shutting down.
   _destroyedPromise: null,
 
+  // The current controller and browser in _onMessageGetSuggestions.  Allows
+  // fetch cancellation from _cancelSuggestions.
+  _currentSuggestion: null,
+
   init: function () {
     Cc["@mozilla.org/globalmessagemanager;1"].
       getService(Ci.nsIMessageListenerManager).
       addMessageListener(INBOUND_MESSAGE, this);
     Services.obs.addObserver(this, "browser-search-engine-modified", false);
     Services.obs.addObserver(this, "shutdown-leaks-before-check", false);
     Services.prefs.addObserver("browser.search.hiddenOneOffs", this, false);
     this._stringBundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties");
@@ -160,16 +170,22 @@ this.ContentSearch = {
         this._suggestionMap.set(event.detail, browserData);
       }
       msg.target.removeEventListener("SwapDocShells", msg, true);
       msg.target = event.detail;
       msg.target.addEventListener("SwapDocShells", msg, true);
     };
     msg.target.addEventListener("SwapDocShells", msg, true);
 
+    // Search requests cause cancellation of all Suggestion requests from the
+    // same browser.
+    if (msg.data.type == "Search") {
+      this._cancelSuggestions(msg);
+    }
+
     this._eventQueue.push({
       type: "Message",
       data: msg,
     });
     this._processEventQueue();
   },
 
   observe: function (subj, topic, data) {
@@ -203,16 +219,37 @@ this.ContentSearch = {
         Cu.reportError(err);
       } finally {
         this._currentEventPromise = null;
         this._processEventQueue();
       }
     }.bind(this));
   },
 
+  _cancelSuggestions: function (msg) {
+    let cancelled = false;
+    // cancel active suggestion request
+    if (this._currentSuggestion && this._currentSuggestion.target == msg.target) {
+      this._currentSuggestion.controller.stop();
+      cancelled = true;
+    }
+    // cancel queued suggestion requests
+    for (let i = 0; i < this._eventQueue.length; i++) {
+      let m = this._eventQueue[i].data;
+      if (msg.target == m.target && m.data.type == "GetSuggestions") {
+        this._eventQueue.splice(i, 1);
+        cancelled = true;
+        i--;
+      }
+    }
+    if (cancelled) {
+      this._reply(msg, "SuggestionsCancelled");
+    }
+  },
+
   _onMessage: Task.async(function* (msg) {
     let methodName = "_onMessage" + msg.data.type;
     if (methodName in this) {
       yield this._initService();
       yield this[methodName](msg, msg.data.data);
       msg.target.removeEventListener("SwapDocShells", msg, true);
     }
   }),
@@ -297,17 +334,24 @@ this.ContentSearch = {
     let { controller } = browserData;
     let ok = SearchSuggestionController.engineOffersSuggestions(engine);
     controller.maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS;
     controller.maxRemoteResults = ok ? MAX_SUGGESTIONS : 0;
     controller.remoteTimeout = data.remoteTimeout || undefined;
     let priv = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
     // fetch() rejects its promise if there's a pending request, but since we
     // process our event queue serially, there's never a pending request.
+    this._currentSuggestion = { controller: controller, target: msg.target };
     let suggestions = yield controller.fetch(data.searchString, priv, engine);
+    this._currentSuggestion = null;
+
+    // suggestions will be null if the request was cancelled
+    if (!suggestions) {
+      return;
+    }
 
     // Keep the form history result so RemoveFormHistoryEntry can remove entries
     // from it.  Keeping only one result isn't foolproof because the client may
     // try to remove an entry from one set of suggestions after it has requested
     // more but before it's received them.  In that case, the entry may not
     // appear in the new suggestions.  But that should happen rarely.
     browserData.previousFormHistoryResult = suggestions.formHistoryResult;
 
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1942,11 +1942,12 @@ toolbarbutton.chevron > .toolbarbutton-i
   visibility: visible;
   /* override toolkit/themes/linux/global/menu.css */
   -moz-padding-end: 0 !important;
   -moz-margin-end: 0 !important;
 }
 
 .browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
+  overflow: hidden;
 }
 
 %include ../shared/usercontext/usercontext.inc.css
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3616,11 +3616,12 @@ notification[value="loop-sharing-notific
 
 #context-navigation > .menuitem-iconic {
   padding-left: 0;
   padding-right: 0;
 }
 
 .browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
+  overflow: hidden;
 }
 
 %include ../shared/usercontext/usercontext.inc.css
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2802,11 +2802,12 @@ notification[value="loop-sharing-notific
 
 
 @media not all and (-moz-os-version: windows-xp) {
 %include browser-aero.css
 }
 
 .browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
+  overflow: hidden;
 }
 
 %include ../shared/usercontext/usercontext.inc.css
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -332,16 +332,29 @@ if test -n "$MOZ_NATIVE_DEVICES" ; then
     MOZ_ANDROID_AAR(play-services-base, 8.1.0, google, com/google/android/gms)
     MOZ_ANDROID_AAR(play-services-basement, 8.1.0, google, com/google/android/gms)
     MOZ_ANDROID_AAR(play-services-cast, 8.1.0, google, com/google/android/gms)
     MOZ_ANDROID_AAR(mediarouter-v7, 23.0.1, android, com/android/support, REQUIRED_INTERNAL_IMPL)
 fi
 
 ])
 
+AC_DEFUN([MOZ_ANDROID_GOOGLE_CLOUD_MESSAGING],
+[
+
+if test -n "$MOZ_ANDROID_GCM" ; then
+    AC_SUBST(MOZ_ANDROID_GCM)
+
+    MOZ_ANDROID_AAR(play-services-base, 8.1.0, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-basement, 8.1.0, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-gcm, 8.1.0, google, com/google/android/gms)
+fi
+
+])
+
 dnl Configure an Android SDK.
 dnl Arg 1: target SDK version, like 22.
 dnl Arg 2: build tools version, like 22.0.1.
 AC_DEFUN([MOZ_ANDROID_SDK],
 [
 
 MOZ_ARG_WITH_STRING(android-sdk,
 [  --with-android-sdk=DIR
--- a/build/dumbmake-dependencies
+++ b/build/dumbmake-dependencies
@@ -43,28 +43,29 @@ toolkit/library
   image/build
     image
   intl/build
     intl
   media
   profile
   services
   startupcache
+  devtools/server
+  devtools/shared
 browser/app
   browser/base
   browser/components
   devtools/client
   browser/locales
   browser/modules
   browser/themes
   toolkit
   toolkit/components
   toolkit/components/jsdownloads
   toolkit/content
   toolkit/crashreporter
-  devtools/shared
   toolkit/forgetaboutsite
   toolkit/identity
   toolkit/modules
   toolkit/mozapps/extensions
   toolkit/profile
   toolkit/themes
   toolkit/webapps
--- a/configure.in
+++ b/configure.in
@@ -3750,16 +3750,17 @@ BUILD_CTYPES=1
 MOZ_USE_NATIVE_POPUP_WINDOWS=
 MOZ_ANDROID_HISTORY=
 MOZ_WEBSMS_BACKEND=
 MOZ_ANDROID_BEAM=
 MOZ_LOCALE_SWITCHER=
 MOZ_ANDROID_READING_LIST_SERVICE=
 MOZ_ANDROID_SEARCH_ACTIVITY=
 MOZ_ANDROID_DOWNLOADS_INTEGRATION=
+MOZ_ANDROID_GCM=
 MOZ_ANDROID_MLS_STUMBLER=
 MOZ_ANDROID_SHARE_OVERLAY=
 MOZ_EXCLUDE_HYPHENATION_DICTIONARIES=
 MOZ_INSTALL_TRACKING=
 MOZ_SWITCHBOARD=
 ACCESSIBILITY=1
 MOZ_TIME_MANAGER=
 MOZ_SIMPLEPUSH=
@@ -3906,16 +3907,24 @@ AC_SUBST(MOZ_BING_API_KEY)
 
 # Allow specifying an Adjust SDK key file that contains the app token used for
 # Adjust SDK requests.
 MOZ_ARG_WITH_STRING(adjust-sdk-keyfile,
 [  --with-adjust-sdk-keyfile=file   Use the app token contained in the given keyfile for Adjust SDK requests],
   MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN=`cat $withval`)
 AC_SUBST(MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN)
 
+# Allow specifying a GCM sender ID key file that contains the sender ID used for
+# GCM requests.  Note that GCM sender IDs are not sensitive: see, for example,
+# http://stackoverflow.com/a/18216063.
+MOZ_ARG_WITH_STRING(gcm-senderid-keyfile,
+[  --with-gcm-senderid-keyfile=file GCM sender ID for GCM requests],
+  MOZ_ANDROID_GCM_SENDERID=`cat $withval`)
+AC_SUBST(MOZ_ANDROID_GCM_SENDERID)
+
 # Whether to include optional-but-large font files in the final APK.
 # We want this in mobile/android/confvars.sh, so it goes early.
 MOZ_ARG_DISABLE_BOOL(android-include-fonts,
 [  --disable-android-include-fonts
                           Disable the inclusion of fonts into the final APK],
     MOZ_ANDROID_EXCLUDE_FONTS=1)
 
 if test -n "$MOZ_ANDROID_EXCLUDE_FONTS"; then
@@ -4575,16 +4584,17 @@ AC_SUBST(MOZ_DISTRIBUTION_ID)
 
 
 dnl ========================================================
 dnl Google Play Services, placed here so it can depend on
 dnl values set by configure.sh above.
 dnl ========================================================
 
 MOZ_ANDROID_GOOGLE_PLAY_SERVICES
+MOZ_ANDROID_GOOGLE_CLOUD_MESSAGING
 
 
 dnl ========================================================
 dnl = Pango
 dnl ========================================================
 if test "$MOZ_ENABLE_GTK" -o "$MOZ_ENABLE_QT"
 then
     PKG_CHECK_MODULES(_PANGOCHK, pango >= $PANGO_VERSION)
@@ -4882,16 +4892,23 @@ fi
 dnl ========================================================
 dnl = Include Switchboard A/B framework on Android
 dnl ========================================================
 if test -n "$MOZ_SWITCHBOARD"; then
     AC_DEFINE(MOZ_SWITCHBOARD)
 fi
 
 dnl ========================================================
+dnl = Enable GCM on Android.
+dnl ========================================================
+if test -n "$MOZ_ANDROID_GCM"; then
+    AC_DEFINE(MOZ_ANDROID_GCM)
+fi
+
+dnl ========================================================
 dnl = Enable IPDL's "expensive" unit tests
 dnl ========================================================
 MOZ_IPDL_TESTS=
 
 MOZ_ARG_ENABLE_BOOL(ipdl-tests,
 [  --enable-ipdl-tests     Enable expensive IPDL tests],
     MOZ_IPDL_TESTS=1,
     MOZ_IPDL_TESTS=)
@@ -8587,16 +8604,17 @@ AC_SUBST(MOZ_DIRECTX_SDK_PATH)
 AC_SUBST(MOZ_D3DCOMPILER_XP_DLL)
 AC_SUBST(MOZ_D3DCOMPILER_XP_CAB)
 
 AC_SUBST(MOZ_ANDROID_HISTORY)
 AC_SUBST(MOZ_WEBSMS_BACKEND)
 AC_SUBST(MOZ_ANDROID_BEAM)
 AC_SUBST(MOZ_LOCALE_SWITCHER)
 AC_SUBST(MOZ_DISABLE_GECKOVIEW)
+AC_SUBST(MOZ_ANDROID_GCM)
 AC_SUBST(MOZ_ANDROID_GECKOLIBS_AAR)
 AC_SUBST(MOZ_ANDROID_READING_LIST_SERVICE)
 AC_SUBST(MOZ_ANDROID_SEARCH_ACTIVITY)
 AC_SUBST(MOZ_ANDROID_SHARE_OVERLAY)
 AC_SUBST(MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES)
 AC_SUBST(MOZ_ANDROID_TAB_QUEUE)
 AC_SUBST(MOZ_ANDROID_MLS_STUMBLER)
 AC_SUBST(MOZ_ANDROID_DOWNLOADS_INTEGRATION)
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-01.js
@@ -20,18 +20,25 @@ function test() {
     gFiltering = gDebugger.DebuggerView.Filtering;
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
 
     waitForSourceShown(gPanel, ".html").then(performTest);
   });
 }
 
 function performTest() {
+  // Make sure that the search box becomes focused when pressing ctrl+f - Bug 1211038
+  gEditor.focus();
+  synthesizeKeyFromKeyTag(gDebugger.document.getElementById("tokenSearchKey"));
+  let focusedEl = Services.focus.focusedElement;
+  focusedEl = focusedEl.ownerDocument.getBindingParent(focusedEl) || focusedEl;
+  is(focusedEl, gDebugger.document.getElementById("searchbox"), "Searchbox is focused");
+
   setText(gSearchBox, "#html");
-  
+
   EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger);
   is(gFiltering.searchData.toSource(), '["#", ["", "html"]]',
     "The searchbox data wasn't parsed correctly.");
   ok(isCaretPos(gPanel, 35, 7),
     "The editor didn't jump to the correct line.");
 
   EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger);
   is(gFiltering.searchData.toSource(), '["#", ["", "html"]]',
@@ -39,17 +46,17 @@ function performTest() {
   ok(isCaretPos(gPanel, 5, 6),
     "The editor didn't jump to the correct line.");
 
   EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger);
   is(gFiltering.searchData.toSource(), '["#", ["", "html"]]',
     "The searchbox data wasn't parsed correctly.");
   ok(isCaretPos(gPanel, 3, 15),
     "The editor didn't jump to the correct line.");
-  
+
   setText(gSearchBox, ":12");
   is(gFiltering.searchData.toSource(), '[":", ["", 12]]',
     "The searchbox data wasn't parsed correctly.");
   ok(isCaretPos(gPanel, 12),
     "The editor didn't jump to the correct line.");
 
   EventUtils.synthesizeKey("g", { metaKey: true }, gDebugger);
   is(gFiltering.searchData.toSource(), '[":", ["", 13]]',
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -1210,8 +1210,35 @@ function getSplitConsole(toolbox, win) {
   return new Promise(resolve => {
     toolbox.getPanelWhenReady("webconsole").then(() => {
       ok(toolbox.splitConsole, "Split console is shown.");
       let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
       resolve(jsterm);
     });
   });
 }
+
+// This can be removed once debugger uses shared-head.js (bug 1181838)
+function synthesizeKeyFromKeyTag(key) {
+  is(key && key.tagName, "key", "Successfully retrieved the <key> node");
+
+  let modifiersAttr = key.getAttribute("modifiers");
+
+  let name = null;
+
+  if (key.getAttribute("keycode"))
+    name = key.getAttribute("keycode");
+  else if (key.getAttribute("key"))
+    name = key.getAttribute("key");
+
+  isnot(name, null, "Successfully retrieved keycode/key");
+
+  let modifiers = {
+    shiftKey: !!modifiersAttr.match("shift"),
+    ctrlKey: !!modifiersAttr.match("control"),
+    altKey: !!modifiersAttr.match("alt"),
+    metaKey: !!modifiersAttr.match("meta"),
+    accelKey: !!modifiersAttr.match("accel")
+  };
+
+  info("Synthesizing key " + name + " " + JSON.stringify(modifiers));
+  EventUtils.synthesizeKey(name, modifiers);
+}
--- a/devtools/client/memory/actions/breakdown.js
+++ b/devtools/client/memory/actions/breakdown.js
@@ -1,43 +1,33 @@
 /* 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";
 
-// @TODO 1215606
-// Use this assert instead of utils when fixed.
-// const { assert } = require("devtools/shared/DevToolsUtils");
-const { breakdownEquals, createSnapshot, assert } = require("../utils");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { breakdownEquals, createSnapshot } = require("../utils");
 const { actions, snapshotState: states } = require("../constants");
-const { takeCensus } = require("./snapshot");
+const { refreshSelectedCensus } = require("./snapshot");
 
 const setBreakdownAndRefresh = exports.setBreakdownAndRefresh = function (heapWorker, breakdown) {
   return function *(dispatch, getState) {
-    // Clears out all stored census data and sets
-    // the breakdown
+    // Clears out all stored census data and sets the breakdown.
     dispatch(setBreakdown(breakdown));
-    let snapshot = getState().snapshots.find(s => s.selected);
-
-    // If selected snapshot does not have updated census if the breakdown
-    // changed, retake the census with new breakdown
-    if (snapshot && !breakdownEquals(snapshot.breakdown, breakdown)) {
-      yield dispatch(takeCensus(heapWorker, snapshot));
-    }
+    yield dispatch(refreshSelectedCensus(heapWorker));
   };
 };
 
 /**
  * Clears out all census data in the snapshots and sets
  * a new breakdown.
  *
  * @param {Breakdown} breakdown
  */
 const setBreakdown = exports.setBreakdown = function (breakdown) {
-  // @TODO 1215606
   assert(typeof breakdown === "object" && breakdown.by,
     `Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(breakdown)}`);
 
   return {
     type: actions.SET_BREAKDOWN,
     breakdown,
   }
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/inverted.js
@@ -0,0 +1,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 { actions } = require("../constants");
+const { refreshSelectedCensus } = require("./snapshot");
+
+const toggleInverted = exports.toggleInverted = function () {
+  return { type: actions.TOGGLE_INVERTED };
+};
+
+exports.toggleInvertedAndRefresh = function (heapWorker) {
+  return function* (dispatch, getState) {
+    dispatch(toggleInverted());
+    yield dispatch(refreshSelectedCensus(heapWorker));
+  };
+};
--- a/devtools/client/memory/actions/moz.build
+++ b/devtools/client/memory/actions/moz.build
@@ -1,10 +1,11 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'allocations.js',
     'breakdown.js',
+    'inverted.js',
     'snapshot.js',
 )
--- a/devtools/client/memory/actions/snapshot.js
+++ b/devtools/client/memory/actions/snapshot.js
@@ -1,119 +1,163 @@
 /* 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";
 
-// @TODO 1215606
-// Use this assert instead of utils when fixed.
-// const { assert } = require("devtools/shared/DevToolsUtils");
-const { getSnapshot, breakdownEquals, createSnapshot, assert } = require("../utils");
+const { assert, reportException } = require("devtools/shared/DevToolsUtils");
+const { getSnapshot, breakdownEquals, createSnapshot } = require("../utils");
 const { actions, snapshotState: states } = require("../constants");
 
 /**
  * A series of actions are fired from this task to save, read and generate the initial
  * census from a snapshot.
  *
  * @param {MemoryFront}
  * @param {HeapAnalysesClient}
  * @param {Object}
  */
 const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function (front, heapWorker) {
   return function *(dispatch, getState) {
     let snapshot = yield dispatch(takeSnapshot(front));
+
     yield dispatch(readSnapshot(heapWorker, snapshot));
-    yield dispatch(takeCensus(heapWorker, snapshot));
+    if (snapshot.state === states.READ) {
+      yield dispatch(takeCensus(heapWorker, snapshot));
+    }
   };
 };
 
 /**
  * Selects a snapshot and if the snapshot's census is using a different
  * breakdown, take a new census.
  *
  * @param {HeapAnalysesClient}
  * @param {Snapshot}
  */
 const selectSnapshotAndRefresh = exports.selectSnapshotAndRefresh = function (heapWorker, snapshot) {
   return function *(dispatch, getState) {
     dispatch(selectSnapshot(snapshot));
-
-    // Attempt to take another census; if the snapshot already is using
-    // the correct breakdown, this will noop.
-    yield dispatch(takeCensus(heapWorker, snapshot));
+    yield dispatch(refreshSelectedCensus(heapWorker));
   };
 };
 
 /**
  * @param {MemoryFront}
  */
 const takeSnapshot = exports.takeSnapshot = function (front) {
   return function *(dispatch, getState) {
     let snapshot = createSnapshot();
     dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot });
     dispatch(selectSnapshot(snapshot));
 
-    let path = yield front.saveHeapSnapshot();
+    let path;
+    try {
+      path = yield front.saveHeapSnapshot();
+    } catch (error) {
+      reportException("takeSnapshot", error);
+      dispatch({ type: actions.SNAPSHOT_ERROR, snapshot, error });
+      return;
+    }
+
     dispatch({ type: actions.TAKE_SNAPSHOT_END, snapshot, path });
-
     return snapshot;
   };
 };
 
 /**
  * Reads a snapshot into memory; necessary to do before taking
  * a census on the snapshot. May only be called once per snapshot.
  *
  * @param {HeapAnalysesClient}
  * @param {Snapshot} snapshot,
  */
 const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, snapshot) {
   return function *(dispatch, getState) {
-    // @TODO 1215606
     assert(snapshot.state === states.SAVED,
-      "Should only read a snapshot once");
+      `Should only read a snapshot once. Found snapshot in state ${snapshot.state}`);
 
     dispatch({ type: actions.READ_SNAPSHOT_START, snapshot });
-    yield heapWorker.readHeapSnapshot(snapshot.path);
+    try {
+      yield heapWorker.readHeapSnapshot(snapshot.path);
+    } catch (error) {
+      reportException("readSnapshot", error);
+      dispatch({ type: actions.SNAPSHOT_ERROR, snapshot, error });
+      return;
+    }
+
     dispatch({ type: actions.READ_SNAPSHOT_END, snapshot });
   };
 };
 
 /**
  * @param {HeapAnalysesClient} heapWorker
  * @param {Snapshot} snapshot,
  *
  * @see {Snapshot} model defined in devtools/client/memory/models.js
  * @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
  * @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
  */
 const takeCensus = exports.takeCensus = function (heapWorker, snapshot) {
   return function *(dispatch, getState) {
-    // @TODO 1215606
     assert([states.READ, states.SAVED_CENSUS].includes(snapshot.state),
-      "Can only take census of snapshots in READ or SAVED_CENSUS state");
+      `Can only take census of snapshots in READ or SAVED_CENSUS state, found ${snapshot.state}`);
 
     let census;
+    let inverted = getState().inverted;
     let breakdown = getState().breakdown;
 
-    // If breakdown hasn't changed, don't do anything
-    if (breakdownEquals(breakdown, snapshot.breakdown)) {
+    // If breakdown and inversion haven't changed, don't do anything.
+    if (inverted === snapshot.inverted && breakdownEquals(breakdown, snapshot.breakdown)) {
       return;
     }
 
     // Keep taking a census if the breakdown changes during. Recheck
     // that the breakdown used for the census is the same as
     // the state's breakdown.
     do {
+      inverted = getState().inverted;
       breakdown = getState().breakdown;
-      dispatch({ type: actions.TAKE_CENSUS_START, snapshot, breakdown });
-      census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, { asTreeNode: true });
-    } while (!breakdownEquals(breakdown, getState().breakdown));
+      dispatch({ type: actions.TAKE_CENSUS_START, snapshot, inverted, breakdown });
+      let opts = inverted ? { asInvertedTreeNode: true } : { asTreeNode: true };
+
+      try {
+        census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, opts);
+      } catch(error) {
+        reportException("takeCensus", error);
+        dispatch({ type: actions.SNAPSHOT_ERROR, snapshot, error });
+        return;
+      }
+    }
+    while (inverted !== getState().inverted ||
+           !breakdownEquals(breakdown, getState().breakdown));
+
+    dispatch({ type: actions.TAKE_CENSUS_END, snapshot, breakdown, inverted, census });
+  };
+};
 
-    dispatch({ type: actions.TAKE_CENSUS_END, snapshot, breakdown, census });
+/**
+ * Refresh the selected snapshot's census data, if need be (for example,
+ * breakdown configuration changed).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) {
+  return function*(dispatch, getState) {
+    let snapshot = getState().snapshots.find(s => s.selected);
+
+    // Intermediate snapshot states will get handled by the task action that is
+    // orchestrating them. For example, if the snapshot's state is
+    // SAVING_CENSUS, then the takeCensus action will keep taking a census until
+    // the inverted property matches the inverted state. If the snapshot is
+    // still in the process of being saved or read, the takeSnapshotAndCensus
+    // task action will follow through and ensure that a census is taken.
+    if (snapshot && snapshot.state === states.SAVED_CENSUS) {
+      yield dispatch(takeCensus(heapWorker, snapshot));
+    }
   };
 };
 
 /**
  * @param {Snapshot}
  * @see {Snapshot} model defined in devtools/client/memory/models.js
  */
 const selectSnapshot = exports.selectSnapshot = function (snapshot) {
--- a/devtools/client/memory/app.js
+++ b/devtools/client/memory/app.js
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { toggleRecordingAllocationStacks } = require("./actions/allocations");
 const { setBreakdownAndRefresh } = require("./actions/breakdown");
+const { toggleInvertedAndRefresh } = require("./actions/inverted");
 const { selectSnapshotAndRefresh, takeSnapshotAndCensus } = require("./actions/snapshot");
 const { breakdownNameToSpec, getBreakdownDisplayData } = require("./utils");
 const Toolbar = createFactory(require("./components/toolbar"));
 const List = createFactory(require("./components/list"));
 const SnapshotListItem = createFactory(require("./components/snapshot-list-item"));
 const HeapView = createFactory(require("./components/heap"));
 const { app: appModel } = require("./models");
 
@@ -34,31 +35,35 @@ const App = createClass({
   render() {
     let {
       dispatch,
       snapshots,
       front,
       heapWorker,
       breakdown,
       allocations,
+      inverted
     } = this.props;
 
     let selectedSnapshot = snapshots.find(s => s.selected);
 
     return (
       dom.div({ id: "memory-tool" }, [
 
         Toolbar({
           breakdowns: getBreakdownDisplayData(),
           onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)),
           onBreakdownChange: breakdown =>
             dispatch(setBreakdownAndRefresh(heapWorker, breakdownNameToSpec(breakdown))),
           onToggleRecordAllocationStacks: () =>
             dispatch(toggleRecordingAllocationStacks(front)),
-          allocations
+          allocations,
+          inverted,
+          onToggleInverted: () =>
+            dispatch(toggleInvertedAndRefresh(heapWorker))
         }),
 
         dom.div({ id: "memory-tool-container" }, [
           List({
             itemComponent: SnapshotListItem,
             items: snapshots,
             onClick: snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot))
           }),
--- a/devtools/client/memory/components/heap.js
+++ b/devtools/client/memory/components/heap.js
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const { safeErrorString } = require("devtools/shared/DevToolsUtils");
 const Tree = createFactory(require("./tree"));
 const TreeItem = createFactory(require("./tree-item"));
-const { getSnapshotStatusText } = require("../utils");
+const { getSnapshotStatusTextFull } = require("../utils");
 const { snapshotState: states } = require("../constants");
 const { snapshot: snapshotModel } = require("../models");
 const TAKE_SNAPSHOT_TEXT = "Take snapshot";
 // If HEAP_TREE_ROW_HEIGHT changes, be sure to change `var(--heap-tree-row-height)`
 // in `devtools/client/themes/memory.css`
 const HEAP_TREE_ROW_HEIGHT = 14;
 
 /**
@@ -35,18 +36,16 @@ function createParentMap (node, aggregat
  *
  * @param {CensusTreeNode} census
  * @return {Object}
  */
 function createTreeProperties (census) {
   let map = createParentMap(census);
 
   return {
-    // getParent only used for focusing parents when child selected;
-    // handle this later?
     getParent: node => map(node.id),
     getChildren: node => node.children || [],
     renderItem: (item, depth, focused, arrow) => new TreeItem({ item, depth, focused, arrow }),
     getRoots: () => census.children,
     getKey: node => node.id,
     itemHeight: HEAP_TREE_ROW_HEIGHT,
   };
 }
@@ -62,45 +61,57 @@ const Heap = module.exports = createClas
 
   propTypes: {
     onSnapshotClick: PropTypes.func.isRequired,
     snapshot: snapshotModel,
   },
 
   render() {
     let { snapshot, onSnapshotClick } = this.props;
-    let pane;
     let census = snapshot ? snapshot.census : null;
     let state = snapshot ? snapshot.state : "initial";
+    let statusText = snapshot ? getSnapshotStatusTextFull(snapshot) : "";
+    let content;
 
     switch (state) {
       case "initial":
-        pane = dom.div({ className: "heap-view-panel", "data-state": "initial" },
-          dom.button({ className: "take-snapshot", onClick: onSnapshotClick }, TAKE_SNAPSHOT_TEXT)
-        );
+        content = dom.button({
+          className: "devtools-toolbarbutton take-snapshot",
+          onClick: onSnapshotClick,
+          // Want to use the [standalone] tag to leverage our styles,
+          // but React hates that evidently
+          "data-standalone": true,
+          "data-text-only": true,
+        }, TAKE_SNAPSHOT_TEXT)
+        break;
+      case states.ERROR:
+        content = [
+          dom.span({ className: "snapshot-status error" }, statusText),
+          dom.pre({}, safeErrorString(snapshot.error || new Error("blahblah"))),
+        ];
         break;
       case states.SAVING:
       case states.SAVED:
       case states.READING:
       case states.READ:
       case states.SAVING_CENSUS:
-        pane = dom.div({ className: "heap-view-panel", "data-state": state },
-          getSnapshotStatusText(snapshot));
+        content = dom.span({ className: "snapshot-status devtools-throbber" }, statusText)
         break;
       case states.SAVED_CENSUS:
-        pane = dom.div({ className: "heap-view-panel", "data-state": "loaded" },
+        content = [
           dom.div({ className: "header" },
             dom.span({ className: "heap-tree-item-bytes" }, "Bytes"),
             dom.span({ className: "heap-tree-item-count" }, "Count"),
             dom.span({ className: "heap-tree-item-total-bytes" }, "Total Bytes"),
             dom.span({ className: "heap-tree-item-total-count" }, "Total Count"),
             dom.span({ className: "heap-tree-item-name" }, "Name")
           ),
           Tree(createTreeProperties(snapshot.census))
-        );
+        ];
         break;
     }
+    let pane = dom.div({ className: "heap-view-panel", "data-state": state }, content);
 
     return (
       dom.div({ id: "heap-view", "data-state": state }, pane)
     )
   }
 });
--- a/devtools/client/memory/components/toolbar.js
+++ b/devtools/client/memory/components/toolbar.js
@@ -11,43 +11,57 @@ const Toolbar = module.exports = createC
   propTypes: {
     breakdowns: PropTypes.arrayOf(PropTypes.shape({
       name: PropTypes.string.isRequired,
       displayName: PropTypes.string.isRequired,
     })).isRequired,
     onTakeSnapshotClick: PropTypes.func.isRequired,
     onBreakdownChange: PropTypes.func.isRequired,
     onToggleRecordAllocationStacks: PropTypes.func.isRequired,
-    allocations: models.allocations
+    allocations: models.allocations,
+    onToggleInverted: PropTypes.func.isRequired,
+    inverted: PropTypes.bool.isRequired,
   },
 
   render() {
     let {
       onTakeSnapshotClick,
       onBreakdownChange,
       breakdowns,
       onToggleRecordAllocationStacks,
       allocations,
+      onToggleInverted,
+      inverted
     } = this.props;
 
     return (
       DOM.div({ className: "devtools-toolbar" }, [
         DOM.button({ className: `take-snapshot devtools-button`, onClick: onTakeSnapshotClick }),
 
         DOM.select({
           className: `select-breakdown`,
           onChange: e => onBreakdownChange(e.target.value),
         }, breakdowns.map(({ name, displayName }) => DOM.option({ value: name }, displayName))),
 
         DOM.label({}, [
           DOM.input({
             type: "checkbox",
+            checked: inverted,
+            onChange: onToggleInverted,
+          }),
+          // TODO bug 1214799
+          "Invert tree"
+        ]),
+
+        DOM.label({}, [
+          DOM.input({
+            type: "checkbox",
             checked: allocations.recording,
             disabled: allocations.togglingInProgress,
             onChange: onToggleRecordAllocationStacks,
           }),
           // TODO bug 1214799
           "Record allocation stacks"
         ])
-      ])
+])
     );
   }
 });
--- a/devtools/client/memory/components/tree-item.js
+++ b/devtools/client/memory/components/tree-item.js
@@ -1,29 +1,52 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
 const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
 const INDENT = 10;
+const MAX_SOURCE_LENGTH = 200;
+
 
 /**
  * An arrow that displays whether its node is expanded (▼) or collapsed
  * (▶). When its node has no children, it is hidden.
  */
 const TreeItem = module.exports = createClass({
   displayName: "tree-item",
 
   render() {
     let { item, depth, arrow, focused } = this.props;
 
-    return dom.div({ className: "heap-tree-item" },
+    return dom.div({ className: `heap-tree-item ${focused ? "focused" :""}` },
       dom.span({ className: "heap-tree-item-bytes" }, item.bytes),
       dom.span({ className: "heap-tree-item-count" }, item.count),
       dom.span({ className: "heap-tree-item-total-bytes" }, item.totalBytes),
       dom.span({ className: "heap-tree-item-total-count" }, item.totalCount),
       dom.span({ className: "heap-tree-item-name", style: { marginLeft: depth * INDENT }},
         arrow,
-        item.name
+        this.toLabel(item.name)
       )
     );
+  },
+
+  toLabel(name) {
+    return isSavedFrame(name)
+      ? this.savedFrameToLabel(name)
+      : String(name);
+  },
+
+  savedFrameToLabel(frame) {
+    return [
+      dom.span({ className: "heap-tree-item-function-display-name" },
+               frame.functionDisplayFrame || ""),
+      dom.span({ className: "heap-tree-item-at" }, "@"),
+      dom.span({ className: "heap-tree-item-source" }, frame.source.slice(0, MAX_SOURCE_LENGTH)),
+      dom.span({ className: "heap-tree-item-colon" }, ":"),
+      dom.span({ className: "heap-tree-item-line" }, frame.line),
+      dom.span({ className: "heap-tree-item-colon" }, ":"),
+      dom.span({ className: "heap-tree-item-column" }, frame.column)
+    ];
   }
 });
--- a/devtools/client/memory/constants.js
+++ b/devtools/client/memory/constants.js
@@ -21,16 +21,22 @@ actions.TAKE_CENSUS_END = "take-census-e
 
 // When requesting that the server start/stop recording allocation stacks.
 actions.TOGGLE_RECORD_ALLOCATION_STACKS_START = "toggle-record-allocation-stacks-start";
 actions.TOGGLE_RECORD_ALLOCATION_STACKS_END = "toggle-record-allocation-stacks-end";
 
 // Fired by UI to select a snapshot to view.
 actions.SELECT_SNAPSHOT = "select-snapshot";
 
+// Fired to toggle tree inversion on or off.
+actions.TOGGLE_INVERTED = "toggle-inverted";
+
+// Fired when there is an error processing a snapshot or taking a census.
+actions.SNAPSHOT_ERROR = "snapshot-error";
+
 // Options passed to MemoryFront's startRecordingAllocations never change.
 exports.ALLOCATION_RECORDING_OPTIONS = {
   probability: 1,
   maxLogLength: 1
 };
 
 const COUNT = { by: "count", count: true, bytes: true };
 const INTERNAL_TYPE = { by: "internalType", then: COUNT };
@@ -66,18 +72,22 @@ const breakdowns = exports.breakdowns = 
 };
 
 const snapshotState = exports.snapshotState = {};
 
 /**
  * Various states a snapshot can be in.
  * An FSM describing snapshot states:
  *
- * SAVING -> SAVED -> READING -> READ   <-  <-  <- SAVED_CENSUS
- *                                    ↘             ↗
- *                                     SAVING_CENSUS
+ *     SAVING -> SAVED -> READING -> READ   <-  <-  <- SAVED_CENSUS
+ *                                        ↘             ↗
+ *                                         SAVING_CENSUS
+ *
+ * Any of these states may go to the ERROR state, from which they can never
+ * leave (mwah ha ha ha!)
  */
+snapshotState.ERROR = "snapshot-state-error";
 snapshotState.SAVING = "snapshot-state-saving";
 snapshotState.SAVED = "snapshot-state-saved";
 snapshotState.READING = "snapshot-state-reading";
 snapshotState.READ = "snapshot-state-read";
 snapshotState.SAVING_CENSUS = "snapshot-state-saving-census";
 snapshotState.SAVED_CENSUS = "snapshot-state-saved-census";
--- a/devtools/client/memory/models.js
+++ b/devtools/client/memory/models.js
@@ -26,16 +26,20 @@ let snapshotModel = exports.snapshot = P
   selected: PropTypes.bool.isRequired,
   // fs path to where the snapshot is stored; used to
   // identify the snapshot for HeapAnalysesClient.
   path: PropTypes.string,
   // Data of a census breakdown
   census: PropTypes.object,
   // The breakdown used to generate the current census
   breakdown: breakdownModel,
+  // Whether the currently cached census tree is inverted or not.
+  inverted: PropTypes.bool,
+  // If an error was thrown while processing this snapshot, the `Error` instance is attached here.
+  error: PropTypes.object,
   // State the snapshot is in
   // @see ./constants.js
   state: function (props, propName) {
     let stateNames = Object.keys(states);
     let current = props.state;
     let shouldHavePath = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
     let shouldHaveCensus = [states.SAVED_CENSUS];
 
@@ -58,18 +62,20 @@ let allocationsModel = exports.allocatio
   // stacks on or off right now.
   togglingInProgress: PropTypes.bool.isRequired,
 });
 
 let appModel = exports.app = {
   // {MemoryFront} Used to communicate with platform
   front: PropTypes.instanceOf(MemoryFront),
   // Allocations recording related data.
-  allocations: allocationsModel,
+  allocations: allocationsModel.isRequired,
   // {HeapAnalysesClient} Used to interface with snapshots
   heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
   // The breakdown object DSL describing how we want
   // the census data to be.
   // @see `js/src/doc/Debugger/Debugger.Memory.md`
   breakdown: breakdownModel.isRequired,
   // List of reference to all snapshots taken
   snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
+  // True iff we want the tree displayed inverted.
+  inverted: PropTypes.bool.isRequired,
 };
deleted file mode 100644
--- a/devtools/client/memory/modules/census-view.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/* 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 file contains the tree view, displaying all the samples and frames
- * received from the proviler in a tree-like structure.
- */
-
-const { Cc, Ci, Cu, Cr } = require("chrome");
-const { Heritage } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
-const { AbstractTreeItem } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
-
-const INDENTATION = exports.INDENTATION = 16; // px
-const DEFAULT_AUTO_EXPAND_DEPTH = 2;
-const COURSE_TYPES = ["objects", "scripts", "strings", "other"];
-
-/**
- * Every instance of a `CensusView` represents a row in the census tree. The same
- * parent node is used for all rows.
- *
- * @param CensusView parent
- *        The CensusView considered the parent row. Should be null
- *        for root node.
- * @param {CensusTreeNode} censusTreeNode
- *        Data from `takeCensus` transformed via `CensusTreeNode`.
- *        @see browser/toolkit/heapsnapshot/census-tree-node.js
- * @param number level [optional]
- *        The indentation level in the call tree. The root node is at level 0.
- * @param boolean hidden [optional]
- *        Whether this node should be hidden and not contribute to depth/level
- *        calculations. Defaults to false.
- * @param number autoExpandDepth [optional]
- *        The depth to which the tree should automatically expand. Defaults to
- *        the caller's `autoExpandDepth` if a caller exists, otherwise defaults
- *        to DEFAULT_AUTO_EXPAND_DEPTH.
- */
-function CensusView ({ caller, censusTreeNode, level, hidden, autoExpandDepth }) {
-  AbstractTreeItem.call(this, { parent: caller, level: level|0 - (hidden ? 1 : 0) });
-
-  this.autoExpandDepth = autoExpandDepth != null
-    ? autoExpandDepth
-    : caller ? caller.autoExpandDepth
-             : DEFAULT_AUTO_EXPAND_DEPTH;
-
-  this.caller = caller;
-  this.censusTreeNode = censusTreeNode;
-  this.hidden = hidden;
-};
-
-CensusView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
-  /**
-   * Creates the view for this tree node.
-   * @param nsIDOMNode document
-   * @param nsIDOMNode arrowNode
-   * @return nsIDOMNode
-   */
-  _displaySelf: function (document, arrowNode) {
-    let data = this.censusTreeNode;
-
-    let cells = [];
-
-    // Only use an arrow if there are children
-    if (data.children && data.children.length) {
-      cells.push(arrowNode);
-    }
-
-    cells.push(this._createCell(document, data.name, "name"));
-
-    if (data.bytes != null) {
-      cells.push(this._createCell(document, data.bytes, "bytes"));
-    }
-    if (data.count != null) {
-      cells.push(this._createCell(document, data.count, "count"));
-    }
-
-    let targetNode = document.createElement("li");
-    targetNode.className = "heap-tree-item";
-    targetNode.style.MozMarginStart = `${this.level * INDENTATION}px`;
-    if (this.hidden) {
-      targetNode.style.display = "none";
-    }
-
-    for (let i = 0; i < cells.length; i++) {
-      targetNode.appendChild(cells[i]);
-    }
-
-    return targetNode;
-  },
-
-  /**
-   * Populates this node in the call tree with the corresponding "callees".
-   * These are defined in the `frame` data source for this call view.
-   * @param array:AbstractTreeItem children
-   */
-  _populateSelf: function (children) {
-    let newLevel = this.level + 1;
-    let data = this.censusTreeNode;
-
-    if (data.children) {
-      for (let node of data.children) {
-        children.push(new CensusView({
-          caller: this,
-          level: newLevel,
-          censusTreeNode: node,
-        }));
-      }
-    }
-  },
-
-  /**
-   * Functions creating each cell in this call view.
-   * Invoked by `_displaySelf`.
-   */
-  _createCell: function (doc, value, type) {
-    let cell = doc.createElement("span");
-    cell.className = "plain heap-tree-cell";
-    cell.setAttribute("type", type);
-    cell.innerHTML = value;
-    return cell;
-  },
-});
-
-exports.CensusView = CensusView;
deleted file mode 100644
--- a/devtools/client/memory/modules/moz.build
+++ /dev/null
@@ -1,8 +0,0 @@
-# vim: set filetype=python:
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-DevToolsModules(
-    'census-view.js',
-)
--- a/devtools/client/memory/moz.build
+++ b/devtools/client/memory/moz.build
@@ -1,26 +1,24 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'actions',
     'components',
-    'modules',
     'reducers',
 ]
 
 DevToolsModules(
     'app.js',
     'constants.js',
     'initializer.js',
     'models.js',
     'panel.js',
     'reducers.js',
     'store.js',
     'utils.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
-MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
--- a/devtools/client/memory/reducers.js
+++ b/devtools/client/memory/reducers.js
@@ -2,8 +2,9 @@
  * 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";
 
 exports.allocations = require("./reducers/allocations");
 exports.snapshots = require("./reducers/snapshots");
 exports.breakdown = require("./reducers/breakdown");
 exports.errors = require("./reducers/errors");
+exports.inverted = require("./reducers/inverted");
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers/inverted.js
@@ -0,0 +1,10 @@
+/* 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 { actions } = require("../constants");
+
+module.exports = function (inverted = false, action) {
+  return action.type === actions.TOGGLE_INVERTED ? !inverted : inverted;
+};
--- a/devtools/client/memory/reducers/moz.build
+++ b/devtools/client/memory/reducers/moz.build
@@ -2,10 +2,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'allocations.js',
     'breakdown.js',
     'errors.js',
+    'inverted.js',
     'snapshots.js',
 )
--- a/devtools/client/memory/reducers/snapshots.js
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -2,16 +2,23 @@
  * 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 { actions, snapshotState: states } = require("../constants");
 const { getSnapshot } = require("../utils");
 
 let handlers = Object.create({});
 
+handlers[actions.SNAPSHOT_ERROR] = function (snapshots, action) {
+  let snapshot = getSnapshot(snapshots, action.snapshot);
+  snapshot.state = states.ERROR;
+  snapshot.error = action.error;
+  return [...snapshots];
+};
+
 handlers[actions.TAKE_SNAPSHOT_START] = function (snapshots, { snapshot }) {
   return [...snapshots, snapshot];
 };
 
 handlers[actions.TAKE_SNAPSHOT_END] = function (snapshots, action) {
   return snapshots.map(snapshot => {
     if (snapshot.id === action.snapshot.id) {
       snapshot.state = states.SAVED;
@@ -33,24 +40,26 @@ handlers[actions.READ_SNAPSHOT_END] = fu
   return [...snapshots];
 };
 
 handlers[actions.TAKE_CENSUS_START] = function (snapshots, action) {
   let snapshot = getSnapshot(snapshots, action.snapshot);
   snapshot.state = states.SAVING_CENSUS;
   snapshot.census = null;
   snapshot.breakdown = action.breakdown;
+  snapshot.inverted = action.inverted;
   return [...snapshots];
 };
 
 handlers[actions.TAKE_CENSUS_END] = function (snapshots, action) {
   let snapshot = getSnapshot(snapshots, action.snapshot);
   snapshot.state = states.SAVED_CENSUS;
   snapshot.census = action.census;
   snapshot.breakdown = action.breakdown;
+  snapshot.inverted = action.inverted;
   return [...snapshots];
 };
 
 handlers[actions.SELECT_SNAPSHOT] = function (snapshots, action) {
   return snapshots.map(s => {
     s.selected = s.id === action.snapshot.id;
     return s;
   });
deleted file mode 100644
--- a/devtools/client/memory/test/mochitest/chrome.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[DEFAULT]
-tags = devtools
-skip-if = buildapp == 'b2g' || os == 'android'
-support-files =
-  head.js
-
-[test_census-view-01.html]
deleted file mode 100644
--- a/devtools/client/memory/test/mochitest/head.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cu = Components.utils;
-var Cr = Components.results;
-var CC = Components.Constructor;
-
-const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
deleted file mode 100644
--- a/devtools/client/memory/test/mochitest/test_census-view-01.html
+++ /dev/null
@@ -1,103 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<!--
-Bug 1067491 - Test taking a census over the RDP.
--->
-<head>
-  <meta charset="utf-8">
-  <title>Census Tree 01</title>
-  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <link href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css" />
-  <link href="chrome://devtools/skin/themes/light-theme.css" type="text/css" />
-  <link href="chrome://devtools/skin/themes/common.css" type="text/css" />
-  <link href="chrome://devtools/skin/themes/widgets.css" type="text/css" />
-  <link href="chrome://devtools/skin/themes/memory.css" type="text/css" />
-</head>
-<body>
-<ul id="container" style="width:100%;height:300px;"></ul>
-<pre id="test">
-<script src="head.js" type="application/javascript;version=1.8"></script>
-<script>
-window.onload = function() {
-  var { censusReportToCensusTreeNode } = require("devtools/shared/heapsnapshot/census-tree-node");
-  var { INDENTATION, CensusView } = require("devtools/client/memory/modules/census-view");
-  SimpleTest.waitForExplicitFinish();
-  const countBreakdown = { by: "count", count: true, bytes: true };
-
-  const BREAKDOWN = {
-    by: "coarseType",
-    objects: { by: "objectClass", then: countBreakdown },
-    strings: countBreakdown,
-    scripts: countBreakdown,
-    other: { by: "internalType", then: countBreakdown },
-  };
-
-  const REPORT = {
-    "objects": {
-      "Function": { bytes: 10, count: 1 },
-      "Array": { bytes: 20, count: 2 },
-    },
-    "strings": { bytes: 10, count: 1 },
-    "scripts": { bytes: 20, count: 2 },
-    "other": {
-      "js::Shape": { bytes: 30, count: 3 },
-      "js::Shape2": { bytes: 40, count: 4 }
-    },
-  };
-
-  const EXPECTED_ROWS = [
-    { level: 0, name: "other", bytes: 0, count: 0 },
-        { level: 1, name: "js::Shape2", bytes: 40, count: 4, },
-        { level: 1, name: "js::Shape", bytes: 30, count: 3, },
-    { level: 0, name: "objects", bytes: 0, count: 0 },
-        { level: 1, name: "Array", bytes: 20, count: 2, },
-        { level: 1, name: "Function", bytes: 10, count: 1, },
-    { level: 0, name: "scripts", bytes: 20, count: 2, },
-    { level: 0, name: "strings", bytes: 10, count: 1, },
-  ];
-  var censusTreeNode = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
-
-  var view = new CensusView({
-    censusTreeNode: censusTreeNode,
-    hidden: true
-  });
-
-  view.attachTo(document.querySelector("#container"));
-
-  var ul = document.querySelector("#container");
-  var children = Array.from(ul.children).filter(n => n.style.display !== "none");
-
-  for (var i = 0; i < children.length; i++) {
-    var el = children[i];
-    var expected = EXPECTED_ROWS[i];
-    var nameEl = el.querySelector(".heap-tree-cell[type='name']");
-    var bytesEl = el.querySelector(".heap-tree-cell[type='bytes']");
-    var countEl = el.querySelector(".heap-tree-cell[type='count']");
-
-    is(nameEl.innerHTML, expected.name,
-      `correct name "${expected.name}" in heap tree`);
-
-    is(el.style.MozMarginStart, (INDENTATION * expected.level) + "px",
-      `correct indentation for ${expected.name}`);
-
-    if ("bytes" in expected) {
-      is(bytesEl.innerHTML, String(expected.bytes),
-        `correct bytes "${expected.bytes}" in heap tree`);
-    } else {
-      ok(!bytesEl, `no bytes correctly displayed for ${expected.name}`);
-    }
-
-    if ("count" in expected) {
-      is(countEl.innerHTML, String(expected.count),
-        `correct count "${expected.count}" in heap tree`);
-    } else {
-      ok(!countEl, `no count correctly displayed for ${expected.name}`);
-    }
-  }
-
-  SimpleTest.finish();
-};
-</script>
-</pre>
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that changing inverted state properly refreshes the selected census.
+
+let { breakdowns, snapshotState: states } = require("devtools/client/memory/constants");
+let { toggleInvertedAndRefresh } = require("devtools/client/memory/actions/inverted");
+let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  let heapWorker = new HeapAnalysesClient();
+  yield front.attach();
+  let store = Store();
+  let { getState, dispatch } = store;
+
+  equal(getState().inverted, false, "not inverted by default");
+
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS]);
+  ok(true, "saved 3 snapshots and took a census of each of them");
+
+  dispatch(toggleInvertedAndRefresh(heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS,
+                                       states.SAVING_CENSUS]);
+  ok(true, "toggling inverted should recompute the selected snapshot's census");
+
+  equal(getState().inverted, true, "now inverted");
+
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS]);
+
+  equal(getState().snapshots[0].inverted, false);
+  equal(getState().snapshots[1].inverted, false);
+  equal(getState().snapshots[2].inverted, true);
+
+  dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1]));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVING_CENSUS,
+                                       states.SAVED_CENSUS]);
+  ok(true, "selecting non-inverted census should trigger a recompute");
+
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS]);
+
+  equal(getState().snapshots[0].inverted, false);
+  equal(getState().snapshots[1].inverted, true);
+  equal(getState().snapshots[2].inverted, true);
+
+  heapWorker.destroy();
+  yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that changing inverted state in the middle of taking a snapshot results
+// in an inverted census.
+
+let { snapshotState: states } = require("devtools/client/memory/constants");
+let { toggleInverted, toggleInvertedAndRefresh } = require("devtools/client/memory/actions/inverted");
+let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  let heapWorker = new HeapAnalysesClient();
+  yield front.attach();
+  let store = Store();
+  let { getState, dispatch } = store;
+
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVING]);
+
+  dispatch(toggleInverted());
+
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  ok(getState().inverted,
+     "should want inverted trees");
+  ok(getState().snapshots[0].inverted,
+     "snapshot-we-were-in-the-middle-of-saving's census should be inverted");
+
+  dispatch(toggleInvertedAndRefresh(heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
+  ok(true, "toggling inverted retriggers census");
+  ok(!getState().inverted, "no long inverted");
+
+  dispatch(toggleInverted());
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  ok(getState().inverted, "inverted again");
+  ok(getState().snapshots[0].inverted,
+     "census-we-were-in-the-middle-of-recomputing should be inverted again");
+
+  heapWorker.destroy();
+  yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test toggling the top level inversion state of the tree.
+
+let { toggleInverted } = require("devtools/client/memory/actions/inverted");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  let heapWorker = new HeapAnalysesClient();
+  yield front.attach();
+  let store = Store();
+  const { getState, dispatch } = store;
+
+  equal(getState().inverted, false, "not inverted by default");
+
+  dispatch(toggleInverted());
+  equal(getState().inverted, true, "now inverted after toggling");
+
+  dispatch(toggleInverted());
+  equal(getState().inverted, false, "not inverted again after toggling again");
+
+  heapWorker.destroy();
+  yield front.detach();
+});
--- a/devtools/client/memory/test/unit/xpcshell.ini
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -1,15 +1,18 @@
 [DEFAULT]
 tags = devtools
 head = head.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
+[test_action-toggle-inverted.js]
+[test_action-toggle-inverted-and-refresh-01.js]
+[test_action-toggle-inverted-and-refresh-02.js]
 [test_action-toggle-recording-allocations.js]
 [test_action-select-snapshot.js]
 [test_action-set-breakdown.js]
 [test_action-set-breakdown-and-refresh-01.js]
 [test_action-set-breakdown-and-refresh-02.js]
 [test_action-take-census.js]
 [test_action-take-snapshot.js]
 [test_action-take-snapshot-and-census.js]
--- a/devtools/client/memory/utils.js
+++ b/devtools/client/memory/utils.js
@@ -1,30 +1,23 @@
 /* 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/. */
 
+const { assert } = require("devtools/shared/DevToolsUtils");
 const { Preferences } = require("resource://gre/modules/Preferences.jsm");
 const CUSTOM_BREAKDOWN_PREF = "devtools.memory.custom-breakdowns";
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { snapshotState: states, breakdowns } = require("./constants");
+const FULL_ERROR_TEXT = "There was an error processing this snapshot.";
+const ERROR_SNAPSHOT_TEXT = "⚠ Error!";
 const SAVING_SNAPSHOT_TEXT = "Saving snapshot...";
 const READING_SNAPSHOT_TEXT = "Reading snapshot...";
 const SAVING_CENSUS_TEXT = "Taking heap census...";
 
-// @TODO 1215606
-// Use DevToolsUtils.assert when fixed.
-exports.assert = function (condition, message) {
-  if (!condition) {
-    const err = new Error("Assertion failure: " + message);
-    DevToolsUtils.reportException("DevToolsUtils.assert", err);
-    throw err;
-  }
-};
-
 /**
  * Returns an array of objects with the unique key `name`
  * and `displayName` for each breakdown.
  *
  * @return {Object{name, displayName}}
  */
 exports.getBreakdownDisplayData = function () {
   return exports.getBreakdownNames().map(name => {
@@ -89,45 +82,65 @@ exports.breakdownNameToSpec = function (
   else if (name in breakdowns) {
     return breakdowns[name].breakdown;
   }
   return Object.create(null);
 };
 
 /**
  * Returns a string representing a readable form of the snapshot's state.
+ * More brief than `getSnapshotStatusText`.
  *
  * @param {Snapshot} snapshot
  * @return {String}
  */
 exports.getSnapshotStatusText = function (snapshot) {
-  exports.assert((snapshot || {}).state,
+  assert((snapshot || {}).state,
     `Snapshot must have expected state, found ${(snapshot || {}).state}.`);
 
   switch (snapshot.state) {
+    case states.ERROR:
+      return ERROR_SNAPSHOT_TEXT;
     case states.SAVING:
       return SAVING_SNAPSHOT_TEXT;
     case states.SAVED:
     case states.READING:
       return READING_SNAPSHOT_TEXT;
     case states.SAVING_CENSUS:
       return SAVING_CENSUS_TEXT;
     // If it's read, it shouldn't have any label, as we could've cleared the
     // census cache by changing the breakdown, and we should lazily
     // go to SAVING_CENSUS. If it's SAVED_CENSUS, we have no status to display.
     case states.READ:
     case states.SAVED_CENSUS:
       return "";
   }
 
-  DevToolsUtils.reportException(`Snapshot in unexpected state: ${snapshot.state}`);
+  assert(false, `Snapshot in unexpected state: ${snapshot.state}`);
   return "";
 }
 
 /**
+ * Returns a string representing a readable form of the snapshot's state;
+ * more verbose than `getSnapshotStatusText`.
+ *
+ * @param {Snapshot} snapshot
+ * @return {String}
+ */
+exports.getSnapshotStatusTextFull = function (snapshot) {
+  assert((snapshot || {}).state,
+    `Snapshot must have expected state, found ${(snapshot || {}).state}.`);
+  switch (snapshot.state) {
+    case states.ERROR:
+      return FULL_ERROR_TEXT;
+  }
+  return exports.getSnapshotStatusText(snapshot);
+}
+
+/**
  * Takes an array of snapshots and a snapshot and returns
  * the snapshot instance in `snapshots` that matches
  * the snapshot passed in.
  *
  * @param {Array<Snapshot>} snapshots
  * @param {Snapshot}
  * @return ?Snapshot
  */
--- a/devtools/client/sourceeditor/codemirror/lib/codemirror.js
+++ b/devtools/client/sourceeditor/codemirror/lib/codemirror.js
@@ -3086,17 +3086,18 @@
       setDocumentHeight(cm, op.barMeasure);
     if (op.updatedDisplay || op.startHeight != cm.doc.height)
       updateScrollbars(cm, op.barMeasure);
 
     if (op.selectionChanged) restartBlink(cm);
 
     if (cm.state.focused && op.updateInput)
       cm.display.input.reset(op.typing);
-    if (op.focus && op.focus == activeElt()) ensureFocus(op.cm);
+    if (op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus()))
+      ensureFocus(op.cm);
   }
 
   function endOperation_finish(op) {
     var cm = op.cm, display = cm.display, doc = cm.doc;
 
     if (op.updatedDisplay) postUpdateDisplay(cm, op.update);
 
     // Abort mouse wheel delta measurement, when scrolling explicitly
--- a/devtools/client/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -20,17 +20,18 @@
 }
 
 html, .theme-body, #app, #memory-tool, #memory-tool-container {
   height: 100%;
 }
 
 .theme-body {
   overflow: hidden;
-  background-color: var(--theme-body-background);
+  /* Not sure why .theme-body declares it as `background` and overrides */
+  background-color: var(--theme-toolbar-background) !important;
 }
 
 #memory-tool-container {
   display: flex;
   flex-direction: row;
   --toolbar-height: 20px;
   /**
    * If --heap-tree-row-height changes, be sure to change HEAP_TREE_ROW_HEIGHT
@@ -40,37 +41,37 @@ html, .theme-body, #app, #memory-tool, #
 }
 
 /**
  * TODO bug 1213100
  * should generalize toolbar buttons with images in them
  * toolbars.inc.css contains definitions for .devtools-button,
  * I wager that many of the below styles can be rolled into that
  */
-.devtools-button.take-snapshot {
+.devtools-toolbar .devtools-button.take-snapshot {
   margin: 2px 1px;
   padding: 1px;
   border-width: 0px;
   /* [standalone] buttons override min-height from 18px to 24px -- why? */
   min-height: 18px;
   /* not sure why this is needed for positioning */
   display: -moz-box;
 }
 
-.devtools-button.take-snapshot::before {
+.devtools-toolbar .devtools-button.take-snapshot::before {
   background-image: url(images/command-screenshot.png);
   -moz-appearance: none;
   width: 16px;
   height: 16px;
   background-size: 64px 16px;
   background-position: 0 center;
   background-repeat: no-repeat;
 }
 @media (min-resolution: 1.1dppx) {
-  .devtools-button.take-snapshot::before {
+  .devtools-toolbar .devtools-button.take-snapshot::before {
     background-image: url(images/command-screenshot@2x.png);
   }
 }
 
 /**
  * TODO bug 1213100
  * Should this be codified in .devtools-toolbar itself?
  */
@@ -104,27 +105,29 @@ html, .theme-body, #app, #memory-tool, #
   margin: 0;
   padding: 0;
   width: 186px;
   list-style-type: none;
   font-size: 12px;
   height: 100%;
   overflow-y: scroll;
   background-color: var(--theme-toolbar-background);
+  color: var(--theme-body-color-alt);
   border-color: var(--theme-splitter-color);
-  color: var(--theme-body-color-alt);
-  border-left-width: 1px;
+  border-style: solid;
+  border-width: 0px 1px 0px 0px;
 }
 
 .list > li {
   height: 40px;
   color: var(--theme-body-color);
   border-bottom: 1px solid rgba(128,128,128,0.15);
   padding: 8px;
   cursor: pointer;
+  color: var(--theme-selection-color);
 }
 
 .list > li.selected {
   background-color: var(--theme-selection-background);
   color: var(--theme-selection-color);
 }
 
 .snapshot-list-item span {
@@ -143,39 +146,61 @@ html, .theme-body, #app, #memory-tool, #
 
 /**
  * Main panel
  */
 
 #heap-view {
   flex: 1 1 auto;
   border-color: var(--theme-splitter-color);
-  color: var(--theme-body-color)
+  color: var(--theme-body-color);
   border-left-width: 1px;
 }
 
 #heap-view .heap-view-panel {
   width: 100%;
   height: 100%;
+  background-color: var(--theme-toolbar-background)
+}
+
+#heap-view .heap-view-panel .snapshot-status, #heap-view .take-snapshot {
+  font-size: 120%;
+  display: block;
+  margin: 65px auto;
+}
+
+#heap-view .heap-view-panel .snapshot-status {
+  width: 500px;
+  text-align: center;
 }
 
 #heap-view .take-snapshot {
+  padding: 5px;
+}
+
+#heap-view .heap-view-panel[data-state="snapshot-state-error"] pre {
+  background-color: var(--theme-body-background);
+  overflow-y: scroll;
+  height: 100px;
+  margin: 20px;
+  padding: 20px;
 }
 
 /**
  * Heap Tree View
  */
 
 #heap-view .theme-twisty {
   float: left;
 }
 
 .tree {
   height: 100%;
   overflow-y: scroll;
+  background-color: var(--theme-body-background);
 }
 
 .tree .header {
   height: 17px;
   overflow: hidden;
   color: var(--theme-body-color);
   background-color: var(--theme-tab-toolbar-background);
 }
@@ -201,18 +226,19 @@ html, .theme-body, #app, #memory-tool, #
 .tree-node:nth-child(2n) {
   background-color: var(--row-alt-background-color);
 }
 
 .tree-node:hover {
   background-color: var(--row-hover-background-color);
 }
 
-.tree-node:focus {
+.tree-node:focus, .heap-tree-item.focused {
   background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
 }
 
 .header {
   background-color: var(--theme-tab-toolbar-background);
 }
 
 .header span {
   white-space: nowrap;
@@ -239,8 +265,25 @@ html, .theme-body, #app, #memory-tool, #
 .heap-tree-item-bytes,
 .heap-tree-item-total-bytes {
   width: 7vw;
 }
 
 .heap-tree-item-name {
   padding-left: 10px;
 }
+
+.error::before {
+  content: "";
+  background-image: url(chrome://devtools/skin/themes/images/webconsole.svg);
+  background-repeat: no-repeat;
+  background-size: 72px 60px;
+  width: 12px;
+  height: 12px;
+  display: inline-block;
+
+  background-position: -24px -24px;
+  margin: 2px 5px 0 0;
+  max-height: 12px;
+}
+.theme-light .error::before {
+  background-image: url(chrome://devtools/skin/themes/images/webconsole.svg#light-icons);
+}
--- a/devtools/client/themes/toolbars.inc.css
+++ b/devtools/client/themes/toolbars.inc.css
@@ -74,17 +74,17 @@
 }
 
 .devtools-menulist:-moz-focusring,
 .devtools-toolbarbutton:-moz-focusring {
   outline: 1px dotted hsla(210,30%,85%,0.7);
   outline-offset: -4px;
 }
 
-.devtools-toolbarbutton[standalone] {
+.devtools-toolbarbutton[standalone], .devtools-toolbarbutton[data-standalone] {
   -moz-margin-end: 5px;
   border-width: 1px;
   border-style: solid;
 }
 .devtools-toolbarbutton[label][standalone] {
   min-height: 2em;
 }
 
@@ -159,20 +159,22 @@
 }
 .theme-light .devtools-menulist,
 .theme-light .devtools-toolbarbutton {
   border-color: rgba(170, 170, 170, .5); /* Splitters */
 }
 
 /* Text-only buttons */
 .theme-light .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
+.theme-light .devtools-toolbarbutton[data-text-only],
 .theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image] {
   background-color: rgba(170, 170, 170, .2); /* Splitter */
 }
 .theme-dark .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
+.theme-dark .devtools-toolbarbutton[data-text-only],
 .theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image] {
   background-color: rgba(0, 0, 0, .2); /* Splitter */
 }
 
 /* Button States */
 .theme-dark .devtools-toolbarbutton:not([disabled]):hover,
 .theme-dark #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover,
 .theme-dark .devtools-toolbarbutton:not([disabled])[label]:not([text-as-image]):not([type=menu-button]):hover {
@@ -265,17 +267,17 @@
   margin: 0;
   padding: 0;
   min-width: 32px;
   min-height: 18px;
   /* The icon is absolutely positioned in the button using ::before */
   position: relative;
 }
 
-.devtools-button[standalone] {
+.devtools-button[standalone], .devtools-button[data-standalone] {
   min-height: 32px;
   border-width: 1px;
 }
 
 /* Button States */
 .theme-dark .devtools-button:not([disabled]):hover {
   background: rgba(0, 0, 0, .3); /* Splitters */
 }
--- a/devtools/shared/DevToolsUtils.js
+++ b/devtools/shared/DevToolsUtils.js
@@ -813,8 +813,15 @@ exports.openFileStream = function (fileP
 
 exports.isGenerator = function (fn) {
   return typeof fn === "function" && fn.isGenerator();
 };
 
 exports.isPromise = function (p) {
   return p && typeof p.then === "function";
 };
+
+/**
+ * Return true if `thing` is a SavedFrame, false otherwise.
+ */
+exports.isSavedFrame = function (thing) {
+  return Object.prototype.toString.call(thing) === "[object SavedFrame]";
+};
--- a/devtools/shared/heapsnapshot/CoreDump.proto
+++ b/devtools/shared/heapsnapshot/CoreDump.proto
@@ -105,17 +105,17 @@ message StackFrame {
 message Node {
     optional uint64     id                   = 1;
 
     // De-duplicated two-byte string.
     oneof TypeNameOrRef {
         bytes           typeName             = 2;
         uint64          typeNameRef          = 3;
     }
-    
+
     optional uint64     size                 = 4;
     repeated Edge       edges                = 5;
     optional StackFrame allocationStack      = 6;
 
     // De-duplicated one-byte string.
     oneof JSObjectClassNameOrRef {
         bytes           jsObjectClassName    = 7;
         uint64          jsObjectClassNameRef = 8;
--- a/devtools/shared/heapsnapshot/HeapSnapshot.cpp
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
@@ -99,28 +99,38 @@ HeapSnapshot::Create(JSContext* cx,
 template<typename MessageType>
 static bool
 parseMessage(ZeroCopyInputStream& stream, MessageType& message)
 {
   // We need to create a new `CodedInputStream` for each message so that the
   // 64MB limit is applied per-message rather than to the whole stream.
   CodedInputStream codedStream(&stream);
 
+  // The protobuf message nesting that core dumps exhibit is dominated by
+  // allocation stacks' frames. In the most deeply nested case, each frame has
+  // two messages: a StackFrame message and a StackFrame::Data message. These
+  // frames are on top of a small constant of other messages. There are a
+  // MAX_STACK_DEPTH number of frames, so we multiply this by 3 to make room for
+  // the two messages per frame plus some head room for the constant number of
+  // non-dominating messages.
+  codedStream.SetRecursionLimit(HeapSnapshot::MAX_STACK_DEPTH * 3);
+
   // Because protobuf messages aren't self-delimiting, we serialize each message
   // preceeded by its size in bytes. When deserializing, we read this size and
   // then limit reading from the stream to the given byte size. If we didn't,
   // then the first message would consume the entire stream.
 
   uint32_t size = 0;
   if (NS_WARN_IF(!codedStream.ReadVarint32(&size)))
     return false;
 
   auto limit = codedStream.PushLimit(size);
   if (NS_WARN_IF(!message.ParseFromCodedStream(&codedStream)) ||
-      NS_WARN_IF(!codedStream.ConsumedEntireMessage()))
+      NS_WARN_IF(!codedStream.ConsumedEntireMessage()) ||
+      NS_WARN_IF(codedStream.BytesUntilLimit() != 0))
   {
     return false;
   }
 
   codedStream.PopLimit(limit);
   return true;
 }
 
@@ -904,17 +914,18 @@ class MOZ_STACK_CLASS StreamWriter : pub
     uint64_t ref = oneByteStringsAlreadySerialized.count();
     if (!oneByteStringsAlreadySerialized.add(ptr, string, ref))
       return false;
 
     setString(stringData.release());
     return true;
   }
 
-  protobuf::StackFrame* getProtobufStackFrame(JS::ubi::StackFrame& frame) {
+  protobuf::StackFrame* getProtobufStackFrame(JS::ubi::StackFrame& frame,
+                                              size_t depth = 1) {
     // NB: de-duplicated string properties must be written in the same order
     // here as they are read in `HeapSnapshot::saveStackFrame` or else indices
     // in references to already serialized strings will be off.
 
     MOZ_ASSERT(frame,
                "null frames should be represented as the lack of a serialized "
                "stack frame");
 
@@ -952,18 +963,18 @@ class MOZ_STACK_CLASS StreamWriter : pub
                                [&] (std::string* name) { data->set_allocated_functiondisplayname(name); },
                                [&] (uint64_t ref) { data->set_functiondisplaynameref(ref); }))
       {
         return nullptr;
       }
     }
 
     auto parent = frame.parent();
-    if (parent) {
-      auto protobufParent = getProtobufStackFrame(parent);
+    if (parent && depth < HeapSnapshot::MAX_STACK_DEPTH) {
+      auto protobufParent = getProtobufStackFrame(parent, depth + 1);
       if (!protobufParent)
         return nullptr;
       data->set_allocated_parent(protobufParent);
     }
 
     protobufStackFrame->set_allocated_data(data.release());
 
     if (!framesAlreadySerialized.put(id))
--- a/devtools/shared/heapsnapshot/HeapSnapshot.h
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.h
@@ -66,16 +66,23 @@ class HeapSnapshot final : public nsISup
   bool saveNode(const protobuf::Node& node);
 
   // Save the given `protobuf::StackFrame` message in this `HeapSnapshot` as a
   // `DeserializedStackFrame`. The saved stack frame's id is returned via the
   // out parameter.
   bool saveStackFrame(const protobuf::StackFrame& frame,
                       StackFrameId& outFrameId);
 
+public:
+  // The maximum number of stack frames that we will serialize into a core
+  // dump. This helps prevent over-recursion in the protobuf library when
+  // deserializing stacks.
+  static const size_t MAX_STACK_DEPTH = 60;
+
+private:
   // If present, a timestamp in the same units that `PR_Now` gives.
   Maybe<uint64_t> timestamp;
 
   // The id of the root node for this deserialized heap graph.
   NodeId rootId;
 
   // The set of nodes in this deserialized heap graph, keyed by id.
   using NodeSet = js::HashSet<DeserializedNode, DeserializedNode::HashPolicy>;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_deepStack_01.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can save a core dump with very deep allocation stacks and read
+// it back into a HeapSnapshot.
+
+function stackDepth(stack) {
+  return stack ? 1 + stackDepth(stack.parent) : 0;
+}
+
+function run_test() {
+  // Create a Debugger observing a debuggee's allocations.
+  const debuggee = new Cu.Sandbox(null);
+  const dbg = new Debugger(debuggee);
+  dbg.memory.trackingAllocationSites = true;
+
+  // Allocate some objects in the debuggee that will have their allocation
+  // stacks recorded by the Debugger.
+
+  debuggee.eval("this.objects = []");
+  debuggee.eval(
+    (function recursiveAllocate(n) {
+      if (n <= 0)
+        return;
+
+      // Make sure to recurse before pushing the object so that when TCO is
+      // implemented sometime in the future, it doesn't invalidate this test.
+      recursiveAllocate(n - 1);
+      this.objects.push({});
+    }).toString()
+  );
+  debuggee.eval("recursiveAllocate = recursiveAllocate.bind(this);");
+  debuggee.eval("recursiveAllocate(200);");
+
+  // Now save a snapshot that will include the allocation stacks and read it
+  // back again.
+
+  const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
+  ok(true, "Should be able to save a snapshot.");
+
+  const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+  ok(snapshot, "Should be able to read a heap snapshot");
+  ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
+
+  const report = snapshot.takeCensus({
+    breakdown: { by: "allocationStack",
+                 then: { by: "count", bytes: true, count: true },
+                 noStack: { by: "count", bytes: true, count: true }
+               }
+  });
+
+  // Keep this synchronized with `HeapSnapshot::MAX_STACK_DEPTH`!
+  const MAX_STACK_DEPTH = 60;
+
+  let foundStacks = false;
+  report.forEach((v, k) => {
+    if (k === "noStack") {
+      return;
+    }
+
+    foundStacks = true;
+    const depth = stackDepth(k);
+    dumpn("Stack depth is " + depth);
+    ok(depth <= MAX_STACK_DEPTH,
+       "Every stack should have depth less than or equal to the maximum stack depth");
+  });
+  ok(foundStacks);
+
+  do_test_finished();
+}
--- a/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
+++ b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
@@ -29,16 +29,17 @@ support-files =
 [test_HeapAnalyses_takeCensus_01.js]
 [test_HeapAnalyses_takeCensus_02.js]
 [test_HeapAnalyses_takeCensus_03.js]
 [test_HeapAnalyses_takeCensus_04.js]
 [test_HeapAnalyses_takeCensus_05.js]
 [test_HeapAnalyses_takeCensus_06.js]
 [test_HeapAnalyses_takeCensus_07.js]
 [test_HeapSnapshot_creationTime_01.js]
+[test_HeapSnapshot_deepStack_01.js]
 [test_HeapSnapshot_takeCensus_01.js]
 [test_HeapSnapshot_takeCensus_02.js]
 [test_HeapSnapshot_takeCensus_03.js]
 [test_HeapSnapshot_takeCensus_04.js]
 [test_HeapSnapshot_takeCensus_05.js]
 [test_HeapSnapshot_takeCensus_06.js]
 [test_HeapSnapshot_takeCensus_07.js]
 [test_HeapSnapshot_takeCensus_08.js]
--- a/mobile/android/base/AccountsHelper.java
+++ b/mobile/android/base/AccountsHelper.java
@@ -11,32 +11,36 @@ import android.accounts.AccountManagerCa
 import android.accounts.AccountManagerFuture;
 import android.accounts.AuthenticatorException;
 import android.accounts.OperationCanceledException;
 import android.content.Context;
 import android.content.Intent;
 import android.util.Log;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.restrictions.Restriction;
+import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Helper class to manage Android Accounts corresponding to Firefox Accounts.
  */
 public class AccountsHelper implements NativeEventListener {
     public static final String LOGTAG = "GeckoAccounts";
 
     protected final Context mContext;
@@ -52,32 +56,34 @@ public class AccountsHelper implements N
             return;
         }
         dispatcher.registerGeckoThreadListener(this,
                 "Accounts:CreateFirefoxAccountFromJSON",
                 "Accounts:UpdateFirefoxAccountFromJSON",
                 "Accounts:Create",
                 "Accounts:DeleteFirefoxAccount",
                 "Accounts:Exist",
-                "Accounts:ProfileUpdated");
+                "Accounts:ProfileUpdated",
+                "Accounts:ShowSyncPreferences");
     }
 
     public synchronized void uninit() {
         EventDispatcher dispatcher = EventDispatcher.getInstance();
         if (dispatcher == null) {
             Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
             return;
         }
         dispatcher.unregisterGeckoThreadListener(this,
                 "Accounts:CreateFirefoxAccountFromJSON",
                 "Accounts:UpdateFirefoxAccountFromJSON",
                 "Accounts:Create",
                 "Accounts:DeleteFirefoxAccount",
                 "Accounts:Exist",
-                "Accounts:ProfileUpdated");
+                "Accounts:ProfileUpdated",
+                "Accounts:ShowSyncPreferences");
     }
 
     @Override
     public void handleMessage(String event, NativeJSObject message, final EventCallback callback) {
         if (!RestrictedProfiles.isAllowed(mContext, Restriction.DISALLOW_MODIFY_ACCOUNTS)) {
             // We register for messages in all contexts; we drop, with a log and an error to JavaScript,
             // when the profile is restricted.  It's better to return errors than silently ignore messages.
             Log.e(LOGTAG, "Profile is not allowed to modify accounts!  Ignoring event: " + event);
@@ -108,16 +114,36 @@ public class AccountsHelper implements N
                 fxAccount = AndroidFxAccount.addAndroidAccount(mContext,
                         email,
                         mProfile.getName(),
                         authServerEndpoint,
                         tokenServerEndpoint,
                         profileServerEndpoint,
                         state,
                         AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
+
+                final String[] declinedSyncEngines = json.optStringArray("declinedSyncEngines", null);
+                if (declinedSyncEngines != null) {
+                    Log.i(LOGTAG, "User has selected engines; storing to prefs.");
+                    final Map<String, Boolean> selectedEngines = new HashMap<String, Boolean>();
+                    for (String enabledSyncEngine : SyncConfiguration.validEngineNames()) {
+                        selectedEngines.put(enabledSyncEngine, true);
+                    }
+                    for (String declinedSyncEngine : declinedSyncEngines) {
+                        selectedEngines.put(declinedSyncEngine, false);
+                    }
+                    // The "forms" engine has the same state as the "history" engine.
+                    selectedEngines.put("forms", selectedEngines.get("history"));
+                    FxAccountUtils.pii(LOGTAG, "User selected engines: " + selectedEngines.toString());
+                    try {
+                        SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines);
+                    } catch (UnsupportedEncodingException | GeneralSecurityException e) {
+                        Log.e(LOGTAG, "Got exception storing selected engines; ignoring.", e);
+                    }
+                }
             } catch (URISyntaxException | GeneralSecurityException | UnsupportedEncodingException e) {
                 Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e);
                 if (callback != null) {
                     callback.sendError("Could not create Firefox Account from JSON: " + e.toString());
                     return;
                 }
             }
             if (callback != null) {
@@ -265,11 +291,22 @@ public class AccountsHelper implements N
         } else if ("Accounts:ProfileUpdated".equals(event)) {
             final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
             if (account == null) {
                 Log.w(LOGTAG, "Can't change profile of non-existent Firefox Account!; ignored");
                 return;
             }
             final AndroidFxAccount androidFxAccount = new AndroidFxAccount(mContext, account);
             androidFxAccount.fetchProfileJSON();
+        } else if ("Accounts:ShowSyncPreferences".equals(event)) {
+            final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
+            if (account == null) {
+                Log.w(LOGTAG, "Can't change show Sync preferences of non-existent Firefox Account!; ignored");
+                return;
+            }
+            // We don't necessarily have an Activity context here, so we always start in a new task.
+            final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mContext.startActivity(intent);
         }
     }
 }
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -111,16 +111,30 @@ public class AppConstants {
     public static final String MOZ_MOZILLA_API_KEY = "@MOZ_MOZILLA_API_KEY@";
     public static final boolean MOZ_STUMBLER_BUILD_TIME_ENABLED =
 //#ifdef MOZ_ANDROID_MLS_STUMBLER
     true;
 //#else
     false;
 //#endif
 
+    public static final boolean MOZ_ANDROID_GCM =
+//#ifdef MOZ_ANDROID_GCM
+    true;
+//#else
+    false;
+//#endif
+
+    public static final String MOZ_ANDROID_GCM_SENDERID =
+//#ifdef MOZ_ANDROID_GCM_SENDERID
+    "@MOZ_ANDROID_GCM_SENDERID@";
+//#else
+    null;
+//#endif
+
     public static final String MOZ_CHILD_PROCESS_NAME = "@MOZ_CHILD_PROCESS_NAME@";
     public static final String MOZ_UPDATE_CHANNEL = "@MOZ_UPDATE_CHANNEL@";
     public static final String OMNIJAR_NAME = "@OMNIJAR_NAME@";
     public static final String OS_TARGET = "@OS_TARGET@";
     public static final String TARGET_XPCOM_ABI = @TARGET_XPCOM_ABI@;
 
     public static final String USER_AGENT_BOT_LIKE = "Redirector/" + AppConstants.MOZ_APP_VERSION +
         " (Android; rv:" + AppConstants.MOZ_APP_VERSION + ")";
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -13,22 +13,26 @@ import org.mozilla.gecko.DynamicToolbar.
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.TransitionsTracker;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
+import org.mozilla.gecko.db.TabsAccessor;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.firstrun.FirstrunPane;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.health.BrowserHealthRecorder;
 import org.mozilla.gecko.health.BrowserHealthReporter;
 import org.mozilla.gecko.health.HealthRecorder;
 import org.mozilla.gecko.health.SessionInformation;
 import org.mozilla.gecko.home.BrowserSearch;
@@ -77,16 +81,17 @@ import org.mozilla.gecko.util.PrefUtils;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 import org.mozilla.gecko.widget.AnchoredPopup;
 import org.mozilla.gecko.widget.ButtonToast;
 import org.mozilla.gecko.widget.ButtonToast.ToastListener;
 import org.mozilla.gecko.widget.GeckoActionProvider;
 
+import android.accounts.Account;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
@@ -99,16 +104,18 @@ import android.graphics.drawable.BitmapD
 import android.net.Uri;
 import android.nfc.NdefMessage;
 import android.nfc.NdefRecord;
 import android.nfc.NfcAdapter;
 import android.nfc.NfcEvent;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.StrictMode;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.view.MenuItemCompat;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Base64;
 import android.util.Base64OutputStream;
 import android.util.Log;
@@ -3224,26 +3231,23 @@ public class BrowserApp extends GeckoApp
                 }
             }
             return true;
         }
 
         if (itemId == R.id.send_to_device) {
             tab = Tabs.getInstance().getSelectedTab();
             if (tab != null) {
-                String url = tab.getURL();
-                if (url != null) {
-                    if (AboutPages.isAboutReader(url)) {
-                        url = ReaderModeUtils.getUrlFromAboutReader(url);
+                final Tab selectedTab = tab;
+                ThreadUtils.postToBackgroundThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        handleSendToDevice(selectedTab);
                     }
-                    Intent sendToDeviceIntent = GeckoAppShell.getShareIntent(getContext(), url,
-                            "text/plain", tab.getDisplayTitle());
-                    sendToDeviceIntent.setClass(getContext(), ShareDialog.class);
-                    startActivity(sendToDeviceIntent);
-                }
+                });
             }
             return true;
         }
 
         if (itemId == R.id.share) {
             shareCurrentUrl();
             return true;
         }
@@ -3363,16 +3367,56 @@ public class BrowserApp extends GeckoApp
         // a chance.
         if (onContextItemSelected(item)) {
             return true;
         }
 
         return super.onOptionsItemSelected(item);
     }
 
+    /**
+     * Handles a press to the send to device button in the browser menu. The
+     * expected states when the user presses the button are:
+     *   * Not signed in: open to FxA sign-up
+     *   * Signed in but no other devices: display toast with a message
+     * explaining they should connect another device to use this feature
+     *   * Signed in but >= 1 other device: display device list
+     */
+    @WorkerThread
+    private void handleSendToDevice(@NonNull final Tab selectedTab) {
+        final Account account = FirefoxAccounts.getFirefoxAccount(this);
+        if (account == null) {
+            // TODO (bug 1217164): Go back to previous tab on back press
+            final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
+            intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
+            startActivity(intent);
+            return;
+        }
+
+        final BrowserDB browserDB = GeckoProfile.get(this).getDB();
+        final TabsAccessor tabsAccessor = browserDB.getTabsAccessor();
+        final int remoteClientCount = tabsAccessor.getRemoteClientCount(this);
+        if (remoteClientCount == 0) {
+            final Toast toast = Toast.makeText(this, R.string.menu_no_synced_devices, Toast.LENGTH_LONG);
+            toast.show();
+
+        } else {
+            String url = selectedTab.getURL();
+            if (url != null) {
+                if (AboutPages.isAboutReader(url)) {
+                    url = ReaderModeUtils.getUrlFromAboutReader(url);
+                }
+                final Intent sendToDeviceIntent = GeckoAppShell.getShareIntent(getContext(), url,
+                        "text/plain", selectedTab.getDisplayTitle());
+                sendToDeviceIntent.setClass(getContext(), ShareDialog.class);
+                startActivity(sendToDeviceIntent);
+            }
+        }
+    }
+
     @Override
     public boolean onMenuItemLongClick(MenuItem item) {
         if (item.getItemId() == R.id.reload) {
             Tab tab = Tabs.getInstance().getSelectedTab();
             if (tab != null) {
                 tab.doReload(true);
 
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "reload_force");
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -69,16 +69,24 @@ ifdef MOZ_NATIVE_DEVICES
         $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
         $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
         $(ANDROID_PLAY_SERVICES_CAST_AAR_LIB) \
         $(ANDROID_MEDIAROUTER_V7_AAR_LIB) \
         $(ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB) \
         $(NULL)
 endif
 
+ifdef MOZ_ANDROID_GCM
+    JAVA_CLASSPATH += \
+        $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
+        $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
+        $(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
+        $(NULL)
+endif
+
 JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH)))
 
 # Library jars that we're bundling: these are subject to Proguard before inclusion
 # into classes.dex.
 java_bundled_libs := \
     $(ANDROID_SUPPORT_V4_AAR_LIB) \
     $(ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB) \
     $(ANDROID_APPCOMPAT_V7_AAR_LIB) \
@@ -91,16 +99,28 @@ ifdef MOZ_NATIVE_DEVICES
         $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
         $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
         $(ANDROID_PLAY_SERVICES_CAST_AAR_LIB) \
         $(ANDROID_MEDIAROUTER_V7_AAR_LIB) \
         $(ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB) \
         $(NULL)
 endif
 
+ifdef MOZ_ANDROID_GCM
+    java_bundled_libs += \
+        $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
+        $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
+        $(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
+        $(NULL)
+endif
+
+# uniq purloined from http://stackoverflow.com/a/16151140.
+uniq = $(if $1,$(firstword $1) $(call uniq,$(filter-out $(firstword $1),$1)))
+
+java_bundled_libs := $(call uniq,$(java_bundled_libs))
 java_bundled_libs := $(subst $(NULL) ,:,$(strip $(java_bundled_libs)))
 
 # All the jars we're compiling from source. (not to be confused with
 # java_bundled_libs, which holds the jars which we're including as binaries).
 ALL_JARS = \
   constants.jar \
   gecko-R.jar \
   gecko-browser.jar \
@@ -366,16 +386,17 @@ generated/org/mozilla/gecko/R.java: .aap
 generated/android/support/v4/R.java: .aapt.deps ;
 generated/android/support/v7/appcompat/R.java: .aapt.deps ;
 generated/android/support/design/R.java: .aapt.deps ;
 generated/android/support/v7/mediarouter/R.java: .aapt.deps ;
 generated/android/support/v7/recyclerview/R.java: .aapt.deps ;
 generated/com/google/android/gms/R.java: .aapt.deps ;
 generated/com/google/android/gms/base/R.java: .aapt.deps ;
 generated/com/google/android/gms/cast/R.java: .aapt.deps ;
+generated/com/google/android/gms/gcm/R.java: .aapt.deps ;
 
 gecko.ap_: .aapt.deps ;
 R.txt: .aapt.deps ;
 
 # [Comment 2/3] This tom-foolery provides a target that forces a
 # rebuild of gecko.ap_.  This is used during packaging to ensure that
 # resources are fresh.  The alternative would be complicated; see
 # [Comment 1/3].
--- a/mobile/android/base/db/BrowserContract.java
+++ b/mobile/android/base/db/BrowserContract.java
@@ -295,26 +295,23 @@ public class BrowserContract {
 
         // Last used time of the tab.
         public static final String LAST_USED = "last_used";
 
         // Position of the tab. 0 represents foreground.
         public static final String POSITION = "position";
     }
 
-    public static final class Clients {
+    public static final class Clients implements CommonColumns {
         private Clients() {}
         public static final Uri CONTENT_RECENCY_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients_recency");
         public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients");
         public static final String CONTENT_TYPE = "vnd.android.cursor.dir/client";
         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/client";
 
-        // Implicit rowid in SQL table.
-        public static final String ROWID = "rowid";
-
         // Client-provided name string. Could conceivably be null.
         public static final String NAME = "name";
 
         // Sync-assigned GUID for client device. NULL for local tabs.
         public static final String GUID = "guid";
 
         // Last modified time for the client's tab record. For remote records, a server
         // timestamp provided by Sync during insertion.
--- a/mobile/android/base/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/db/BrowserDatabaseHelper.java
@@ -34,17 +34,17 @@ import android.database.sqlite.SQLiteOpe
 import android.net.Uri;
 import android.os.Build;
 import android.util.Log;
 
 
 final class BrowserDatabaseHelper extends SQLiteOpenHelper {
     private static final String LOGTAG = "GeckoBrowserDBHelper";
 
-    public static final int DATABASE_VERSION = 24;
+    public static final int DATABASE_VERSION = 25;
     public static final String DATABASE_NAME = "browser.db";
 
     final protected Context mContext;
 
     static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
     static final String TABLE_HISTORY = History.TABLE_NAME;
     static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
     static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
@@ -192,37 +192,42 @@ final class BrowserDatabaseHelper extend
                 BrowserContract.Clients.DEVICE_TYPE + " TEXT" +
                 ");");
 
         // Index on GUID.
         db.execSQL("CREATE INDEX " + TabsProvider.INDEX_CLIENTS_GUID +
                 " ON " + TABLE_CLIENTS + "(" + BrowserContract.Clients.GUID + ")");
     }
 
-    private void createTabsTable(SQLiteDatabase db) {
+    private boolean didCreateTabsTable = false;
+    private void createTabsTable(SQLiteDatabase db, final String tableName) {
         debug("Creating tabs.db: " + db.getPath());
-        debug("Creating " + TABLE_TABS + " table");
+        debug("Creating " + tableName + " table");
 
         // Table for each tab on any client.
-        db.execSQL("CREATE TABLE " + TABLE_TABS + "(" +
+        db.execSQL("CREATE TABLE " + tableName + "(" +
                 BrowserContract.Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                 BrowserContract.Tabs.CLIENT_GUID + " TEXT," +
                 BrowserContract.Tabs.TITLE + " TEXT," +
                 BrowserContract.Tabs.URL + " TEXT," +
                 BrowserContract.Tabs.HISTORY + " TEXT," +
                 BrowserContract.Tabs.FAVICON + " TEXT," +
                 BrowserContract.Tabs.LAST_USED + " INTEGER," +
-                BrowserContract.Tabs.POSITION + " INTEGER" +
+                BrowserContract.Tabs.POSITION + " INTEGER, " +
+                "FOREIGN KEY (" + BrowserContract.Tabs.CLIENT_GUID + ") REFERENCES " +
+                TABLE_CLIENTS + "(" + BrowserContract.Clients.GUID + ") ON DELETE CASCADE" +
                 ");");
+    }
 
+    private void createTabsTableIndices(SQLiteDatabase db, final String tableName) {
         // Indices on CLIENT_GUID and POSITION.
         db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_GUID +
-                " ON " + TABLE_TABS + "(" + BrowserContract.Tabs.CLIENT_GUID + ")");
+                " ON " + tableName + "(" + BrowserContract.Tabs.CLIENT_GUID + ")");
         db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_POSITION +
-                " ON " + TABLE_TABS + "(" + BrowserContract.Tabs.POSITION + ")");
+                " ON " + tableName + "(" + BrowserContract.Tabs.POSITION + ")");
     }
 
     // Insert a client row for our local Fennec client.
     private void createLocalClient(SQLiteDatabase db) {
         debug("Inserting local Fennec client into " + TABLE_CLIENTS + " table");
 
         ContentValues values = new ContentValues();
         values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
@@ -336,19 +341,22 @@ final class BrowserDatabaseHelper extend
         for (Table table : BrowserProvider.sTables) {
             table.onCreate(db);
         }
 
         createBookmarksTable(db);
         createHistoryTable(db);
         createFaviconsTable(db);
         createThumbnailsTable(db);
-        createTabsTable(db);
         createClientsTable(db);
         createLocalClient(db);
+        createTabsTable(db, TABLE_TABS);
+        didCreateTabsTable = true;
+        createTabsTableIndices(db, TABLE_TABS);
+
 
         createBookmarksWithFaviconsView(db);
         createHistoryWithFaviconsView(db);
         createCombinedViewOn19(db);
 
         createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
             R.string.bookmarks_folder_places, 0);
 
@@ -361,18 +369,20 @@ final class BrowserDatabaseHelper extend
 
     /**
      * Copies the tabs and clients tables out of the given tabs.db file and into the destinationDB.
      *
      * @param tabsDBFile Path to existing tabs.db.
      * @param destinationDB The destination database.
      */
     public void copyTabsDB(File tabsDBFile, SQLiteDatabase destinationDB) {
-        createTabsTable(destinationDB);
         createClientsTable(destinationDB);
+        createTabsTable(destinationDB, TABLE_TABS);
+        didCreateTabsTable = true;
+        createTabsTableIndices(destinationDB, TABLE_TABS);
 
         SQLiteDatabase oldTabsDB = null;
         try {
             oldTabsDB = SQLiteDatabase.openDatabase(tabsDBFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
 
             if (!DBUtils.copyTable(oldTabsDB, TABLE_CLIENTS, destinationDB, TABLE_CLIENTS)) {
                 Log.e(LOGTAG, "Failed to migrate table clients; ignoring.");
             }
@@ -982,16 +992,51 @@ final class BrowserDatabaseHelper extend
             try {
                 FileUtils.delete(file);
             } catch (Exception e) {
                 Log.e(LOGTAG, "Exception occurred while trying to delete " + file.getPath() + "; ignoring.", e);
             }
         }
     }
 
+    private void upgradeDatabaseFrom24to25(SQLiteDatabase db) {
+        if (didCreateTabsTable) {
+            debug("No need to rev tabs schema; foreign key constraint exists.");
+            return;
+        }
+
+        debug("Rewriting tabs table.");
+        createTabsTable(db, "tmp_tabs");
+
+        // Remove indexes. We don't need them now, and we'll be throwing away the table.
+        db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_GUID);
+        db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_POSITION);
+
+        db.execSQL("INSERT INTO tmp_tabs (" +
+                        // Here are the columns we can preserve.
+                        BrowserContract.Tabs._ID + ", " +
+                        BrowserContract.Tabs.CLIENT_GUID + ", " +
+                        BrowserContract.Tabs.TITLE + ", " +
+                        BrowserContract.Tabs.URL + ", " +
+                        BrowserContract.Tabs.HISTORY + ", " +
+                        BrowserContract.Tabs.FAVICON + ", " +
+                        BrowserContract.Tabs.LAST_USED + ", " +
+                        BrowserContract.Tabs.POSITION +
+                        ") " +
+                        "SELECT " +
+                        "_id, client_guid, title, url, history, favicon, last_used, position" +
+                        " FROM " + TABLE_TABS);
+
+        // Now switch these tables over and recreate the indices.
+        db.execSQL("DROP TABLE " + TABLE_TABS);
+        db.execSQL("ALTER TABLE tmp_tabs RENAME TO " + TABLE_TABS);
+        createTabsTableIndices(db, TABLE_TABS);
+        didCreateTabsTable =true;
+    }
+
     private void createV19CombinedView(SQLiteDatabase db) {
         db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
         db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
 
         createCombinedViewOn19(db);
     }
 
     @Override
@@ -1057,16 +1102,20 @@ final class BrowserDatabaseHelper extend
 
                 case 23:
                     upgradeDatabaseFrom22to23(db);
                     break;
 
                 case 24:
                     upgradeDatabaseFrom23to24(db);
                     break;
+
+                case 25:
+                    upgradeDatabaseFrom24to25(db);
+                    break;
             }
         }
 
         for (Table table : BrowserProvider.sTables) {
             table.onUpgrade(db, oldVersion, newVersion);
         }
 
         // Delete the obsolete favicon database after all other upgrades complete.
--- a/mobile/android/base/db/LocalTabsAccessor.java
+++ b/mobile/android/base/db/LocalTabsAccessor.java
@@ -67,16 +67,30 @@ public class LocalTabsAccessor implement
     private final Uri clientsUriWithProfile;
 
     public LocalTabsAccessor(String profileName) {
         tabsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Tabs.CONTENT_URI);
         clientsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_URI);
         clientsRecencyUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_RECENCY_URI);
     }
 
+    @Override
+    public int getRemoteClientCount(final Context context) {
+        final Cursor remoteClientsCursor = getRemoteClientsByRecencyCursor(context);
+        if (remoteClientsCursor == null) {
+            return 0;
+        }
+
+        try {
+            return remoteClientsCursor.getCount();
+        } finally {
+            remoteClientsCursor.close();
+        }
+    }
+
     /**
      * Extracts a List of just RemoteClients from a cursor.
      * The supplied cursor should be grouped by guid and sorted by most recently used.
      */
     @Override
     public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) {
         final ArrayList<RemoteClient> clients = new ArrayList<>(cursor.getCount());
 
--- a/mobile/android/base/db/StubBrowserDB.java
+++ b/mobile/android/base/db/StubBrowserDB.java
@@ -113,16 +113,21 @@ class StubURLMetadata implements URLMeta
     }
 }
 
 class StubTabsAccessor implements TabsAccessor {
     public StubTabsAccessor() {
     }
 
     @Override
+    public int getRemoteClientCount(Context context) {
+        return 0;
+    }
+
+    @Override
     public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) {
         return new ArrayList<>();
     }
 
     @Override
     public List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
         return new ArrayList<RemoteClient>();
     }
--- a/mobile/android/base/db/TabsAccessor.java
+++ b/mobile/android/base/db/TabsAccessor.java
@@ -12,16 +12,17 @@ import org.mozilla.gecko.Tab;
 
 import java.util.List;
 
 public interface TabsAccessor {
     public interface OnQueryTabsCompleteListener {
         public void onQueryTabsComplete(List<RemoteClient> clients);
     }
 
+    public int getRemoteClientCount(Context context);
     public Cursor getRemoteClientsByRecencyCursor(Context context);
     public Cursor getRemoteTabsCursor(Context context);
     public Cursor getRemoteTabsCursor(Context context, int limit);
     public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(final Cursor cursor);
     public List<RemoteClient> getClientsFromCursor(final Cursor cursor);
     public void getTabs(final Context context, final OnQueryTabsCompleteListener listener);
     public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener);
     public void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs);
--- a/mobile/android/base/db/TabsProvider.java
+++ b/mobile/android/base/db/TabsProvider.java
@@ -157,24 +157,23 @@ public class TabsProvider extends Shared
         trace("Calling delete in transaction on URI: " + uri);
 
         final int match = URI_MATCHER.match(uri);
         int deleted = 0;
 
         switch (match) {
             case CLIENTS_ID:
                 trace("Delete on CLIENTS_ID: " + uri);
-                selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID));
+                selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case CLIENTS:
                 trace("Delete on CLIENTS: " + uri);
                 // Delete from both TABLE_TABS and TABLE_CLIENTS.
-                deleteValues(uri, selection, selectionArgs, TABLE_TABS);
                 deleted = deleteValues(uri, selection, selectionArgs, TABLE_CLIENTS);
                 break;
 
             case TABS_ID:
                 trace("Delete on TABS_ID: " + uri);
                 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID));
                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
@@ -231,17 +230,17 @@ public class TabsProvider extends Shared
         trace("Calling update in transaction on URI: " + uri);
 
         int match = URI_MATCHER.match(uri);
         int updated = 0;
 
         switch (match) {
             case CLIENTS_ID:
                 trace("Update on CLIENTS_ID: " + uri);
-                selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID));
+                selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case CLIENTS:
                 trace("Update on CLIENTS: " + uri);
                 updated = updateValues(uri, values, selection, selectionArgs, TABLE_CLIENTS);
                 break;
 
@@ -292,17 +291,17 @@ public class TabsProvider extends Shared
                 }
 
                 qb.setProjectionMap(TABS_PROJECTION_MAP);
                 qb.setTables(TABLE_TABS + " LEFT OUTER JOIN " + TABLE_CLIENTS + " ON (" + TABLE_TABS + "." + Tabs.CLIENT_GUID + " = " + TABLE_CLIENTS + "." + Clients.GUID + ")");
                 break;
 
             case CLIENTS_ID:
                 trace("Query is on CLIENTS_ID: " + uri);
-                selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID));
+                selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case CLIENTS:
                 trace("Query is on CLIENTS: " + uri);
                 if (TextUtils.isEmpty(sortOrder)) {
                     sortOrder = DEFAULT_CLIENTS_SORT_ORDER;
                 } else {
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -140,16 +140,22 @@
 <!ENTITY overlay_share_send_tab_btn_label "Send to another device">
 <!ENTITY overlay_share_no_url "No link found in this share">
 <!ENTITY overlay_share_select_device "Select device">
 <!-- Localization note (overlay_no_synced_devices) : Used when the menu option
      to send a tab to a synced device is pressed and no other synced devices
      are found. -->
 <!ENTITY overlay_no_synced_devices "No Firefox Account connected devices found">
 
+<!-- Localization note (menu_no_synced_devices): Used in a toast when the user
+     clicks on the button to send a tab to another device and there are no
+     other devices present. This label should briefly inform the user that they
+     need another connected device in order to use the feature. -->
+<!ENTITY menu_no_synced_devices "Send this tab to another connected device">
+
 <!ENTITY pref_category_search3 "Search">
 <!ENTITY pref_category_search_summary "Customize your search providers">
 <!ENTITY pref_category_display "Display">
 <!ENTITY pref_category_display_summary "Text, title bar, full-screen browsing">
 <!ENTITY pref_category_privacy_short "Privacy">
 <!ENTITY pref_category_privacy_summary2 "Control logins, cookies, tracking, data">
 <!ENTITY pref_category_vendor "&vendorShortName;">
 <!ENTITY pref_category_vendor_summary "About &brandShortName;, FAQs, data choices">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -685,16 +685,38 @@ if CONFIG['MOZ_NATIVE_DEVICES']:
         ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']]
         resjar.generated_sources += ['com/google/android/gms/R.java']
 
     if CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR']:
         ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.cast']
         ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_RES']]
         resjar.generated_sources += ['com/google/android/gms/cast/R.java']
 
+if CONFIG['MOZ_ANDROID_GCM']:
+    gbjar.extra_jars += [
+        CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_LIB'],
+        CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'],
+        CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_LIB'],
+    ]
+
+    if CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR']:
+        ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
+        ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_RES']]
+        resjar.generated_sources += ['com/google/android/gms/R.java']
+
+    if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']:
+        ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
+        ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']]
+        resjar.generated_sources += ['com/google/android/gms/R.java']
+
+    if CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR']:
+        ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.gcm']
+        ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_RES']]
+        resjar.generated_sources += ['com/google/android/gms/gcm/R.java']
+
 gbjar.extra_jars += [CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB']]
 gbjar.extra_jars += [CONFIG['ANDROID_DESIGN_AAR_LIB']]
 gbjar.extra_jars += [CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_LIB']]
 
 gbjar.javac_flags += ['-Xlint:all,-deprecation,-fallthrough', '-J-Xmx512m', '-J-Xms128m']
 
 # gecko-thirdparty is a good place to put small independent libraries
 gtjar = add_java_jar('gecko-thirdparty')
@@ -830,21 +852,22 @@ ANDROID_ASSETS_DIRS += [
 ]
 
 # We do not expose MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN here because that
 # would leak the value to build logs.  Instead we expose the token quietly where
 # appropriate in Makefile.in.
 for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_LINKER_EXTRACT', 'MOZ_DEBUG',
             'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER',
             'MOZ_ANDROID_SHARE_OVERLAY', 'MOZ_ANDROID_DOWNLOADS_INTEGRATION', 'MOZ_INSTALL_TRACKING',
+            'MOZ_ANDROID_GCM',
             'MOZ_ANDROID_TAB_QUEUE', 'MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES'):
     if CONFIG[var]:
         DEFINES[var] = 1
 
-for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL'):
+for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL', 'MOZ_ANDROID_GCM_SENDERID'):
     if CONFIG[var]:
         DEFINES[var] = CONFIG[var]
 
 for var in ('ANDROID_PACKAGE_NAME', 'ANDROID_CPU_ARCH',
             'GRE_MILESTONE', 'MOZ_APP_BASENAME', 'MOZ_MOZILLA_API_KEY',
             'MOZ_APP_DISPLAYNAME', 'MOZ_APP_ID', 'MOZ_APP_NAME',
             'MOZ_APP_VENDOR', 'MOZ_APP_VERSION', 'MOZ_CHILD_PROCESS_NAME',
             'MOZ_ANDROID_APPLICATION_CLASS', 'MOZ_ANDROID_BROWSER_INTENT_CLASS', 'MOZ_ANDROID_SEARCH_INTENT_CLASS',
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -131,16 +131,17 @@
   <string name="overlay_share_bookmark_btn_label">&overlay_share_bookmark_btn_label;</string>
   <string name="overlay_share_reading_list_btn_label">&overlay_share_reading_list_btn_label;</string>
   <string name="overlay_share_bookmark_btn_label_already">&overlay_share_bookmark_btn_label_already;</string>
   <string name="overlay_share_reading_list_btn_label_already">&overlay_share_reading_list_btn_label_already;</string>
   <string name="overlay_share_send_tab_btn_label">&overlay_share_send_tab_btn_label;</string>
   <string name="overlay_share_no_url">&overlay_share_no_url;</string>
   <string name="overlay_share_select_device">&overlay_share_select_device;</string>
   <string name="overlay_no_synced_devices">&overlay_no_synced_devices;</string>
+  <string name="menu_no_synced_devices">&menu_no_synced_devices;</string>
 
   <string name="settings">&settings;</string>
   <string name="settings_title">&settings_title;</string>
   <string name="pref_category_input_options">&pref_category_input_options;</string>
   <string name="pref_category_advanced">&pref_category_advanced;</string>
   <string name="pref_category_customize">&pref_category_customize;</string>
   <string name="pref_category_customize_summary">&pref_category_customize_summary;</string>
   <string name="pref_category_customize_alt_summary">&pref_category_customize_alt_summary;</string>
--- a/mobile/android/base/sync/repositories/android/FennecTabsRepository.java
+++ b/mobile/android/base/sync/repositories/android/FennecTabsRepository.java
@@ -350,38 +350,34 @@ public class FennecTabsRepository extend
     } finally {
       cursor.moveToPosition(position);
     }
 
     return record;
   }
 
   /**
-   * Deletes all non-local clients and remote tabs.
-   *
-   * This function doesn't delete non-local clients due to bug in TabsProvider. Refer Bug 1025128.
-   *
-   * Upon remote tabs deletion, the clients without tabs are not shown in UI.
+   * Deletes all non-local clients and their associated remote tabs.
    */
   public static void deleteNonLocalClientsAndTabs(Context context) {
-    final String nonLocalTabsSelection = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
+    final String nonLocalClientSelection = BrowserContract.Clients.GUID + " IS NOT NULL";
 
-    ContentProviderClient tabsProvider = context.getContentResolver()
-            .acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI);
-    if (tabsProvider == null) {
-        Logger.warn(LOG_TAG, "Unable to create tabsProvider!");
+    ContentProviderClient clientsProvider = context.getContentResolver()
+            .acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
+    if (clientsProvider == null) {
+        Logger.warn(LOG_TAG, "Unable to create clientsProvider!");
         return;
     }
 
     try {
-      Logger.info(LOG_TAG, "Clearing all non-local tabs for default profile.");
-      tabsProvider.delete(BrowserContractHelpers.TABS_CONTENT_URI, nonLocalTabsSelection, null);
+      Logger.info(LOG_TAG, "Clearing all non-local clients and their associated remote tabs for default profile.");
+      clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, nonLocalClientSelection, null);
     } catch (RemoteException e) {
       Logger.warn(LOG_TAG, "Error while deleting", e);
     } finally {
       try {
-        tabsProvider.release();
+        clientsProvider.release();
       } catch (Exception e) {
-        Logger.warn(LOG_TAG, "Got exception releasing tabsProvider!", e);
+        Logger.warn(LOG_TAG, "Got exception releasing clientsProvider!", e);
       }
     }
   }
 }
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -97,16 +97,21 @@ MOZ_ANDROID_MLS_STUMBLER=1
 MOZ_ANDROID_DOWNLOADS_INTEGRATION=1
 
 # Enable Tab Queue
 MOZ_ANDROID_TAB_QUEUE=1
 
 # Use the low-memory GC tuning.
 export JS_GC_SMALL_CHUNK_SIZE=1
 
+# Enable GCM registration on Nightly builds only.
+if test "$NIGHTLY_BUILD"; then
+  MOZ_ANDROID_GCM=1
+fi
+
 # Enable Firefox Account avatars.
 MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES=1
 
 # Enable checking that add-ons are signed by the trusted root
 MOZ_ADDON_SIGNING=1
 
 # Enable the Switchboard A/B framework code.
 # Note: The framework is always included in the app. This flag controls
--- a/mobile/android/gradle/app/build.gradle
+++ b/mobile/android/gradle/app/build.gradle
@@ -123,16 +123,19 @@ spoon {
     debug = true
     // It's not helpful to pass when we don't have a device connected.
     failIfNoDeviceConnected = true
 
     def spoonPackageName
     if (gradle.startParameter.taskNames.contains('runBrowserTests')) {
         spoonPackageName = 'org.mozilla.tests.browser.junit3'
     }
+    if (gradle.startParameter.taskNames.contains('runBackgroundTests')) {
+        spoonPackageName = 'org.mozilla.gecko.background'
+    }
     if (project.hasProperty('spoonPackageName')) {
         // Command line overrides everything.
         spoonPackageName = project.spoonPackageName
     }
     if (spoonPackageName) {
         instrumentationArgs = ['-e', "package=${spoonPackageName}".toString()]
     }
 }
@@ -142,9 +145,12 @@ afterEvaluate {
     tasks["spoon${android.testBuildType.capitalize()}AndroidTest"].outputs.upToDateWhen { false }
 
     // This is an awkward way to define different sets of instrumentation tests.
     // The task name itself is fished at runtime and the package name configured
     // in the spoon configuration.
     task runBrowserTests {
         dependsOn tasks["spoonDebugAndroidTest"]
     }
+    task runBackgroundTests {
+        dependsOn tasks["spoonDebugAndroidTest"]
+    }
 }
--- a/mobile/android/gradle/base/build.gradle
+++ b/mobile/android/gradle/base/build.gradle
@@ -100,16 +100,22 @@ dependencies {
 
     if (mozconfig.substs.MOZ_NATIVE_DEVICES) {
         compile 'com.android.support:mediarouter-v7:23.0.1'
         compile 'com.google.android.gms:play-services-basement:8.1.0'
         compile 'com.google.android.gms:play-services-base:8.1.0'
         compile 'com.google.android.gms:play-services-cast:8.1.0'
     }
 
+    if (mozconfig.substs.MOZ_ANDROID_GCM) {
+        compile 'com.google.android.gms:play-services-basement:8.1.0'
+        compile 'com.google.android.gms:play-services-base:8.1.0'
+        compile 'com.google.android.gms:play-services-gcm:8.1.0'
+    }
+
     compile project(':thirdparty')
 
     testCompile 'junit:junit:4.12'
     testCompile 'org.robolectric:robolectric:3.0'
     testCompile 'org.simpleframework:simple-http:4.1.13'
 }
 
 apply plugin: 'idea'
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -112,17 +112,17 @@ class MachCommands(MachCommandBase):
         srcdir('omnijar/src/main/java/themes', 'mobile/android/themes')
 
         srcdir('app/build.gradle', 'mobile/android/gradle/app/build.gradle')
         srcdir('app/src/androidTest/res', 'build/mobile/robocop/res')
         srcdir('app/src/androidTest/assets', 'mobile/android/tests/browser/robocop/assets')
         # Test code.
         srcdir('app/src/robocop_harness/org/mozilla/gecko', 'build/mobile/robocop')
         srcdir('app/src/robocop/org/mozilla/gecko/tests', 'mobile/android/tests/browser/robocop')
-        srcdir('app/src/background/org/mozilla/gecko/background', 'mobile/android/tests/background/junit3/src')
+        srcdir('app/src/background', 'mobile/android/tests/background/junit3/src')
         srcdir('app/src/browser', 'mobile/android/tests/browser/junit3/src')
         srcdir('app/src/javaaddons', 'mobile/android/tests/javaaddons/src')
         # Test libraries.
         srcdir('app/libs', 'build/mobile/robocop')
 
         srcdir('base/build.gradle', 'mobile/android/gradle/base/build.gradle')
         srcdir('base/lint.xml', 'mobile/android/gradle/base/lint.xml')
         srcdir('base/src/main/AndroidManifest.xml', 'mobile/android/gradle/base/AndroidManifest.xml')
--- a/mobile/android/modules/Accounts.jsm
+++ b/mobile/android/modules/Accounts.jsm
@@ -153,10 +153,22 @@ var Accounts = Object.freeze({
    * It is an error if no Android Account exists.
    *
    * Returns a Promise that resolves to a boolean indicating success.
    */
   deleteFirefoxAccount: function () {
     return Messaging.sendRequestForResult({
       type: "Accounts:DeleteFirefoxAccount",
     });
+  },
+
+  showSyncPreferences: function () {
+    // Only show Sync preferences of an existing Android Account.
+    return Accounts.getFirefoxAccount().then(account => {
+      if (!account) {
+        throw new Error("Can't show Sync preferences of non-existent Firefox Account!");
+      }
+      return Messaging.sendRequestForResult({
+        type: "Accounts:ShowSyncPreferences"
+      });
+    });
   }
 });
--- a/mobile/android/modules/FxAccountsWebChannel.jsm
+++ b/mobile/android/modules/FxAccountsWebChannel.jsm
@@ -25,16 +25,17 @@ const log = Cu.import("resource://gre/mo
 const WEBCHANNEL_ID = "account_updates";
 
 const COMMAND_LOADED               = "fxaccounts:loaded";
 const COMMAND_CAN_LINK_ACCOUNT     = "fxaccounts:can_link_account";
 const COMMAND_LOGIN                = "fxaccounts:login";
 const COMMAND_CHANGE_PASSWORD      = "fxaccounts:change_password";
 const COMMAND_DELETE_ACCOUNT       = "fxaccounts:delete_account";
 const COMMAND_PROFILE_CHANGE       = "profile:change";
+const COMMAND_SYNC_PREFERENCES     = "fxaccounts:sync_preferences";
 
 const PREF_LAST_FXA_USER           = "identity.fxaccounts.lastSignedInUserHash";
 
 XPCOMUtils.defineLazyGetter(this, "strings",
                             () => Services.strings.createBundle("chrome://browser/locale/aboutAccounts.properties")); /*global strings */
 
 Object.defineProperty(this, "NativeWindow",
                       { get: () => Services.wm.getMostRecentWindow("navigator:browser").NativeWindow }); /*global NativeWindow */
@@ -345,16 +346,23 @@ this.FxAccountsWebChannel.prototype = {
               }
               return Accounts.notifyFirefoxAccountProfileChanged();
             })
             .catch(e => {
               log.e(e.toString());
             });
             break;
 
+          case COMMAND_SYNC_PREFERENCES:
+            Accounts.showSyncPreferences()
+            .catch(e => {
+              log.e(e.toString());
+            });
+            break;
+
           default:
             log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command));
             break;
         }
       }
     };
 
     this._channelCallback = listener;
--- a/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild
+++ b/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild
@@ -1,111 +1,111 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 background_junit3_sources = [
-    'src/common/TestAndroidLogWriters.java',
-    'src/common/TestBrowserContractHelpers.java',
-    'src/common/TestDateUtils.java',
-    'src/common/TestUtils.java',
-    'src/common/TestWaitHelper.java',
-    'src/db/AndroidBrowserRepositoryTestCase.java',
-    'src/db/TestAndroidBrowserBookmarksRepository.java',
-    'src/db/TestAndroidBrowserHistoryDataExtender.java',
-    'src/db/TestAndroidBrowserHistoryRepository.java',
-    'src/db/TestBookmarks.java',
-    'src/db/TestCachedSQLiteOpenHelper.java',
-    'src/db/TestClientsDatabase.java',
-    'src/db/TestClientsDatabaseAccessor.java',
-    'src/db/TestFennecTabsRepositorySession.java',
-    'src/db/TestFennecTabsStorage.java',
-    'src/db/TestFormHistoryRepositorySession.java',
-    'src/db/TestPasswordsRepository.java',
-    'src/fxa/authenticator/TestAccountPickler.java',
-    'src/fxa/TestAccountLoader.java',
-    'src/fxa/TestBrowserIDKeyPairGeneration.java',
-    'src/fxa/TestFirefoxAccounts.java',
-    'src/healthreport/MockDatabaseEnvironment.java',
-    'src/healthreport/MockHealthReportDatabaseStorage.java',
-    'src/healthreport/MockHealthReportSQLiteOpenHelper.java',
-    'src/healthreport/MockProfileInformationCache.java',
-    'src/healthreport/prune/TestHealthReportPruneService.java',
-    'src/healthreport/prune/TestPrunePolicyDatabaseStorage.java',
-    'src/healthreport/TestEnvironmentBuilder.java',
-    'src/healthreport/TestEnvironmentV1HashAppender.java',
-    'src/healthreport/TestHealthReportBroadcastService.java',
-    'src/healthreport/TestHealthReportDatabaseStorage.java',
-    'src/healthreport/TestHealthReportGenerator.java',
-    'src/healthreport/TestHealthReportProvider.java',
-    'src/healthreport/TestHealthReportSQLiteOpenHelper.java',
-    'src/healthreport/TestProfileInformationCache.java',
-    'src/healthreport/upload/TestAndroidSubmissionClient.java',
-    'src/healthreport/upload/TestHealthReportUploadService.java',
-    'src/helpers/AndroidSyncTestCase.java',
-    'src/helpers/BackgroundServiceTestCase.java',
-    'src/helpers/DBHelpers.java',
-    'src/helpers/DBProviderTestCase.java',
-    'src/helpers/FakeProfileTestCase.java',
-    'src/nativecode/test/TestNativeCrypto.java',
-    'src/sync/AndroidSyncTestCaseWithAccounts.java',
-    'src/sync/helpers/BookmarkHelpers.java',
-    'src/sync/helpers/DefaultBeginDelegate.java',
-    'src/sync/helpers/DefaultCleanDelegate.java',
-    'src/sync/helpers/DefaultDelegate.java',
-    'src/sync/helpers/DefaultFetchDelegate.java',
-    'src/sync/helpers/DefaultFinishDelegate.java',
-    'src/sync/helpers/DefaultGuidsSinceDelegate.java',
-    'src/sync/helpers/DefaultSessionCreationDelegate.java',
-    'src/sync/helpers/DefaultStoreDelegate.java',
-    'src/sync/helpers/ExpectBeginDelegate.java',
-    'src/sync/helpers/ExpectBeginFailDelegate.java',
-    'src/sync/helpers/ExpectFetchDelegate.java',
-    'src/sync/helpers/ExpectFetchSinceDelegate.java',
-    'src/sync/helpers/ExpectFinishDelegate.java',
-    'src/sync/helpers/ExpectFinishFailDelegate.java',
-    'src/sync/helpers/ExpectGuidsSinceDelegate.java',
-    'src/sync/helpers/ExpectInvalidRequestFetchDelegate.java',
-    'src/sync/helpers/ExpectInvalidTypeStoreDelegate.java',
-    'src/sync/helpers/ExpectManyStoredDelegate.java',
-    'src/sync/helpers/ExpectNoGUIDsSinceDelegate.java',
-    'src/sync/helpers/ExpectNoStoreDelegate.java',
-    'src/sync/helpers/ExpectStoreCompletedDelegate.java',
-    'src/sync/helpers/ExpectStoredDelegate.java',
-    'src/sync/helpers/HistoryHelpers.java',
-    'src/sync/helpers/PasswordHelpers.java',
-    'src/sync/helpers/SessionTestHelper.java',
-    'src/sync/helpers/SimpleSuccessBeginDelegate.java',
-    'src/sync/helpers/SimpleSuccessCreationDelegate.java',
-    'src/sync/helpers/SimpleSuccessFetchDelegate.java',
-    'src/sync/helpers/SimpleSuccessFinishDelegate.java',
-    'src/sync/helpers/SimpleSuccessStoreDelegate.java',
-    'src/sync/TestAccountPickler.java',
-    'src/sync/TestClientsStage.java',
-    'src/sync/TestConfigurationMigrator.java',
-    'src/sync/TestResetting.java',
-    'src/sync/TestSendTabData.java',
-    'src/sync/TestStoreTracking.java',
-    'src/sync/TestSyncAccounts.java',
-    'src/sync/TestSyncAuthenticatorService.java',
-    'src/sync/TestSyncConfiguration.java',
-    'src/sync/TestTabsRecord.java',
-    'src/sync/TestUpgradeRequired.java',
-    'src/sync/TestWebURLFinder.java',
-    'src/telemetry/TestTelemetryRecorder.java',
-    'src/testhelpers/BaseMockServerSyncStage.java',
-    'src/testhelpers/CommandHelpers.java',
-    'src/testhelpers/DefaultGlobalSessionCallback.java',
-    'src/testhelpers/JPakeNumGeneratorFixed.java',
-    'src/testhelpers/MockAbstractNonRepositorySyncStage.java',
-    'src/testhelpers/MockClientsDatabaseAccessor.java',
-    'src/testhelpers/MockClientsDataDelegate.java',
-    'src/testhelpers/MockGlobalSession.java',
-    'src/testhelpers/MockPrefsGlobalSession.java',
-    'src/testhelpers/MockRecord.java',
-    'src/testhelpers/MockServerSyncStage.java',
-    'src/testhelpers/MockSharedPreferences.java',
-    'src/testhelpers/StubDelegate.java',
-    'src/testhelpers/WaitHelper.java',
-    'src/testhelpers/WBORepository.java',
+    'src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java',
+    'src/org/mozilla/gecko/background/common/TestBrowserContractHelpers.java',
+    'src/org/mozilla/gecko/background/common/TestDateUtils.java',
+    'src/org/mozilla/gecko/background/common/TestUtils.java',
+    'src/org/mozilla/gecko/background/common/TestWaitHelper.java',
+    'src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java',
+    'src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java',
+    'src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java',
+    'src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java',
+    'src/org/mozilla/gecko/background/db/TestBookmarks.java',
+    'src/org/mozilla/gecko/background/db/TestCachedSQLiteOpenHelper.java',
+    'src/org/mozilla/gecko/background/db/TestClientsDatabase.java',
+    'src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java',
+    'src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java',
+    'src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java',
+    'src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java',
+    'src/org/mozilla/gecko/background/db/TestPasswordsRepository.java',
+    'src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java',
+    'src/org/mozilla/gecko/background/fxa/TestAccountLoader.java',
+    'src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java',
+    'src/org/mozilla/gecko/background/fxa/TestFirefoxAccounts.java',
+    'src/org/mozilla/gecko/background/healthreport/MockDatabaseEnvironment.java',
+    'src/org/mozilla/gecko/background/healthreport/MockHealthReportDatabaseStorage.java',
+    'src/org/mozilla/gecko/background/healthreport/MockHealthReportSQLiteOpenHelper.java',
+    'src/org/mozilla/gecko/background/healthreport/MockProfileInformationCache.java',
+    'src/org/mozilla/gecko/background/healthreport/prune/TestHealthReportPruneService.java',
+    'src/org/mozilla/gecko/background/healthreport/prune/TestPrunePolicyDatabaseStorage.java',
+    'src/org/mozilla/gecko/background/healthreport/TestEnvironmentBuilder.java',
+    'src/org/mozilla/gecko/background/healthreport/TestEnvironmentV1HashAppender.java',
+    'src/org/mozilla/gecko/background/healthreport/TestHealthReportBroadcastService.java',
+    'src/org/mozilla/gecko/background/healthreport/TestHealthReportDatabaseStorage.java',
+    'src/org/mozilla/gecko/background/healthreport/TestHealthReportGenerator.java',
+    'src/org/mozilla/gecko/background/healthreport/TestHealthReportProvider.java',
+    'src/org/mozilla/gecko/background/healthreport/TestHealthReportSQLiteOpenHelper.java',
+    'src/org/mozilla/gecko/background/healthreport/TestProfileInformationCache.java',
+    'src/org/mozilla/gecko/background/healthreport/upload/TestAndroidSubmissionClient.java',
+    'src/org/mozilla/gecko/background/healthreport/upload/TestHealthReportUploadService.java',
+    'src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java',
+    'src/org/mozilla/gecko/background/helpers/BackgroundServiceTestCase.java',
+    'src/org/mozilla/gecko/background/helpers/DBHelpers.java',
+    'src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java',
+    'src/org/mozilla/gecko/background/helpers/FakeProfileTestCase.java',
+    'src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java',
+    'src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java',
+    'src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java',
+    'src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java',
+    'src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java',
+    'src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java',
+    'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java',
+    'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java',
+    'src/org/mozilla/gecko/background/sync/TestAccountPickler.java',
+    'src/org/mozilla/gecko/background/sync/TestClientsStage.java',
+    'src/org/mozilla/gecko/background/sync/TestConfigurationMigrator.java',
+    'src/org/mozilla/gecko/background/sync/TestResetting.java',
+    'src/org/mozilla/gecko/background/sync/TestSendTabData.java',
+    'src/org/mozilla/gecko/background/sync/TestStoreTracking.java',
+    'src/org/mozilla/gecko/background/sync/TestSyncAccounts.java',
+    'src/org/mozilla/gecko/background/sync/TestSyncAuthenticatorService.java',
+    'src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java',
+    'src/org/mozilla/gecko/background/sync/TestTabsRecord.java',
+    'src/org/mozilla/gecko/background/sync/TestUpgradeRequired.java',
+    'src/org/mozilla/gecko/background/sync/TestWebURLFinder.java',
+    'src/org/mozilla/gecko/background/telemetry/TestTelemetryRecorder.java',
+    'src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java',
+    'src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java',
+    'src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java',
+    'src/org/mozilla/gecko/background/testhelpers/JPakeNumGeneratorFixed.java',
+    'src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java',
+    'src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java',
+    'src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java',
+    'src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java',
+    'src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java',
+    'src/org/mozilla/gecko/background/testhelpers/MockRecord.java',
+    'src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java',
+    'src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java',
+    'src/org/mozilla/gecko/background/testhelpers/StubDelegate.java',
+    'src/org/mozilla/gecko/background/testhelpers/WaitHelper.java',
+    'src/org/mozilla/gecko/background/testhelpers/WBORepository.java',
 ]
--- a/mobile/android/tests/background/junit3/instrumentation.ini
+++ b/mobile/android/tests/background/junit3/instrumentation.ini
@@ -1,47 +1,47 @@
 [DEFAULT]
 subsuite = background
 
-[src/common/TestAndroidLogWriters.java]
-[src/common/TestBrowserContractHelpers.java]
-[src/common/TestDateUtils.java]
-[src/common/TestUtils.java]
-[src/common/TestWaitHelper.java]
-[src/db/TestAndroidBrowserBookmarksRepository.java]
-[src/db/TestAndroidBrowserHistoryDataExtender.java]
-[src/db/TestAndroidBrowserHistoryRepository.java]
-[src/db/TestBookmarks.java]
-[src/db/TestCachedSQLiteOpenHelper.java]
-[src/db/TestClientsDatabase.java]
-[src/db/TestClientsDatabaseAccessor.java]
-[src/db/TestFennecTabsRepositorySession.java]
-[src/db/TestFennecTabsStorage.java]
-[src/db/TestFormHistoryRepositorySession.java]
-[src/db/TestPasswordsRepository.java]
-[src/fxa/TestBrowserIDKeyPairGeneration.java]
-[src/fxa/authenticator/TestAccountPickler.java]
-[src/healthreport/TestEnvironmentBuilder.java]
-[src/healthreport/TestEnvironmentV1HashAppender.java]
-[src/healthreport/TestHealthReportBroadcastService.java]
-[src/healthreport/TestHealthReportDatabaseStorage.java]
-[src/healthreport/TestHealthReportGenerator.java]
-[src/healthreport/TestHealthReportProvider.java]
-[src/healthreport/TestHealthReportSQLiteOpenHelper.java]
-[src/healthreport/TestProfileInformationCache.java]
-[src/healthreport/prune/TestHealthReportPruneService.java]
-[src/healthreport/prune/TestPrunePolicyDatabaseStorage.java]
-[src/healthreport/upload/TestAndroidSubmissionClient.java]
-[src/healthreport/upload/TestHealthReportUploadService.java]
-[src/nativecode/test/TestNativeCrypto.java]
-[src/sync/TestAccountPickler.java]
-[src/sync/TestClientsStage.java]
-[src/sync/TestConfigurationMigrator.java]
-[src/sync/TestResetting.java]
-[src/sync/TestSendTabData.java]
-[src/sync/TestStoreTracking.java]
-[src/sync/TestSyncAccounts.java]
-[src/sync/TestSyncAuthenticatorService.java]
-[src/sync/TestSyncConfiguration.java]
-[src/sync/TestTabsRecord.java]
-[src/sync/TestUpgradeRequired.java]
-[src/sync/TestWebURLFinder.java]
-[src/telemetry/TestTelemetryRecorder.java]
+[src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java]
+[src/org/mozilla/gecko/background/common/TestBrowserContractHelpers.java]
+[src/org/mozilla/gecko/background/common/TestDateUtils.java]
+[src/org/mozilla/gecko/background/common/TestUtils.java]
+[src/org/mozilla/gecko/background/common/TestWaitHelper.java]
+[src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java]
+[src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java]
+[src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java]
+[src/org/mozilla/gecko/background/db/TestBookmarks.java]
+[src/org/mozilla/gecko/background/db/TestCachedSQLiteOpenHelper.java]
+[src/org/mozilla/gecko/background/db/TestClientsDatabase.java]
+[src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java]
+[src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java]
+[src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java]
+[src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java]
+[src/org/mozilla/gecko/background/db/TestPasswordsRepository.java]
+[src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java]
+[src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java]
+[src/org/mozilla/gecko/background/healthreport/TestEnvironmentBuilder.java]
+[src/org/mozilla/gecko/background/healthreport/TestEnvironmentV1HashAppender.java]
+[src/org/mozilla/gecko/background/healthreport/TestHealthReportBroadcastService.java]
+[src/org/mozilla/gecko/background/healthreport/TestHealthReportDatabaseStorage.java]
+[src/org/mozilla/gecko/background/healthreport/TestHealthReportGenerator.java]
+[src/org/mozilla/gecko/background/healthreport/TestHealthReportProvider.java]
+[src/org/mozilla/gecko/background/healthreport/TestHealthReportSQLiteOpenHelper.java]
+[src/org/mozilla/gecko/background/healthreport/TestProfileInformationCache.java]
+[src/org/mozilla/gecko/background/healthreport/prune/TestHealthReportPruneService.java]
+[src/org/mozilla/gecko/background/healthreport/prune/TestPrunePolicyDatabaseStorage.java]
+[src/org/mozilla/gecko/background/healthreport/upload/TestAndroidSubmissionClient.java]
+[src/org/mozilla/gecko/background/healthreport/upload/TestHealthReportUploadService.java]
+[src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java]
+[src/org/mozilla/gecko/background/sync/TestAccountPickler.java]
+[src/org/mozilla/gecko/background/sync/TestClientsStage.java]
+[src/org/mozilla/gecko/background/sync/TestConfigurationMigrator.java]
+[src/org/mozilla/gecko/background/sync/TestResetting.java]
+[src/org/mozilla/gecko/background/sync/TestSendTabData.java]
+[src/org/mozilla/gecko/background/sync/TestStoreTracking.java]
+[src/org/mozilla/gecko/background/sync/TestSyncAccounts.java]
+[src/org/mozilla/gecko/background/sync/TestSyncAuthenticatorService.java]
+[src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java]
+[src/org/mozilla/gecko/background/sync/TestTabsRecord.java]
+[src/org/mozilla/gecko/background/sync/TestUpgradeRequired.java]
+[src/org/mozilla/gecko/background/sync/TestWebURLFinder.java]
+[src/org/mozilla/gecko/background/telemetry/TestTelemetryRecorder.java]
rename from mobile/android/tests/background/junit3/src/common/TestAndroidLogWriters.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java
rename from mobile/android/tests/background/junit3/src/common/TestBrowserContractHelpers.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestBrowserContractHelpers.java
rename from mobile/android/tests/background/junit3/src/common/TestDateUtils.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestDateUtils.java
rename from mobile/android/tests/background/junit3/src/common/TestUtils.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestUtils.java
rename from mobile/android/tests/background/junit3/src/common/TestWaitHelper.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestWaitHelper.java
rename from mobile/android/tests/background/junit3/src/db/AndroidBrowserRepositoryTestCase.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java
rename from mobile/android/tests/background/junit3/src/db/TestAndroidBrowserBookmarksRepository.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java
rename from mobile/android/tests/background/junit3/src/db/TestAndroidBrowserHistoryDataExtender.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java
rename from mobile/android/tests/background/junit3/src/db/TestAndroidBrowserHistoryRepository.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java
rename from mobile/android/tests/background/junit3/src/db/TestBookmarks.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java
rename from mobile/android/tests/background/junit3/src/db/TestCachedSQLiteOpenHelper.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestCachedSQLiteOpenHelper.java
rename from mobile/android/tests/background/junit3/src/db/TestClientsDatabase.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabase.java
rename from mobile/android/tests/background/junit3/src/db/TestClientsDatabaseAccessor.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java
rename from mobile/android/tests/background/junit3/src/db/TestFennecTabsRepositorySession.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java
--- a/mobile/android/tests/background/junit3/src/db/TestFennecTabsRepositorySession.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java
@@ -36,31 +36,47 @@ public class TestFennecTabsRepositorySes
   // Override these to test against data that is not live.
   public static final String TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS ?";
   public static final String[] TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS = new String[] { TEST_CLIENT_GUID };
 
   public static final String TEST_CLIENTS_GUID_IS_LOCAL_SELECTION = BrowserContract.Clients.GUID + " IS ?";
   public static final String[] TEST_CLIENTS_GUID_IS_LOCAL_SELECTION_ARGS = new String[] { TEST_CLIENT_GUID };
 
   protected ContentProviderClient tabsClient = null;
+  protected ContentProviderClient clientsClient = null;
 
   protected ContentProviderClient getTabsClient() {
     final ContentResolver cr = getApplicationContext().getContentResolver();
     return cr.acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI);
   }
 
+  protected ContentProviderClient getClientsClient() {
+    final ContentResolver cr = getApplicationContext().getContentResolver();
+    return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
+  }
+
   public TestFennecTabsRepositorySession() throws NoContentProviderException {
     super();
   }
 
   @Override
   public void setUp() {
     if (tabsClient == null) {
       tabsClient = getTabsClient();
     }
+    if (clientsClient == null) {
+      clientsClient = getClientsClient();
+    }
+  }
+
+  protected int deleteTestClient(final ContentProviderClient clientsClient) throws RemoteException {
+    if (clientsClient == null) {
+      return -1;
+    }
+    return clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, TEST_CLIENTS_GUID_IS_LOCAL_SELECTION, TEST_CLIENTS_GUID_IS_LOCAL_SELECTION_ARGS);
   }
 
   protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException {
     if (tabsClient == null) {
       return -1;
     }
     return tabsClient.delete(BrowserContractHelpers.TABS_CONTENT_URI,
         TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION, TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS);
@@ -69,16 +85,23 @@ public class TestFennecTabsRepositorySes
   @Override
   protected void tearDown() throws Exception {
     if (tabsClient != null) {
       deleteAllTestTabs(tabsClient);
 
       tabsClient.release();
       tabsClient = null;
     }
+
+    if (clientsClient != null) {
+      deleteTestClient(clientsClient);
+
+      clientsClient.release();
+      clientsClient = null;
+    }
   }
 
   protected FennecTabsRepository getRepository() {
     /**
      * Override this chain in order to avoid our test code having to create two
      * sessions all the time.
      */
     return new FennecTabsRepository(clientsDataDelegate) {
@@ -224,24 +247,22 @@ public class TestFennecTabsRepositorySes
   }
 
   // Verify that storing a tabs record writes a clients record with the correct
   // device type to the Fennec clients provider.
   public void testStore() throws NoContentProviderException, RemoteException {
     // Get a valid tabsRecord to write.
     final TabsRecord tabsRecord = insertTestTabsAndExtractTabsRecord();
     deleteAllTestTabs(tabsClient);
+    deleteTestClient(clientsClient);
 
     final ContentResolver cr = getApplicationContext().getContentResolver();
     final ContentProviderClient clientsClient = cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
 
     try {
-      // We can't delete only our test clients due to a Fennec CP issue with guid vs. client_guid.
-      clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, null, null);
-
       // This clients DB is not the Fennec DB; it's Sync's own clients DB.
       final ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(getApplicationContext());
       try {
         ClientRecord clientRecord = new ClientRecord(TEST_CLIENT_GUID);
         clientRecord.name = TEST_CLIENT_NAME;
         clientRecord.type = TEST_CLIENT_DEVICE_TYPE;
         db.store(clientRecord);
       } finally {
rename from mobile/android/tests/background/junit3/src/db/TestFennecTabsStorage.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java
--- a/mobile/android/tests/background/junit3/src/db/TestFennecTabsStorage.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java
@@ -21,16 +21,17 @@ import android.test.ActivityInstrumentat
  *
  * @author rnewman
  *
  */
 public class TestFennecTabsStorage extends ActivityInstrumentationTestCase2<Activity> {
   public static final String TEST_CLIENT_GUID = "test guid"; // Real GUIDs never contain spaces.
   public static final String TEST_CLIENT_NAME = "test client name";
 
+  public static final String CLIENTS_GUID_IS = BrowserContract.Clients.GUID + " = ?";
   public static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?";
 
   protected Tab testTab1;
   protected Tab testTab2;
   protected Tab testTab3;
 
   public TestFennecTabsStorage() {
     super(Activity.class);
@@ -41,28 +42,42 @@ public class TestFennecTabsStorage exten
     return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
   }
 
   protected ContentProviderClient getTabsClient() {
     final ContentResolver cr = getInstrumentation().getTargetContext().getApplicationContext().getContentResolver();
     return cr.acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI);
   }
 
+  protected int deleteTestClient(final ContentProviderClient clientsClient) throws RemoteException {
+    if (clientsClient == null) {
+      return -1;
+    }
+    return clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String[] { TEST_CLIENT_GUID });
+  }
+
   protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException {
     if (tabsClient == null) {
       return -1;
     }
-    return tabsClient.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID });
+    return tabsClient.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, new String[]{TEST_CLIENT_GUID});
   }
 
   @Override
   protected void tearDown() throws Exception {
     deleteAllTestTabs(getTabsClient());
   }
 
+  protected void insertTestClient(final ContentProviderClient clientsClient) throws RemoteException {
+    ContentValues cv = new ContentValues();
+    cv.put(BrowserContract.Clients.GUID, TEST_CLIENT_GUID);
+    cv.put(BrowserContract.Clients.NAME, TEST_CLIENT_NAME);
+    clientsClient.insert(BrowserContractHelpers.CLIENTS_CONTENT_URI, cv);
+  }
+
   @SuppressWarnings("unchecked")
   protected void insertSomeTestTabs(ContentProviderClient tabsClient) throws RemoteException {
     final JSONArray history1 = new JSONArray();
     history1.add("http://test.com/test1.html");
     testTab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000);
 
     final JSONArray history2 = new JSONArray();
     history2.add("http://test.com/test2.html#1");
@@ -164,18 +179,21 @@ public class TestFennecTabsStorage exten
     }
 
     int deleted = clientsClient.delete(uri, null, null);
     assertEquals(2, deleted);
   }
 
   public void testTabFromCursor() throws Exception {
     final ContentProviderClient tabsClient = getTabsClient();
+    final ContentProviderClient clientsClient = getClientsClient();
 
     deleteAllTestTabs(tabsClient);
+    deleteTestClient(clientsClient);
+    insertTestClient(clientsClient);
     insertSomeTestTabs(tabsClient);
 
     final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
     Cursor cursor = null;
     try {
       cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
       assertEquals(3, cursor.getCount());
 
@@ -189,9 +207,31 @@ public class TestFennecTabsStorage exten
 
       cursor.moveToPosition(2);
       final Tab parsed3 = Tab.fromCursor(cursor);
       assertEquals(testTab3, parsed3);
     } finally {
       cursor.close();
     }
   }
+
+  public void testDeletingClientDeletesTabs() throws Exception {
+    final ContentProviderClient tabsClient = getTabsClient();
+    final ContentProviderClient clientsClient = getClientsClient();
+
+    deleteAllTestTabs(tabsClient);
+    deleteTestClient(clientsClient);
+    insertTestClient(clientsClient);
+    insertSomeTestTabs(tabsClient);
+
+    // Delete just the client...
+    clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String [] { TEST_CLIENT_GUID });
+
+    Cursor cursor = null;
+    try {
+      cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, null);
+      // ... and all that client's tabs should be removed.
+      assertEquals(0, cursor.getCount());
+    } finally {
+      cursor.close();
+    }
+  }
 }
rename from mobile/android/tests/background/junit3/src/db/TestFormHistoryRepositorySession.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java
rename from mobile/android/tests/background/junit3/src/db/TestPasswordsRepository.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestPasswordsRepository.java
rename from mobile/android/tests/background/junit3/src/fxa/TestAccountLoader.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestAccountLoader.java
rename from mobile/android/tests/background/junit3/src/fxa/TestBrowserIDKeyPairGeneration.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java
rename from mobile/android/tests/background/junit3/src/fxa/TestFirefoxAccounts.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestFirefoxAccounts.java
rename from mobile/android/tests/background/junit3/src/fxa/authenticator/TestAccountPickler.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java
rename from mobile/android/tests/background/junit3/src/healthreport/MockDatabaseEnvironment.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/MockDatabaseEnvironment.java
rename from mobile/android/tests/background/junit3/src/healthreport/MockHealthReportDatabaseStorage.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/MockHealthReportDatabaseStorage.java
rename from mobile/android/tests/background/junit3/src/healthreport/MockHealthReportSQLiteOpenHelper.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/MockHealthReportSQLiteOpenHelper.java
rename from mobile/android/tests/background/junit3/src/healthreport/MockProfileInformationCache.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/MockProfileInformationCache.java
rename from mobile/android/tests/background/junit3/src/healthreport/TestEnvironmentBuilder.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/TestEnvironmentBuilder.java
rename from mobile/android/tests/background/junit3/src/healthreport/TestEnvironmentV1HashAppender.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/TestEnvironmentV1HashAppender.java
rename from mobile/android/tests/background/junit3/src/healthreport/TestHealthReportBroadcastService.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/TestHealthReportBroadcastService.java
rename from mobile/android/tests/background/junit3/src/healthreport/TestHealthReportDatabaseStorage.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/TestHealthReportDatabaseStorage.java
rename from mobile/android/tests/background/junit3/src/healthreport/TestHealthReportGenerator.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/TestHealthReportGenerator.java
rename from mobile/android/tests/background/junit3/src/healthreport/TestHealthReportProvider.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/TestHealthReportProvider.java
rename from mobile/android/tests/background/junit3/src/healthreport/TestHealthReportSQLiteOpenHelper.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/TestHealthReportSQLiteOpenHelper.java
rename from mobile/android/tests/background/junit3/src/healthreport/TestProfileInformationCache.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/TestProfileInformationCache.java
rename from mobile/android/tests/background/junit3/src/healthreport/prune/TestHealthReportPruneService.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/prune/TestHealthReportPruneService.java
rename from mobile/android/tests/background/junit3/src/healthreport/prune/TestPrunePolicyDatabaseStorage.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/prune/TestPrunePolicyDatabaseStorage.java
rename from mobile/android/tests/background/junit3/src/healthreport/upload/TestAndroidSubmissionClient.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/upload/TestAndroidSubmissionClient.java
rename from mobile/android/tests/background/junit3/src/healthreport/upload/TestHealthReportUploadService.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/healthreport/upload/TestHealthReportUploadService.java
rename from mobile/android/tests/background/junit3/src/helpers/AndroidSyncTestCase.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java
rename from mobile/android/tests/background/junit3/src/helpers/BackgroundServiceTestCase.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/BackgroundServiceTestCase.java
rename from mobile/android/tests/background/junit3/src/helpers/DBHelpers.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBHelpers.java
rename from mobile/android/tests/background/junit3/src/helpers/DBProviderTestCase.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java
rename from mobile/android/tests/background/junit3/src/helpers/FakeProfileTestCase.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/FakeProfileTestCase.java
rename from mobile/android/tests/background/junit3/src/nativecode/test/TestNativeCrypto.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java
rename from mobile/android/tests/background/junit3/src/sync/AndroidSyncTestCaseWithAccounts.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java
rename from mobile/android/tests/background/junit3/src/sync/TestAccountPickler.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestAccountPickler.java
rename from mobile/android/tests/background/junit3/src/sync/TestClientsStage.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java
rename from mobile/android/tests/background/junit3/src/sync/TestConfigurationMigrator.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestConfigurationMigrator.java
rename from mobile/android/tests/background/junit3/src/sync/TestResetting.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java
rename from mobile/android/tests/background/junit3/src/sync/TestSendTabData.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSendTabData.java
rename from mobile/android/tests/background/junit3/src/sync/TestStoreTracking.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java
rename from mobile/android/tests/background/junit3/src/sync/TestSyncAccounts.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncAccounts.java
rename from mobile/android/tests/background/junit3/src/sync/TestSyncAuthenticatorService.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncAuthenticatorService.java
rename from mobile/android/tests/background/junit3/src/sync/TestSyncConfiguration.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java
rename from mobile/android/tests/background/junit3/src/sync/TestTabsRecord.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestTabsRecord.java
--- a/mobile/android/tests/background/junit3/src/sync/TestTabsRecord.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestTabsRecord.java
@@ -13,16 +13,17 @@ import org.mozilla.gecko.sync.repositori
 import android.content.ContentProviderClient;
 import android.database.Cursor;
 
 public class TestTabsRecord extends TestFennecTabsStorage {
   public void testTabsRecordFromCursor() throws Exception {
     final ContentProviderClient tabsClient = getTabsClient();
 
     deleteAllTestTabs(tabsClient);
+    insertTestClient(getClientsClient());
     insertSomeTestTabs(tabsClient);
 
     final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
     Cursor cursor = null;
     try {
       cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
       assertEquals(3, cursor.getCount());
 
rename from mobile/android/tests/background/junit3/src/sync/TestUpgradeRequired.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestUpgradeRequired.java
rename from mobile/android/tests/background/junit3/src/sync/TestWebURLFinder.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestWebURLFinder.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/BookmarkHelpers.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/DefaultBeginDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/DefaultCleanDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/DefaultDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/DefaultFetchDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/DefaultFinishDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/DefaultGuidsSinceDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/DefaultSessionCreationDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/DefaultStoreDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectBeginDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectBeginFailDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectFetchDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectFetchSinceDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectFinishDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectFinishFailDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectGuidsSinceDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectInvalidRequestFetchDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectInvalidTypeStoreDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectManyStoredDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectNoGUIDsSinceDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectNoStoreDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectStoreCompletedDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/ExpectStoredDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/HistoryHelpers.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/PasswordHelpers.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/SessionTestHelper.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessBeginDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessCreationDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessFetchDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessFinishDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java
rename from mobile/android/tests/background/junit3/src/sync/helpers/SimpleSuccessStoreDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java
rename from mobile/android/tests/background/junit3/src/telemetry/TestTelemetryRecorder.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/telemetry/TestTelemetryRecorder.java
rename from mobile/android/tests/background/junit3/src/testhelpers/BaseMockServerSyncStage.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
rename from mobile/android/tests/background/junit3/src/testhelpers/CommandHelpers.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java
rename from mobile/android/tests/background/junit3/src/testhelpers/DefaultGlobalSessionCallback.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
rename from mobile/android/tests/background/junit3/src/testhelpers/JPakeNumGeneratorFixed.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/JPakeNumGeneratorFixed.java
rename from mobile/android/tests/background/junit3/src/testhelpers/MockAbstractNonRepositorySyncStage.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java
rename from mobile/android/tests/background/junit3/src/testhelpers/MockClientsDataDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java
rename from mobile/android/tests/background/junit3/src/testhelpers/MockClientsDatabaseAccessor.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java
rename from mobile/android/tests/background/junit3/src/testhelpers/MockGlobalSession.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
rename from mobile/android/tests/background/junit3/src/testhelpers/MockPrefsGlobalSession.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
rename from mobile/android/tests/background/junit3/src/testhelpers/MockRecord.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockRecord.java
rename from mobile/android/tests/background/junit3/src/testhelpers/MockServerSyncStage.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java
rename from mobile/android/tests/background/junit3/src/testhelpers/MockSharedPreferences.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java
rename from mobile/android/tests/background/junit3/src/testhelpers/StubDelegate.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/StubDelegate.java
rename from mobile/android/tests/background/junit3/src/testhelpers/WBORepository.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WBORepository.java
rename from mobile/android/tests/background/junit3/src/testhelpers/WaitHelper.java
rename to mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java
--- a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestJarReader.java
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestJarReader.java
@@ -56,17 +56,17 @@ public class TestJarReader extends Instr
         stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
         assertNull(stream);
     }
 
     protected void assertExtractStream(String url) throws IOException {
         final File file = GeckoJarReader.extractStream(getInstrumentation().getTargetContext(), url, getInstrumentation().getContext().getCacheDir(), ".test");
         assertNotNull(file);
         try {
-            assertTrue(file.getName().endsWith("temp"));
+            assertTrue(file.getName().endsWith("test"));
             final String contents = FileUtils.getFileContents(file);
             assertNotNull(contents);
             assertTrue(contents.length() > 0);
         } finally {
             file.delete();
         }
     }
 
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -600,29 +600,33 @@ class RecursiveMakeBackend(CommonBackend
             # We'd like to install these via manifests as preprocessed files.
             # But they currently depend on non-standard flags being added via
             # some Makefiles, so for now we just pass them through to the
             # underlying Makefile.in.
             for f in obj.files:
                 backend_file.write('DIST_FILES += %s\n' % f)
 
         elif isinstance(obj, AndroidResDirs):
+            # Order matters.
             for p in obj.paths:
                 backend_file.write('ANDROID_RES_DIRS += %s\n' % p.full_path)
 
         elif isinstance(obj, AndroidAssetsDirs):
+            # Order matters.
             for p in obj.paths:
                 backend_file.write('ANDROID_ASSETS_DIRS += %s\n' % p.full_path)
 
         elif isinstance(obj, AndroidExtraResDirs):
-            for p in obj.paths:
-                backend_file.write('ANDROID_EXTRA_RES_DIRS += %s\n' % p.full_path)
+            # Order does not matter.
+            for p in sorted(set(p.full_path for p in obj.paths)):
+                backend_file.write('ANDROID_EXTRA_RES_DIRS += %s\n' % p)
 
         elif isinstance(obj, AndroidExtraPackages):
-            for p in obj.packages:
+            # Order does not matter.
+            for p in sorted(set(obj.packages)):
                 backend_file.write('ANDROID_EXTRA_PACKAGES += %s\n' % p)
 
         else:
             return False
 
         return True
 
     def _fill_root_mk(self):
@@ -1243,17 +1247,17 @@ INSTALL_TARGETS += %(prefix)s
         if jar.sources:
             backend_file.write('%s_JAVAFILES := %s\n' %
                 (target, ' '.join(jar.sources)))
         if jar.generated_sources:
             backend_file.write('%s_PP_JAVAFILES := %s\n' %
                 (target, ' '.join(mozpath.join('generated', f) for f in jar.generated_sources)))
         if jar.extra_jars:
             backend_file.write('%s_EXTRA_JARS := %s\n' %
-                (target, ' '.join(jar.extra_jars)))
+                (target, ' '.join(sorted(set(jar.extra_jars)))))
         if jar.javac_flags:
             backend_file.write('%s_JAVAC_FLAGS := %s\n' %
                 (target, ' '.join(jar.javac_flags)))
 
     def _process_android_eclipse_project_data(self, project, backend_file):
         # We add a single target to the backend.mk corresponding to
         # the moz.build defining the Android Eclipse project. This
         # target depends on some targets to be fresh, and installs a
--- a/toolkit/mozapps/update/common/moz.build
+++ b/toolkit/mozapps/update/common/moz.build
@@ -7,23 +7,26 @@
 EXPORTS += [
     'readstrings.h',
     'updatedefines.h',
     'updatelogging.h',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXPORTS += [
-        'certificatecheck.h',
         'pathhash.h',
-        'registrycertificates.h',
         'uachelper.h',
         'updatehelper.cpp',
         'updatehelper.h',
     ]
+    if CONFIG['MOZ_MAINTENANCE_SERVICE']:
+        EXPORTS += [
+            'certificatecheck.h',
+            'registrycertificates.h',
+        ]
 
 Library('updatecommon')
 
 DEFINES['NS_NO_XPCOM'] = True
 
 srcdir = '.'
 
 include('sources.mozbuild')
--- a/toolkit/mozapps/update/common/sources.mozbuild
+++ b/toolkit/mozapps/update/common/sources.mozbuild
@@ -1,25 +1,28 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 sources = []
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     sources += [
-        'certificatecheck.cpp',
         'pathhash.cpp',
-        'registrycertificates.cpp',
         'uachelper.cpp',
         'updatehelper.cpp',
     ]
-    OS_LIBS += [
-        'crypt32',
-        'wintrust',
-    ]
+    if CONFIG['MOZ_MAINTENANCE_SERVICE']:
+        sources += [
+            'certificatecheck.cpp',
+            'registrycertificates.cpp',
+        ]
+        OS_LIBS += [
+            'crypt32',
+            'wintrust',
+        ]
 
 sources += [
     'readstrings.cpp',
     'updatelogging.cpp',
 ]
 
 SOURCES += sorted(['%s/%s' % (srcdir, s) for s in sources])
--- a/toolkit/mozapps/update/updater/updater.cpp
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -122,17 +122,19 @@ static bool sUseHardLinks = true;
 
 #if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \
     !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK)
 #include "nss.h"
 #include "prerror.h"
 #endif
 
 #ifdef XP_WIN
+#ifdef MOZ_MAINTENANCE_SERVICE
 #include "registrycertificates.h"
+#endif
 BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra);
 BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer,
                             LPCWSTR siblingFilePath,
                             LPCWSTR newFileName);
 #include "updatehelper.h"
 
 // Closes the handle if valid and if the updater is elevated returns with the
 // return code specified. This prevents multiple launches of the callback
@@ -1871,17 +1873,17 @@ LaunchWinPostProcess(const WCHAR *instal
   }
 
   WCHAR exefullpath[MAX_PATH + 1] = { L'\0' };
   wcsncpy(exefullpath, installationDir, MAX_PATH);
   if (!PathAppendSafe(exefullpath, exefile)) {
     return false;
   }
 
-#if !defined(TEST_UPDATER)
+#if !defined(TEST_UPDATER) && defined(MOZ_MAINTENANCE_SERVICE)
   if (sUsingService &&
       !DoesBinaryMatchAllowedCertificates(installationDir, exefullpath)) {
     return false;
   }
 #endif
 
   WCHAR dlogFile[MAX_PATH + 1];
   if (!PathGetSiblingFilePath(dlogFile, exefullpath, L"uninstall.update")) {
@@ -2747,18 +2749,20 @@ int NS_main(int argc, NS_tchar **argv)
   }
 
   // The callback is the remaining arguments starting at callbackIndex.
   // The argument specified by callbackIndex is the callback executable and the
   // argument prior to callbackIndex is the working directory.
   const int callbackIndex = 6;
 
 #if defined(XP_WIN)
+#ifdef MOZ_MAINTENANCE_SERVICE
   sUsingService = EnvHasValue("MOZ_USING_SERVICE");
   putenv(const_cast<char*>("MOZ_USING_SERVICE="));
+#endif
   // lastFallbackError keeps track of the last error for the service not being
   // used, in case of an error when fallback is not enabled we write the
   // error to the update.status file.
   // When fallback is disabled (MOZ_NO_SERVICE_FALLBACK does not exist) then
   // we will instead fallback to not using the service and display a UAC prompt.
   int lastFallbackError = FALLBACKKEY_UNKNOWN_ERROR;
 
   // Launch a second instance of the updater with the runas verb on Windows