Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 04 Aug 2015 13:35:10 +0200
changeset 287757 b3f61169f7273904ce5d36e13e11c7694e32ad75
parent 287756 1164ec23627381ff9ce7747f8e5441560e7e5b82 (current diff)
parent 287694 5cf4d2f7f2f2b3df2f1edd31b8bdce7882f3875c (diff)
child 287758 0f3c55093831edcb5ce54b083a1fbeccc39324a7
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone42.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
browser/base/content/browser-readinglist.js
browser/components/readinglist/ReadingList.jsm
browser/components/readinglist/SQLiteStore.jsm
browser/components/readinglist/Scheduler.jsm
browser/components/readinglist/ServerClient.jsm
browser/components/readinglist/Sync.jsm
browser/components/readinglist/jar.mn
browser/components/readinglist/moz.build
browser/components/readinglist/sidebar.js
browser/components/readinglist/sidebar.xhtml
browser/components/readinglist/test/ReadingListTestUtils.jsm
browser/components/readinglist/test/browser/browser.ini
browser/components/readinglist/test/browser/browser_sidebar_list.js
browser/components/readinglist/test/browser/browser_sidebar_mouse_nav.js
browser/components/readinglist/test/browser/browser_ui_enable_disable.js
browser/components/readinglist/test/browser/head.js
browser/components/readinglist/test/xpcshell/head.js
browser/components/readinglist/test/xpcshell/test_ReadingList.js
browser/components/readinglist/test/xpcshell/test_SQLiteStore.js
browser/components/readinglist/test/xpcshell/test_ServerClient.js
browser/components/readinglist/test/xpcshell/test_Sync.js
browser/components/readinglist/test/xpcshell/test_scheduler.js
browser/components/readinglist/test/xpcshell/xpcshell.ini
browser/themes/linux/readinglist/sidebar.css
browser/themes/osx/readinglist/sidebar.css
browser/themes/shared/readinglist/icons.svg
browser/themes/shared/readinglist/readinglist-icon.svg
browser/themes/shared/readinglist/readinglist.inc.css
browser/themes/shared/readinglist/sidebar.inc.css
browser/themes/windows/readinglist/sidebar.css
--- a/addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js
@@ -6,17 +6,16 @@
 const { Cu } = require('chrome');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { fromIterator } = require('sdk/util/array');
 
 const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
   'menu_socialSidebar',
   'menu_historySidebar',
   'menu_bookmarksSidebar',
-  'menu_readingListSidebar'
 ];
 
 function isSidebarShowing(window) {
   window = window || getMostRecentBrowserWindow();
   let sidebar = window.document.getElementById('sidebar-box');
   return !sidebar.hidden;
 }
 exports.isSidebarShowing = isSidebarShowing;
--- a/addon-sdk/source/test/sidebar/utils.js
+++ b/addon-sdk/source/test/sidebar/utils.js
@@ -12,17 +12,16 @@ module.metadata = {
 const { Cu } = require('chrome');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { fromIterator } = require('sdk/util/array');
 
 const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
   'menu_socialSidebar',
   'menu_historySidebar',
   'menu_bookmarksSidebar',
-  'menu_readingListSidebar'
 ];
 
 function isSidebarShowing(window) {
   window = window || getMostRecentBrowserWindow();
   let sidebar = window.document.getElementById('sidebar-box');
   return !sidebar.hidden;
 }
 exports.isSidebarShowing = isSidebarShowing;
--- 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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ddfd98cdafefaa1b60273d5568b8dbd13730dae"/>
--- 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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ddfd98cdafefaa1b60273d5568b8dbd13730dae"/>
--- 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="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8bc59310552179f9a8bc6cdd0188e2475df52fb7"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9d0e5057ee5404a31ec1bf76131cb11336a7c3b6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="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="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ddfd98cdafefaa1b60273d5568b8dbd13730dae"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ddfd98cdafefaa1b60273d5568b8dbd13730dae"/>
--- 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="07c383a786f188904311a37f6062c2cb84c9b61d">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ddfd98cdafefaa1b60273d5568b8dbd13730dae"/>
--- 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="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8bc59310552179f9a8bc6cdd0188e2475df52fb7"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9d0e5057ee5404a31ec1bf76131cb11336a7c3b6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ddfd98cdafefaa1b60273d5568b8dbd13730dae"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "caba8b26c52d3c771e9ea6fe288acdaf74c7707e", 
+        "git_revision": "67c38af8347f93ddc005a53f427d651b744b55c1", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "95e9562f0290c1c42c6504a1001faa58f3e51e72", 
+    "revision": "72807eee01421a4ddf6180b2e5a66757a42a7984", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="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="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ddfd98cdafefaa1b60273d5568b8dbd13730dae"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/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="07c383a786f188904311a37f6062c2cb84c9b61d">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="caba8b26c52d3c771e9ea6fe288acdaf74c7707e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="67c38af8347f93ddc005a53f427d651b744b55c1"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ddfd98cdafefaa1b60273d5568b8dbd13730dae"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1922,21 +1922,16 @@ pref("dom.ipc.processHangMonitor", true)
 #ifdef DEBUG
 // Don't report hangs in DEBUG builds. They're too slow and often a
 // debugger is attached.
 pref("dom.ipc.reportProcessHangs", false);
 #else
 pref("dom.ipc.reportProcessHangs", true);
 #endif
 
-pref("browser.readinglist.enabled", false);
-pref("browser.readinglist.sidebarEverOpened", false);
-pref("readinglist.scheduler.enabled", false);
-pref("readinglist.server", "https://readinglist.services.mozilla.com/v1");
-
 pref("browser.reader.detectedFirstArticle", false);
 // Don't limit how many nodes we care about on desktop:
 pref("reader.parse-node-limit", 0);
 
 pref("browser.pocket.enabled", true);
 pref("browser.pocket.api", "api.getpocket.com");
 pref("browser.pocket.site", "getpocket.com");
 pref("browser.pocket.oAuthConsumerKey", "40249-e88c401e1b1f2242d9e441c4");
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -44,16 +44,17 @@ const gXPInstallObserver = {
     }
 
     const anchorID = "addons-notification-icon";
 
     // Make notifications persist a minimum of 30 seconds
     var options = {
       displayURI: installInfo.originatingURI,
       timeout: Date.now() + 30000,
+      removeOnDismissal: true,
     };
 
     let cancelInstallation = () => {
       if (installInfo) {
         for (let install of installInfo.installs)
           install.cancel();
       }
 
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -205,20 +205,16 @@
                   <menupopup id="viewSidebarMenu">
                     <menuitem id="menu_bookmarksSidebar"
                               key="viewBookmarksSidebarKb"
                               observes="viewBookmarksSidebar"/>
                     <menuitem id="menu_historySidebar"
                               key="key_gotoHistory"
                               observes="viewHistorySidebar"
                               label="&historyButton.label;"/>
-                    <menuitem id="menu_readingListSidebar"
-                              key="key_readingListSidebar"
-                              observes="readingListSidebar"
-                              label="&readingList.label;"/>
 
                     <!-- Service providers with sidebars are inserted between these two menuseperators -->
                     <menuseparator hidden="true"/>
                     <menuseparator class="social-provider-menu" hidden="true"/>
                   </menupopup>
                 </menu>
                 <menuseparator/>
                 <menu id="viewFullZoomMenu" label="&fullZoom.label;"
@@ -438,40 +434,16 @@
         <menupopup id="bookmarksToolbarFolderPopup"
 #ifndef XP_MACOSX
                    placespopup="true"
 #endif
                    context="placesContext"
                    onpopupshowing="if (!this.parentNode._placesView)
                                      new PlacesMenu(event, 'place:folder=TOOLBAR');"/>
       </menu>
-#ifndef XP_MACOSX
-# Disabled on Mac because we can't fill native menupopups asynchronously
-      <menuseparator id="menu_readingListSeparator">
-        <observes element="readingListSidebar" attribute="hidden"/>
-      </menuseparator>
-      <menu id="menu_readingList"
-            class="menu-iconic bookmark-item"
-            label="&readingList.label;"
-            container="true">
-        <observes element="readingListSidebar" attribute="hidden"/>
-        <menupopup id="readingListPopup"
-#ifndef XP_MACOSX
-                   placespopup="true"
-#endif
-                   onpopupshowing="ReadingListUI.onReadingListPopupShowing(this);">
-          <menuseparator id="viewReadingListSidebarSeparator"/>
-          <menuitem id="viewReadingListSidebar" class="subviewbutton"
-                    oncommand="SidebarUI.toggle('readingListSidebar');"
-                    label="&readingList.showSidebar.label;">
-            <observes element="readingListSidebar" attribute="checked"/>
-          </menuitem>
-        </menupopup>
-      </menu>
-#endif
       <menuseparator id="bookmarksMenuItemsSeparator"/>
       <!-- Bookmarks menu items -->
       <menuseparator builder="end"
                      class="hide-if-empty-places-result"/>
       <menuitem id="menu_unsortedBookmarks"
                 label="&unsortedBookmarksCmd.label;"
                 oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/>
     </menupopup>
deleted file mode 100644
--- a/browser/base/content/browser-readinglist.js
+++ /dev/null
@@ -1,376 +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/.
-*/
-
-XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
-  "resource:///modules/readinglist/ReadingList.jsm");
-
-const READINGLIST_COMMAND_ID = "readingListSidebar";
-
-let ReadingListUI = {
-  /**
-   * Frame-script messages we want to listen to.
-   * @type {[string]}
-   */
-  MESSAGES: [
-    "ReadingList:GetVisibility",
-    "ReadingList:ToggleVisibility",
-    "ReadingList:ShowIntro",
-  ],
-
-  /**
-   * Add-to-ReadingList toolbar button in the URLbar.
-   * @type {Element}
-   */
-  toolbarButton: null,
-
-  /**
-   * Whether this object is currently registered as a listener with ReadingList.
-   * Used to avoid inadvertantly loading the ReadLingList.jsm module on startup.
-   * @type {Boolean}
-   */
-  listenerRegistered: false,
-
-  /**
-   * Initialize the ReadingList UI.
-   */
-  init() {
-    this.toolbarButton = document.getElementById("readinglist-addremove-button");
-
-    Preferences.observe("browser.readinglist.enabled", this.updateUI, this);
-
-    const mm = window.messageManager;
-    for (let msg of this.MESSAGES) {
-      mm.addMessageListener(msg, this);
-    }
-
-    this.updateUI();
-  },
-
-  /**
-   * Un-initialize the ReadingList UI.
-   */
-  uninit() {
-    Preferences.ignore("browser.readinglist.enabled", this.updateUI, this);
-
-    const mm = window.messageManager;
-    for (let msg of this.MESSAGES) {
-      mm.removeMessageListener(msg, this);
-    }
-
-    if (this.listenerRegistered) {
-      ReadingList.removeListener(this);
-      this.listenerRegistered = false;
-    }
-  },
-
-  /**
-   * Whether the ReadingList feature is enabled or not.
-   * @type {boolean}
-   */
-  get enabled() {
-    return Preferences.get("browser.readinglist.enabled", false);
-  },
-
-  /**
-   * Whether the ReadingList sidebar is currently open or not.
-   * @type {boolean}
-   */
-  get isSidebarOpen() {
-    return SidebarUI.isOpen && SidebarUI.currentID == READINGLIST_COMMAND_ID;
-  },
-
-  /**
-   * Update the UI status, ensuring the UI is shown or hidden depending on
-   * whether the feature is enabled or not.
-   */
-  updateUI() {
-    let enabled = this.enabled;
-    if (enabled) {
-      // This is a no-op if we're already registered.
-      ReadingList.addListener(this);
-      this.listenerRegistered = true;
-    } else {
-      if (this.listenerRegistered) {
-        // This is safe to call if we're not currently registered, but we don't
-        // want to forcibly load the normally lazy-loaded module on startup.
-        ReadingList.removeListener(this);
-        this.listenerRegistered = false;
-      }
-
-      this.hideSidebar();
-    }
-
-    document.getElementById(READINGLIST_COMMAND_ID).setAttribute("hidden", !enabled);
-    document.getElementById(READINGLIST_COMMAND_ID).setAttribute("disabled", !enabled);
-  },
-
-  /**
-   * Show the ReadingList sidebar.
-   * @return {Promise}
-   */
-  showSidebar() {
-    if (this.enabled) {
-      return SidebarUI.show(READINGLIST_COMMAND_ID);
-    }
-  },
-
-  /**
-   * Hide the ReadingList sidebar, if it is currently shown.
-   */
-  hideSidebar() {
-    if (this.isSidebarOpen) {
-      SidebarUI.hide();
-    }
-  },
-
-  /**
-   * Re-refresh the ReadingList bookmarks submenu when it opens.
-   *
-   * @param {Element} target - Menu element opening.
-   */
-  onReadingListPopupShowing: Task.async(function* (target) {
-    if (target.id == "BMB_readingListPopup") {
-      // Setting this class in the .xul file messes with the way
-      // browser-places.js inserts bookmarks in the menu.
-      document.getElementById("BMB_viewReadingListSidebar")
-              .classList.add("panel-subview-footer");
-    }
-
-    while (!target.firstChild.id)
-      target.firstChild.remove();
-
-    let classList = "menuitem-iconic bookmark-item menuitem-with-favicon";
-    let insertPoint = target.firstChild;
-    if (insertPoint.classList.contains("subviewbutton"))
-      classList += " subviewbutton";
-
-    let hasItems = false;
-    yield ReadingList.forEachItem(item => {
-      hasItems = true;
-
-      let menuitem = document.createElement("menuitem");
-      menuitem.setAttribute("label", item.title || item.url);
-      menuitem.setAttribute("class", classList);
-
-      let node = menuitem._placesNode = {
-        // Passing the PlacesUtils.nodeIsURI check is required for the
-        // onCommand handler to load our URI.
-        type: Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
-
-        // makes PlacesUIUtils.canUserRemove return false.
-        // The context menu is broken without this.
-        parent: {type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER},
-
-        // A -1 id makes this item a non-bookmark, which avoids calling
-        // PlacesUtils.annotations.itemHasAnnotation to check if the
-        // bookmark should be opened in the sidebar (this call fails for
-        // readinglist item, and breaks loading our URI).
-        itemId: -1,
-
-        // Used by the tooltip and onCommand handlers.
-        uri: item.url,
-
-        // Used by the tooltip.
-        title: item.title
-      };
-
-      Favicons.getFaviconURLForPage(item.uri, uri => {
-        if (uri) {
-          menuitem.setAttribute("image",
-                                Favicons.getFaviconLinkForIcon(uri).spec);
-        }
-      });
-
-      target.insertBefore(menuitem, insertPoint);
-    }, {sort: "addedOn", descending: true});
-
-    if (!hasItems) {
-      let menuitem = document.createElement("menuitem");
-      let bundle =
-        Services.strings.createBundle("chrome://browser/locale/places/places.properties");
-      menuitem.setAttribute("label", bundle.GetStringFromName("bookmarksMenuEmptyFolder"));
-      menuitem.setAttribute("class", "bookmark-item");
-      menuitem.setAttribute("disabled", true);
-      target.insertBefore(menuitem, insertPoint);
-    }
-  }),
-
-  /**
-   * Hide the ReadingList sidebar, if it is currently shown.
-   */
-  toggleSidebar() {
-    if (this.enabled) {
-      SidebarUI.toggle(READINGLIST_COMMAND_ID);
-    }
-  },
-
-  /**
-   * Respond to messages.
-   */
-  receiveMessage(message) {
-    switch (message.name) {
-      case "ReadingList:GetVisibility": {
-        if (message.target.messageManager) {
-          message.target.messageManager.sendAsyncMessage("ReadingList:VisibilityStatus",
-            { isOpen: this.isSidebarOpen });
-        }
-        break;
-      }
-
-      case "ReadingList:ToggleVisibility": {
-        this.toggleSidebar();
-        break;
-      }
-
-      case "ReadingList:ShowIntro": {
-        if (this.enabled && !Preferences.get("browser.readinglist.introShown", false)) {
-          Preferences.set("browser.readinglist.introShown", true);
-          this.showSidebar();
-        }
-        break;
-      }
-    }
-  },
-
-  /**
-   * Handles toolbar button styling based on page proxy state changes.
-   *
-   * @see SetPageProxyState()
-   *
-   * @param {string} state - New state. Either "valid" or "invalid".
-   */
-  onPageProxyStateChanged: Task.async(function* (state) {
-    if (!this.toolbarButton) {
-      // nothing to do if we have no button.
-      return;
-    }
-
-    let uri;
-    if (this.enabled && state == "valid") {
-      uri = gBrowser.currentURI;
-      if (uri.schemeIs("about"))
-        uri = ReaderMode.getOriginalUrl(uri.spec);
-      else if (!uri.schemeIs("http") && !uri.schemeIs("https"))
-        uri = null;
-    }
-
-    let msg = {topic: "UpdateActiveItem", url: null};
-    if (!uri) {
-      this.toolbarButton.setAttribute("hidden", true);
-      if (this.isSidebarOpen)
-        document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
-      return;
-    }
-
-    let isInList = yield ReadingList.hasItemForURL(uri);
-
-    if (window.closed) {
-      // Skip updating the UI if the window was closed since our hasItemForURL call.
-      return;
-    }
-
-    if (this.isSidebarOpen) {
-      if (isInList)
-        msg.url = typeof uri == "string" ? uri : uri.spec;
-      document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
-    }
-    this.setToolbarButtonState(isInList);
-  }),
-
-  /**
-   * Set the state of the ReadingList toolbar button in the urlbar.
-   * If the current tab's page is in the ReadingList (active), sets the button
-   * to allow removing the page. Otherwise, sets the button to allow adding the
-   * page (not active).
-   *
-   * @param {boolean} active - True if the button should be active (page is
-   *                           already in the list).
-   */
-  setToolbarButtonState(active) {
-    this.toolbarButton.setAttribute("already-added", active);
-
-    let type = (active ? "remove" : "add");
-    let tooltip = gNavigatorBundle.getString(`readingList.urlbar.${type}`);
-    this.toolbarButton.setAttribute("tooltiptext", tooltip);
-
-    this.toolbarButton.removeAttribute("hidden");
-  },
-
-  buttonClick(event) {
-    if (event.button != 0) {
-      return;
-    }
-    this.togglePageByBrowser(gBrowser.selectedBrowser);
-  },
-
-  /**
-   * Toggle a page (from a browser) in the ReadingList, adding if it's not already added, or
-   * removing otherwise.
-   *
-   * @param {<xul:browser>} browser - Browser with page to toggle.
-   * @returns {Promise} Promise resolved when operation has completed.
-   */
-  togglePageByBrowser: Task.async(function* (browser) {
-    let uri = browser.currentURI;
-    if (uri.spec.startsWith("about:reader?"))
-      uri = ReaderMode.getOriginalUrl(uri.spec);
-    if (!uri)
-      return;
-
-    let item = yield ReadingList.itemForURL(uri);
-    if (item) {
-      yield item.delete();
-    } else {
-      yield ReadingList.addItemFromBrowser(browser, uri);
-    }
-  }),
-
-  /**
-   * Checks if a given item matches the current tab in this window.
-   *
-   * @param {ReadingListItem} item - Item to check
-   * @returns True if match, false otherwise.
-   */
-  isItemForCurrentBrowser(item) {
-    let currentURL = gBrowser.currentURI.spec;
-    if (currentURL.startsWith("about:reader?"))
-      currentURL = ReaderMode.getOriginalUrl(currentURL);
-
-    if (item.url == currentURL || item.resolvedURL == currentURL) {
-      return true;
-    }
-    return false;
-  },
-
-  /**
-   * ReadingList event handler for when an item is added.
-   *
-   * @param {ReadingListItem} item - Item added.
-   */
-  onItemAdded(item) {
-    if (!Services.prefs.getBoolPref("browser.readinglist.sidebarEverOpened")) {
-      SidebarUI.show("readingListSidebar");
-    }
-    if (this.isItemForCurrentBrowser(item)) {
-      this.setToolbarButtonState(true);
-      if (this.isSidebarOpen) {
-        let msg = {topic: "UpdateActiveItem", url: item.url};
-        document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
-      }
-    }
-  },
-
-  /**
-   * ReadingList event handler for when an item is deleted.
-   *
-   * @param {ReadingListItem} item - Item deleted.
-   */
-  onItemDeleted(item) {
-    if (this.isItemForCurrentBrowser(item)) {
-      this.setToolbarButtonState(false);
-    }
-  },
-};
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -141,21 +141,16 @@
     <!-- for both places and non-places, the sidebar lives at
          chrome://browser/content/history/history-panel.xul so there are no
          problems when switching between versions -->
     <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;"
                  type="checkbox" group="sidebar"
                  sidebarurl="chrome://browser/content/history/history-panel.xul"
                  oncommand="SidebarUI.toggle('viewHistorySidebar');"/>
 
-    <broadcaster id="readingListSidebar" hidden="true" autoCheck="false" disabled="true"
-                 sidebartitle="&readingList.label;" type="checkbox" group="sidebar"
-                 sidebarurl="chrome://browser/content/readinglist/sidebar.xhtml"
-                 oncommand="SidebarUI.toggle('readingListSidebar');"/>
-
     <broadcaster id="viewWebPanelsSidebar" autoCheck="false"
                  type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul"
                  oncommand="SidebarUI.toggle('viewWebPanelsSidebar');"/>
 
     <broadcaster id="bookmarkThisPageBroadcaster"
                  label="&bookmarkThisPageCmd.label;"
                  bookmarklabel="&bookmarkThisPageCmd.label;"
                  editlabel="&editThisBookmarkCmd.label;"/>
@@ -416,21 +411,16 @@
          key="&historySidebarCmd.commandKey;"
 #ifdef XP_MACOSX
          modifiers="accel,shift"
 #else
          modifiers="accel"
 #endif
          command="viewHistorySidebar"/>
 
-    <key id="key_readingListSidebar"
-         key="&readingList.sidebar.commandKey;"
-         modifiers="accel,alt"
-         command="readingListSidebar"/>
-
     <key id="key_fullZoomReduce"  key="&fullZoomReduceCmd.commandkey;"   command="cmd_fullZoomReduce"  modifiers="accel"/>
     <key                          key="&fullZoomReduceCmd.commandkey2;"  command="cmd_fullZoomReduce"  modifiers="accel"/>
     <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;"  command="cmd_fullZoomEnlarge" modifiers="accel"/>
     <key                          key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
     <key                          key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
     <key id="key_fullZoomReset"   key="&fullZoomResetCmd.commandkey;"    command="cmd_fullZoomReset"   modifiers="accel"/>
     <key                          key="&fullZoomResetCmd.commandkey2;"   command="cmd_fullZoomReset"   modifiers="accel"/>
 
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -6,19 +6,16 @@ Cu.import("resource://gre/modules/XPCOMU
 
 #ifdef MOZ_SERVICES_CLOUDSYNC
 XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
                                   "resource://gre/modules/CloudSync.jsm");
 #else
 let CloudSync = null;
 #endif
 
-XPCOMUtils.defineLazyModuleGetter(this, "ReadingListScheduler",
-                                  "resource:///modules/readinglist/Scheduler.jsm");
-
 // gSyncUI handles updating the tools menu and displaying notifications.
 let gSyncUI = {
   _obs: ["weave:service:sync:start",
          "weave:service:sync:finish",
          "weave:service:sync:error",
          "weave:service:quota:remaining",
          "weave:service:setup-complete",
          "weave:service:login:start",
@@ -26,20 +23,16 @@ let gSyncUI = {
          "weave:service:login:error",
          "weave:service:logout:finish",
          "weave:service:start-over",
          "weave:service:start-over:finish",
          "weave:ui:login:error",
          "weave:ui:sync:error",
          "weave:ui:sync:finish",
          "weave:ui:clear-error",
-
-         "readinglist:sync:start",
-         "readinglist:sync:finish",
-         "readinglist:sync:error",
   ],
 
   _unloaded: false,
   // The number of "active" syncs - while this is non-zero, our button will spin
   _numActiveSyncTasks: 0,
 
   init: function () {
     Cu.import("resource://services-common/stringbundle.js");
@@ -110,41 +103,36 @@ let gSyncUI = {
   _needsSetup() {
     // We want to treat "account needs verification" as "needs setup". So
     // "reach in" to Weave.Status._authManager to check whether we the signed-in
     // user is verified.
     // Referencing Weave.Status spins a nested event loop to initialize the
     // authManager, so this should always return a value directly.
     // This only applies to fxAccounts-based Sync.
     if (Weave.Status._authManager._signedInUser !== undefined) {
-      // So we are using Firefox accounts - in this world, checking Sync isn't
-      // enough as reading list may be configured but not Sync.
-      // We consider ourselves setup if we have a verified user.
-      // XXX - later we should consider checking preferences to ensure at least
-      // one engine is enabled?
+      // If we have a signed in user already, and that user is not verified,
+      // revert to the "needs setup" state.
       return !Weave.Status._authManager._signedInUser ||
              !Weave.Status._authManager._signedInUser.verified;
     }
 
-    // So we are using legacy sync, and reading-list isn't supported for such
-    // users, so check sync itself.
+    // We are using legacy sync - check that.
     let firstSync = "";
     try {
       firstSync = Services.prefs.getCharPref("services.sync.firstSync");
     } catch (e) { }
 
     return Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED ||
            firstSync == "notReady";
   },
 
   _loginFailed: function () {
-    this.log.debug("_loginFailed has sync state=${sync}, readinglist state=${rl}",
-                   { sync: Weave.Status.login, rl: ReadingListScheduler.state});
-    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED ||
-           ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
+    this.log.debug("_loginFailed has sync state=${sync}",
+                   { sync: Weave.Status.login});
+    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
   },
 
   updateUI: function SUI_updateUI() {
     let needsSetup = this._needsSetup();
     let loginFailed = this._loginFailed();
 
     // Start off with a clean slate
     document.getElementById("sync-reauth-state").hidden = true;
@@ -230,18 +218,16 @@ let gSyncUI = {
   },
 
   onSetupComplete: function SUI_onSetupComplete() {
     this.onLoginFinish();
   },
 
   onLoginError: function SUI_onLoginError() {
     this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status);
-    // Note: This is used for *both* Sync and ReadingList login errors.
-    // if login fails, any other notifications are essentially moot
     Weave.Notifications.removeAll();
 
     // if we haven't set up the client, don't show errors
     if (this._needsSetup()) {
       this.updateUI();
       return;
     }
     // if we are still waiting for the identity manager to initialize, or it's
@@ -255,22 +241,20 @@ let gSyncUI = {
       this.updateUI();
       return;
     }
     this.showLoginError();
     this.updateUI();
   },
 
   showLoginError() {
-    // Note: This is used for *both* Sync and ReadingList login errors.
     let title = this._stringBundle.GetStringFromName("error.login.title");
 
     let description;
-    if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE ||
-        this.isProlongedReadingListError()) {
+    if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) {
       this.log.debug("showLoginError has a prolonged login error");
       // Convert to days
       let lastSync =
         Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
       description =
         this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
     } else {
       let reason = Weave.Utils.getErrorString(Weave.Status.login);
@@ -328,17 +312,16 @@ let gSyncUI = {
   doSync: function SUI_doSync() {
     let needsSetup = this._needsSetup();
 
     if (!needsSetup) {
       setTimeout(function () Weave.Service.errorHandler.syncAndReportErrors(), 0);
     }
 
     Services.obs.notifyObservers(null, "cloudsync:user-sync", null);
-    Services.obs.notifyObservers(null, "readinglist:user-sync", null);
   },
 
   handleToolbarButton: function SUI_handleStatusbarButton() {
     if (this._needsSetup())
       this.openSetup();
     else
       this.doSync();
   },
@@ -427,24 +410,16 @@ let gSyncUI = {
     let syncButton = document.getElementById("sync-button");
     let statusButton = document.getElementById("PanelUI-fxa-icon");
 
     let lastSync;
     try {
       lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync"));
     }
     catch (e) { };
-    // and reading-list time - we want whatever one is the most recent.
-    try {
-      let lastRLSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
-      if (!lastSync || lastRLSync > lastSync) {
-        lastSync = lastRLSync;
-      }
-    }
-    catch (e) { };
     if (!lastSync || this._needsSetup()) {
       if (syncButton) {
         syncButton.removeAttribute("tooltiptext");
       }
       if (statusButton) {
         statusButton.removeAttribute("tooltiptext");
       }
       return;
@@ -470,85 +445,16 @@ let gSyncUI = {
 
   onSyncFinish: function SUI_onSyncFinish() {
     let title = this._stringBundle.GetStringFromName("error.sync.title");
 
     // Clear out sync failures on a successful sync
     this.clearError(title);
   },
 
-  // Return true if the reading-list is in a "prolonged" error state. That
-  // engine doesn't impose what that means, so calculate it here. For
-  // consistency, we just use the sync prefs.
-  isProlongedReadingListError() {
-    // If the readinglist scheduler is disabled we don't treat it as prolonged.
-    let enabled = false;
-    try {
-      enabled = Services.prefs.getBoolPref("readinglist.scheduler.enabled");
-    } catch (_) {}
-    if (!enabled) {
-      return false;
-    }
-    let lastSync, threshold, prolonged;
-    try {
-      lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
-      threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") * 1000);
-      prolonged = lastSync <= threshold;
-    } catch (ex) {
-      // no pref, assume not prolonged.
-      prolonged = false;
-    }
-    this.log.debug("isProlongedReadingListError has last successful sync at ${lastSync}, threshold is ${threshold}, prolonged=${prolonged}",
-                   {lastSync, threshold, prolonged});
-    return prolonged;
-  },
-
-  onRLSyncError() {
-    // Like onSyncError, but from the reading-list engine.
-    // However, the current UX around Sync is that error notifications should
-    // generally *not* be seen as they typically aren't actionable - so only
-    // authentication errors (which require user action) and "prolonged" errors
-    // (which technically aren't actionable, but user really should know anyway)
-    // are shown.
-    this.log.debug("onRLSyncError with readingList state", ReadingListScheduler.state);
-    if (ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION) {
-      this.onLoginError();
-      return;
-    }
-    // If it's not prolonged there's nothing to do.
-    if (!this.isProlongedReadingListError()) {
-      this.log.debug("onRLSyncError has a non-authentication, non-prolonged error, so not showing any error UI");
-      return;
-    }
-    // So it's a prolonged error.
-    // Unfortunate duplication from below...
-    this.log.debug("onRLSyncError has a prolonged error");
-    let title = this._stringBundle.GetStringFromName("error.sync.title");
-    // XXX - this is somewhat wrong - we are reporting the threshold we consider
-    // to be prolonged, not how long it actually has been. (ie, lastSync below
-    // is effectively constant) - bit it too is copied from below.
-    let lastSync =
-      Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
-    let description =
-      this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
-    let priority = Weave.Notifications.PRIORITY_INFO;
-    let buttons = [
-      new Weave.NotificationButton(
-        this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"),
-        this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"),
-        function() { gSyncUI.doSync(); return true; }
-      ),
-    ];
-    let notification =
-      new Weave.Notification(title, description, null, priority, buttons);
-    Weave.Notifications.replaceTitle(notification);
-
-    this.updateUI();
-  },
-
   onSyncError: function SUI_onSyncError() {
     this.log.debug("onSyncError: login=${login}, sync=${sync}", Weave.Status);
     let title = this._stringBundle.GetStringFromName("error.sync.title");
 
     if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
       this.onLoginError();
       return;
     }
@@ -632,31 +538,27 @@ let gSyncUI = {
         ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) {
       subject = subject.wrappedJSObject.object;
     }
 
     // First handle "activity" only.
     switch (topic) {
       case "weave:service:sync:start":
       case "weave:service:login:start":
-      case "readinglist:sync:start":
         this.onActivityStart();
         break;
       case "weave:service:sync:finish":
       case "weave:service:sync:error":
       case "weave:service:login:finish":
       case "weave:service:login:error":
-      case "readinglist:sync:finish":
-      case "readinglist:sync:error":
         this.onActivityStop();
         break;
     }
     // Now non-activity state (eg, enabled, errors, etc)
     // Note that sync uses the ":ui:" notifications for errors because sync.
-    // ReadingList has no such concept (yet?; hopefully the :error is enough!)
     switch (topic) {
       case "weave:ui:sync:finish":
         this.onSyncFinish();
         break;
       case "weave:ui:sync:error":
         this.onSyncError();
         break;
       case "weave:service:quota:remaining":
@@ -684,23 +586,16 @@ let gSyncUI = {
         this.initUI();
         break;
       case "weave:notification:added":
         this.initNotifications();
         break;
       case "weave:ui:clear-error":
         this.clearError();
         break;
-
-      case "readinglist:sync:error":
-        this.onRLSyncError();
-        break;
-      case "readinglist:sync:finish":
-        this.clearError();
-        break;
     }
   },
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference
   ])
 };
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -269,17 +269,16 @@ let gInitialPages = [
 #include browser-eme.js
 #include browser-feeds.js
 #include browser-fullScreen.js
 #include browser-fullZoom.js
 #include browser-gestureSupport.js
 #include browser-loop.js
 #include browser-places.js
 #include browser-plugins.js
-#include browser-readinglist.js
 #include browser-safebrowsing.js
 #include browser-sidebar.js
 #include browser-social.js
 #include browser-tabview.js
 #include browser-thumbnails.js
 #include browser-trackingprotection.js
 
 #ifdef MOZ_DATA_REPORTING
@@ -1262,18 +1261,16 @@ var gBrowserInit = {
     window.messageManager.addMessageListener("Browser:URIFixup", gKeywordURIFixup);
 
     BrowserOffline.init();
     OfflineApps.init();
     IndexedDBPromptHelper.init();
 #ifdef E10S_TESTING_ONLY
     gRemoteTabsUI.init();
 #endif
-    ReadingListUI.init();
-
     // Initialize the full zoom setting.
     // We do this before the session restore service gets initialized so we can
     // apply full zoom settings to tabs restored by the session restore service.
     FullZoom.init();
     PanelUI.init();
     LightweightThemeListener.init();
 
     Services.telemetry.getHistogramById("E10S_WINDOW").add(gMultiProcessBrowser);
@@ -1544,18 +1541,16 @@ var gBrowserInit = {
     BrowserOnClick.uninit();
 
     DevEdition.uninit();
 
     TrackingProtection.uninit();
 
     gMenuButtonUpdateBadge.uninit();
 
-    ReadingListUI.uninit();
-
     SidebarUI.uninit();
 
     // Now either cancel delayedStartup, or clean up the services initialized from
     // it.
     if (this._boundDelayedStartup) {
       this._cancelDelayedStartup();
     } else {
       if (Win7Features)
@@ -2544,18 +2539,16 @@ function UpdatePageProxyState()
 {
   if (gURLBar && gURLBar.value != gLastValidURLStr)
     SetPageProxyState("invalid");
 }
 
 function SetPageProxyState(aState)
 {
   BookmarkingUI.onPageProxyStateChanged(aState);
-  ReadingListUI.onPageProxyStateChanged(aState);
-
   if (!gURLBar)
     return;
 
   if (!gProxyFavIcon)
     gProxyFavIcon = document.getElementById("page-proxy-favicon");
 
   gURLBar.setAttribute("pageproxystate", aState);
   gProxyFavIcon.setAttribute("pageproxystate", aState);
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -779,20 +779,16 @@
                 <label class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
               </box>
               <hbox id="urlbar-icons">
                 <image id="page-report-button"
                        class="urlbar-icon"
                        hidden="true"
                        tooltiptext="&pageReportIcon.tooltip;"
                        onclick="gPopupBlockerObserver.onReportButtonClick(event);"/>
-                <image id="readinglist-addremove-button"
-                       class="urlbar-icon"
-                       hidden="true"
-                       onclick="ReadingListUI.buttonClick(event);"/>
                 <image id="reader-mode-button"
                        class="urlbar-icon"
                        hidden="true"
                        onclick="ReaderParent.buttonClick(event);"/>
               </hbox>
               <toolbarbutton id="urlbar-go-button"
                              class="chromeclass-toolbar-additional"
                              onclick="gURLBar.handleCommand(event);"
@@ -913,32 +909,16 @@
                   container="true">
               <menupopup id="BMB_unsortedBookmarksPopup"
                          placespopup="true"
                          context="placesContext"
                          onpopupshowing="if (!this.parentNode._placesView)
                                            new PlacesMenu(event, 'place:folder=UNFILED_BOOKMARKS',
                                                           PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);"/>
             </menu>
-            <menuseparator>
-              <observes element="readingListSidebar" attribute="hidden"/>
-            </menuseparator>
-            <menu id="BMB_readingList"
-                  class="menu-iconic bookmark-item subviewbutton"
-                  label="&readingList.label;"
-                  container="true">
-              <observes element="readingListSidebar" attribute="hidden"/>
-              <menupopup id="BMB_readingListPopup"
-                         placespopup="true"
-                         onpopupshowing="ReadingListUI.onReadingListPopupShowing(this);">
-                <menuitem id="BMB_viewReadingListSidebar" class="subviewbutton"
-                          oncommand="SidebarUI.show('readingListSidebar');"
-                          label="&readingList.showSidebar.label;"/>
-              </menupopup>
-            </menu>
             <menuseparator/>
             <!-- Bookmarks menu items will go here -->
             <menuitem id="BMB_bookmarksShowAll"
                       class="subviewbutton panel-subview-footer"
                       label="&showAllBookmarks2.label;"
                       command="Browser:ShowAllBookmarks"
                       key="manBookmarkKb"/>
           </menupopup>
--- a/browser/base/content/contentSearchUI.js
+++ b/browser/base/content/contentSearchUI.js
@@ -76,35 +76,42 @@ ContentSearchUIController.prototype = {
   // may regress performance - so we set this flag and only set up the buttons
   // if it's set when the suggestions table is actually opened.
   _pendingOneOffRefresh: undefined,
 
   get defaultEngine() {
     return this._defaultEngine;
   },
 
-  set defaultEngine(val) {
-    this._defaultEngine = val;
+  set defaultEngine(engine) {
+    let icon;
+    if (engine.iconBuffer) {
+      icon = this._getFaviconURIFromBuffer(engine.iconBuffer);
+    }
+    else {
+      icon = this._getImageURIForCurrentResolution(
+        "chrome://mozapps/skin/places/defaultFavicon.png");
+    }
+    this._defaultEngine = {
+      name: engine.name,
+      icon: icon,
+    };
     this._updateDefaultEngineHeader();
 
-    if (val && document.activeElement == this.input) {
+    if (engine && document.activeElement == this.input) {
       this._speculativeConnect();
     }
   },
 
   get engines() {
     return this._engines;
   },
 
   set engines(val) {
     this._engines = val;
-    if (!this._table.hidden) {
-      this._setUpOneOffButtons();
-      return;
-    }
     this._pendingOneOffRefresh = true;
   },
 
   // The selectedIndex is the index of the element with the "selected" class in
   // the list obtained by concatenating the suggestion rows, one-off buttons, and
   // search settings button.
   get selectedIndex() {
     let allElts = [...this._suggestionsList.children,
@@ -122,34 +129,64 @@ ContentSearchUIController.prototype = {
   set selectedIndex(idx) {
     // Update the table's rows, and the input when there is a selection.
     this._table.removeAttribute("aria-activedescendant");
     this.input.removeAttribute("aria-activedescendant");
 
     let allElts = [...this._suggestionsList.children,
                    ...this._oneOffButtons,
                    document.getElementById("contentSearchSettingsButton")];
+    // If we are selecting a suggestion and a one-off is selected, don't deselect it.
+    let excludeIndex = idx < this.numSuggestions && this.selectedButtonIndex > -1 ?
+                       this.numSuggestions + this.selectedButtonIndex : -1;
     for (let i = 0; i < allElts.length; ++i) {
       let elt = allElts[i];
       let ariaSelectedElt = i < this.numSuggestions ? elt.firstChild : elt;
       if (i == idx) {
         elt.classList.add("selected");
         ariaSelectedElt.setAttribute("aria-selected", "true");
         this.input.setAttribute("aria-activedescendant", ariaSelectedElt.id);
       }
-      else {
+      else if (i != excludeIndex) {
         elt.classList.remove("selected");
         ariaSelectedElt.setAttribute("aria-selected", "false");
       }
     }
   },
 
+  get selectedButtonIndex() {
+    let elts = [...this._oneOffButtons,
+                document.getElementById("contentSearchSettingsButton")];
+    for (let i = 0; i < elts.length; ++i) {
+      if (elts[i].classList.contains("selected")) {
+        return i;
+      }
+    }
+    return -1;
+  },
+
+  set selectedButtonIndex(idx) {
+    let elts = [...this._oneOffButtons,
+                document.getElementById("contentSearchSettingsButton")];
+    for (let i = 0; i < elts.length; ++i) {
+      let elt = elts[i];
+      if (i == idx) {
+        elt.classList.add("selected");
+        elt.setAttribute("aria-selected", "true");
+      }
+      else {
+        elt.classList.remove("selected");
+        elt.setAttribute("aria-selected", "false");
+      }
+    }
+  },
+
   get selectedEngineName() {
-    let selectedElt = this._table.querySelector(".selected");
-    if (selectedElt && selectedElt.engineName) {
+    let selectedElt = this._oneOffsTable.querySelector(".selected");
+    if (selectedElt) {
       return selectedElt.engineName;
     }
     return this.defaultEngine.name;
   },
 
   get numSuggestions() {
     return this._suggestionsList.children.length;
   },
@@ -189,17 +226,17 @@ ContentSearchUIController.prototype = {
     this._sendMsg("AddFormHistoryEntry", this.input.value);
   },
 
   handleEvent: function (event) {
     this["_on" + event.type[0].toUpperCase() + event.type.substr(1)](event);
   },
 
   _onCommand: function(aEvent) {
-    if (this.selectedIndex == this.numSuggestions + this._oneOffButtons.length) {
+    if (this.selectedButtonIndex == this._oneOffButtons.length) {
       // Settings button was selected.
       this._sendMsg("ManageEngines");
       return;
     }
 
     this.search(aEvent);
 
     if (aEvent) {
@@ -259,29 +296,68 @@ ContentSearchUIController.prototype = {
       this._getSuggestions();
       this.selectAndUpdateInput(-1);
     }
     this._updateSearchWithHeader();
   },
 
   _onKeypress: function (event) {
     let selectedIndexDelta = 0;
+    let selectedSuggestionDelta = 0;
+    let selectedOneOffDelta = 0;
+
     switch (event.keyCode) {
     case event.DOM_VK_UP:
-      if (!this._table.hidden) {
-        selectedIndexDelta = -1;
+      if (this._table.hidden) {
+        return;
       }
+      if (event.getModifierState("Accel")) {
+        if (event.shiftKey) {
+          selectedSuggestionDelta = -1;
+          break;
+        }
+        this._cycleCurrentEngine(true);
+        break;
+      }
+      if (event.altKey) {
+        selectedOneOffDelta = -1;
+        break;
+      }
+      selectedIndexDelta = -1;
       break;
     case event.DOM_VK_DOWN:
       if (this._table.hidden) {
         this._getSuggestions();
+        return;
       }
-      else {
-        selectedIndexDelta = 1;
+      if (event.getModifierState("Accel")) {
+        if (event.shiftKey) {
+          selectedSuggestionDelta = 1;
+          break;
+        }
+        this._cycleCurrentEngine(false);
+        break;
+      }
+      if (event.altKey) {
+        selectedOneOffDelta = 1;
+        break;
       }
+      selectedIndexDelta = 1;
+      break;
+    case event.DOM_VK_TAB:
+      if (this._table.hidden) {
+        return;
+      }
+      // Shift+tab when either the first or no one-off is selected, as well as
+      // tab when the settings button is selected, should change focus as normal.
+      if ((this.selectedButtonIndex <= 0 && event.shiftKey) ||
+          this.selectedButtonIndex == this._oneOffButtons.length && !event.shiftKey) {
+        return;
+      }
+      selectedOneOffDelta = event.shiftKey ? -1 : 1;
       break;
     case event.DOM_VK_RIGHT:
       // Allow normal caret movement until the caret is at the end of the input.
       if (this.input.selectionStart != this.input.selectionEnd ||
           this.input.selectionEnd != this.input.value.length) {
         return;
       }
       if (this.numSuggestions && this.selectedIndex >= 0 &&
@@ -292,47 +368,99 @@ ContentSearchUIController.prototype = {
       } else {
         // If we didn't select anything, make sure to remove the attributes
         // in case they were populated last time.
         this.input.removeAttribute("selection-index");
         this.input.removeAttribute("selection-kind");
       }
       this._stickyInputValue = this.input.value;
       this._hideSuggestions();
-      break;
+      return;
     case event.DOM_VK_RETURN:
       this._onCommand(event);
-      break;
+      return;
     case event.DOM_VK_DELETE:
       if (this.selectedIndex >= 0) {
         this.deleteSuggestionAtIndex(this.selectedIndex);
       }
-      break;
+      return;
     case event.DOM_VK_ESCAPE:
       if (!this._table.hidden) {
         this._hideSuggestions();
       }
+      return;
     default:
       return;
     }
 
+    let currentIndex = this.selectedIndex;
     if (selectedIndexDelta) {
-      // Update the selection.
-      let newSelectedIndex = this.selectedIndex + selectedIndexDelta;
+      let newSelectedIndex = currentIndex + selectedIndexDelta;
       if (newSelectedIndex < -1) {
         newSelectedIndex = this.numSuggestions + this._oneOffButtons.length;
       }
-      else if (this.numSuggestions + this._oneOffButtons.length < newSelectedIndex) {
+      // If are moving up from the first one off, we have to deselect the one off
+      // manually because the selectedIndex setter tries to exclude the selected
+      // one-off (which is desirable for accel+shift+up/down).
+      if (currentIndex == this.numSuggestions && selectedIndexDelta == -1) {
+        this.selectedButtonIndex = -1;
+      }
+      this.selectAndUpdateInput(newSelectedIndex);
+    }
+
+    else if (selectedSuggestionDelta) {
+      let newSelectedIndex;
+      if (currentIndex >= this.numSuggestions || currentIndex == -1) {
+        // No suggestion already selected, select the first/last one appropriately.
+        newSelectedIndex = selectedSuggestionDelta == 1 ?
+                           0 : this.numSuggestions - 1;
+      }
+      else {
+        newSelectedIndex = currentIndex + selectedSuggestionDelta;
+      }
+      if (newSelectedIndex >= this.numSuggestions) {
         newSelectedIndex = -1;
       }
       this.selectAndUpdateInput(newSelectedIndex);
+    }
 
-      // Prevent the input's caret from moving.
-      event.preventDefault();
+    else if (selectedOneOffDelta) {
+      let newSelectedIndex;
+      let currentButton = this.selectedButtonIndex;
+      if (currentButton == -1 || currentButton == this._oneOffButtons.length) {
+        // No one-off already selected, select the first/last one appropriately.
+        newSelectedIndex = selectedOneOffDelta == 1 ?
+                           0 : this._oneOffButtons.length - 1;
+      }
+      else {
+        newSelectedIndex = currentButton + selectedOneOffDelta;
+      }
+      // Allow selection of the settings button via the tab key.
+      if (newSelectedIndex == this._oneOffButtons.length &&
+          event.keyCode != event.DOM_VK_TAB) {
+        newSelectedIndex = -1;
+      }
+      this.selectedButtonIndex = newSelectedIndex;
     }
+
+    // Prevent the input's caret from moving.
+    event.preventDefault();
+  },
+
+  _currentEngineIndex: -1,
+  _cycleCurrentEngine: function (aReverse) {
+    if ((this._currentEngineIndex == this._oneOffButtons.length - 1 && !aReverse) ||
+        (this._currentEngineIndex < 0 && aReverse)) {
+      return;
+    }
+    this._currentEngineIndex += aReverse ? -1 : 1;
+    let engineName = this._currentEngineIndex > -1 ?
+                     this._oneOffButtons[this._currentEngineIndex].engineName :
+                     this._originalDefaultEngine.name;
+    this._sendMsg("SetCurrentEngine", engineName);
   },
 
   _onFocus: function () {
     if (this._mousedown) {
       return;
     }
     // When the input box loses focus to something in our table, we refocus it
     // immediately. This causes the focus highlight to flicker, so we set a
@@ -351,26 +479,40 @@ ContentSearchUIController.prototype = {
       setTimeout(() => this.input.focus(), 0);
       return;
     }
     this.input.removeAttribute("keepfocus");
     this._hideSuggestions();
   },
 
   _onMousemove: function (event) {
-    this.selectedIndex = this._indexOfTableItem(event.target);
+    let idx = this._indexOfTableItem(event.target);
+    if (idx >= this.numSuggestions) {
+      this.selectedButtonIndex = idx - this.numSuggestions;
+      return;
+    }
+    this.selectedIndex = idx;
   },
 
   _onMouseup: function (event) {
     if (event.button == 2) {
       return;
     }
     this._onCommand(event);
   },
 
+  _onMouseout: function (event) {
+    // We only deselect one-off buttons and the settings button when they are
+    // moused out.
+    let idx = this._indexOfTableItem(event.originalTarget);
+    if (idx >= this.numSuggestions) {
+      this.selectedButtonIndex = -1;
+    }
+  },
+
   _onClick: function (event) {
     this._onMouseup(event);
   },
 
   _onContentSearchService: function (event) {
     let methodName = "_onMsg" + event.detail.type;
     if (methodName in this) {
       this[methodName](event.detail.data);
@@ -422,56 +564,54 @@ ContentSearchUIController.prototype = {
     if (this._table.hidden) {
       this.selectedIndex = -1;
       if (this._pendingOneOffRefresh) {
         this._setUpOneOffButtons();
         delete this._pendingOneOffRefresh;
       }
       this._table.hidden = false;
       this.input.setAttribute("aria-expanded", "true");
+      this._originalDefaultEngine = {
+        name: this.defaultEngine.name,
+        icon: this.defaultEngine.icon,
+      };
     }
   },
 
   _onMsgState: function (state) {
-    this.defaultEngine = {
-      name: state.currentEngine.name,
-      icon: this._getFaviconURIFromBuffer(state.currentEngine.iconBuffer),
-    };
     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;
+    }
+    this.defaultEngine = state.currentEngine;
   },
 
   _onMsgCurrentState: function (state) {
     this._onMsgState(state);
   },
 
   _onMsgCurrentEngine: function (engine) {
-    this.defaultEngine = {
-      name: engine.name,
-      icon: this._getFaviconURIFromBuffer(engine.iconBuffer),
-    };
-    if (!this._table.hidden) {
-      this._setUpOneOffButtons();
-      return;
-    }
+    this.defaultEngine = engine;
     this._pendingOneOffRefresh = true;
   },
 
   _onMsgStrings: function (strings) {
     this._strings = strings;
     this._updateDefaultEngineHeader();
     this._updateSearchWithHeader();
     document.getElementById("contentSearchSettingsButton").textContent =
       this._strings.searchSettings;
   },
 
   _updateDefaultEngineHeader: function () {
     let header = document.getElementById("contentSearchDefaultEngineHeader");
-    if (this.defaultEngine.icon) {
-      header.firstChild.setAttribute("src", this.defaultEngine.icon);
-    }
+    header.firstChild.setAttribute("src", this.defaultEngine.icon);
     if (!this._strings) {
       return;
     }
     while (header.firstChild.nextSibling) {
       header.firstChild.nextSibling.remove();
     }
     header.appendChild(document.createTextNode(
       this._strings.searchHeader.replace("%S", this.defaultEngine.name)));
@@ -540,16 +680,24 @@ ContentSearchUIController.prototype = {
   // Converts favicon array buffer into data URI of the right size and dpi.
   _getFaviconURIFromBuffer: function (buffer) {
     let blob = new Blob([buffer]);
     let dpiSize = Math.round(16 * window.devicePixelRatio);
     let sizeStr = dpiSize + "," + dpiSize;
     return URL.createObjectURL(blob) + "#-moz-resolution=" + sizeStr;
   },
 
+  // Adds "@2x" to the name of the given PNG url for "retina" screens.
+  _getImageURIForCurrentResolution: function (uri) {
+    if (window.devicePixelRatio > 1) {
+      return uri.replace(/\.png$/, "@2x.png");
+    }
+    return uri;
+  },
+
   _getSearchEngines: function () {
     this._sendMsg("GetState");
   },
 
   _getStrings: function () {
     this._sendMsg("GetStrings");
   },
 
@@ -567,16 +715,19 @@ ContentSearchUIController.prototype = {
   _clearSuggestionRows: function() {
     while (this._suggestionsList.firstElementChild) {
       this._suggestionsList.firstElementChild.remove();
     }
   },
 
   _hideSuggestions: function () {
     this.input.setAttribute("aria-expanded", "false");
+    this.selectedIndex = -1;
+    this.selectedButtonIndex = -1;
+    this._currentEngineIndex = -1;
     this._table.hidden = true;
   },
 
   _indexOfTableItem: function (elt) {
     if (elt.classList.contains("contentSearchOneOffItem")) {
       return this.numSuggestions + this._oneOffButtons.indexOf(elt);
     }
     if (elt.classList.contains("contentSearchSettingsButton")) {
@@ -600,34 +751,29 @@ ContentSearchUIController.prototype = {
 
     // When the search input box loses focus, we want to immediately give focus
     // back to it if the blur was because the user clicked somewhere in the table.
     // onBlur uses the _mousedown flag to detect this.
     this._table.addEventListener("mousedown", () => { this._mousedown = true; });
     document.addEventListener("mouseup", () => { delete this._mousedown; });
 
     // Deselect the selected element on mouseout if it wasn't a suggestion.
-    this._table.addEventListener("mouseout", () => {
-      if (this.selectedIndex >= this.numSuggestions) {
-        this.selectAndUpdateInput(-1);
-      }
-    });
+    this._table.addEventListener("mouseout", this);
 
     // If a search is loaded in the same tab, ensure the suggestions dropdown
     // is hidden immediately when the page starts loading and not when it first
     // appears, in order to provide timely feedback to the user.
     window.addEventListener("beforeunload", () => { this._hideSuggestions(); });
 
     let headerRow = document.createElementNS(HTML_NS, "tr");
     let header = document.createElementNS(HTML_NS, "td");
     headerRow.setAttribute("class", "contentSearchHeaderRow");
     header.setAttribute("class", "contentSearchHeader");
-    let img = document.createElementNS(HTML_NS, "img");
-    img.setAttribute("src", "chrome://browser/skin/search-engine-placeholder.png");
-    header.appendChild(img);
+    let iconImg = document.createElementNS(HTML_NS, "img");
+    header.appendChild(iconImg);
     header.id = "contentSearchDefaultEngineHeader";
     headerRow.appendChild(header);
     headerRow.addEventListener("click", this);
     this._table.appendChild(headerRow);
 
     let row = document.createElementNS(HTML_NS, "tr");
     row.setAttribute("class", "contentSearchSuggestionsContainer");
     let cell = document.createElementNS(HTML_NS, "td");
@@ -702,20 +848,24 @@ ContentSearchUIController.prototype = {
         row = document.createElementNS(HTML_NS, "tr");
         cell = document.createElementNS(HTML_NS, "td");
         row.setAttribute("class", "contentSearchSuggestionsContainer");
         cell.setAttribute("class", "contentSearchSuggestionsContainer");
       }
       let button = document.createElementNS(HTML_NS, "button");
       button.setAttribute("class", "contentSearchOneOffItem");
       let img = document.createElementNS(HTML_NS, "img");
-      let uri = "chrome://browser/skin/search-engine-placeholder.png";
+      let uri;
       if (engine.iconBuffer) {
         uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
       }
+      else {
+        uri = this._getImageURIForCurrentResolution(
+          "chrome://browser/skin/search-engine-placeholder.png");
+      }
       img.setAttribute("src", uri);
       button.appendChild(img);
       button.style.width = buttonWidth + "px";
       button.setAttribute("title", engine.name);
 
       button.engineName = engine.name;
       button.addEventListener("click", this);
       button.addEventListener("mousemove", this);
--- a/browser/base/content/sync/customize.js
+++ b/browser/base/content/sync/customize.js
@@ -1,25 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 
-addEventListener("load", function () {
-  // unhide the reading-list engine if readinglist is enabled (note this
-  // dialog is only used with FxA sync, so no special action is needed
-  // for legacy sync.)
-  if (Services.prefs.getBoolPref("browser.readinglist.enabled")) {
-    document.getElementById("readinglist-engine").removeAttribute("hidden");
-  }
-});
-
 addEventListener("dialogaccept", function () {
   let pane = document.getElementById("sync-customize-pane");
   // First determine what the preference for the "global" sync enabled pref
   // should be based on the engines selected.
   let prefElts = pane.querySelectorAll("preferences > preference");
   let syncEnabled = false;
   for (let elt of prefElts) {
     if (elt.name.startsWith("services.sync.") && elt.value) {
--- a/browser/base/content/sync/customize.xul
+++ b/browser/base/content/sync/customize.xul
@@ -22,18 +22,16 @@
   <prefpane id="sync-customize-pane">
     <preferences>
       <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/>
       <preference id="engine.history"   name="services.sync.engine.history"   type="bool"/>
       <preference id="engine.tabs"      name="services.sync.engine.tabs"      type="bool"/>
       <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/>
       <preference id="engine.addons"    name="services.sync.engine.addons"    type="bool"/>
       <preference id="engine.prefs"     name="services.sync.engine.prefs"     type="bool"/>
-      <!-- non Sync-Engine engines -->
-      <preference id="engine.readinglist" name="readinglist.scheduler.enabled" type="bool"/>
     </preferences>
 
     <label id="sync-customize-title" value="&syncCustomize.title;"/>
     <description id="sync-customize-subtitle"
 #ifdef XP_UNIX
                  value="&syncCustomizeUnix.description;"
 #else
                  value="&syncCustomize.description;"
@@ -48,21 +46,16 @@
                 accesskey="&engine.bookmarks.accesskey;"
                 preference="engine.bookmarks"/>
       <checkbox label="&engine.passwords.label;"
                 accesskey="&engine.passwords.accesskey;"
                 preference="engine.passwords"/>
       <checkbox label="&engine.history.label;"
                 accesskey="&engine.history.accesskey;"
                 preference="engine.history"/>
-      <checkbox id="readinglist-engine"
-                label="&engine.readinglist.label;"
-                accesskey="&engine.readinglist.accesskey;"
-                preference="engine.readinglist"
-                hidden="true"/>
       <checkbox label="&engine.addons.label;"
                 accesskey="&engine.addons.accesskey;"
                 preference="engine.addons"/>
       <checkbox label="&engine.prefs.label;"
                 accesskey="&engine.prefs.accesskey;"
                 preference="engine.prefs"/>
   </vbox>
 
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -449,16 +449,17 @@ skip-if = e10s # Bug 1100700 - test reli
 [browser_urlbarDelete.js]
 [browser_urlbarEnter.js]
 [browser_urlbarEnterAfterMouseOver.js]
 skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
 [browser_urlbarRevert.js]
 [browser_urlbarSearchSingleWordNotification.js]
 [browser_urlbarStop.js]
 [browser_urlbarTrimURLs.js]
+[browser_urlbar_autoFill_backspaced.js]
 [browser_urlbar_search_healthreport.js]
 [browser_urlbar_searchsettings.js]
 [browser_utilityOverlay.js]
 [browser_visibleFindSelection.js]
 [browser_visibleLabel.js]
 [browser_visibleTabs.js]
 [browser_visibleTabs_bookmarkAllPages.js]
 skip-if = true # Bug 1005420 - fails intermittently. also with e10s enabled: bizarre problem with hidden tab having _mouseenter called, via _setPositionalAttributes, and tab not being found resulting in 'candidate is undefined'
--- a/browser/base/content/test/general/browser_contentSearchUI.js
+++ b/browser/base/content/test/general/browser_contentSearchUI.js
@@ -4,16 +4,18 @@
 const TEST_PAGE_BASENAME = "contentSearchUI.html";
 const TEST_CONTENT_SCRIPT_BASENAME = "contentSearchUI.js";
 const TEST_ENGINE_PREFIX = "browser_searchSuggestionEngine";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 const TEST_ENGINE_2_BASENAME = "searchSuggestionEngine2.xml";
 
 const TEST_MSG = "ContentSearchUIControllerTest";
 
+requestLongerTimeout(2);
+
 add_task(function* emptyInput() {
   yield setUp();
 
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   state = yield msg("key", "VK_BACK_SPACE");
   checkState(state, "", [], -1);
@@ -97,17 +99,17 @@ add_task(function* rightLeftKeys() {
 
   state = yield msg("key", "VK_DOWN");
   checkState(state, "xfoo", ["xfoo", "xbar"], 0);
 
   // This should make the xfoo suggestion sticky.  To make sure it sticks,
   // trigger suggestions again and cycle through them by pressing Down until
   // nothing is selected again.
   state = yield msg("key", "VK_RIGHT");
-  checkState(state, "xfoo", [], 0);
+  checkState(state, "xfoo", [], -1);
 
   state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
   checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
 
   state = yield msg("key", "VK_DOWN");
   checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);
 
   state = yield msg("key", "VK_DOWN");
@@ -120,30 +122,212 @@ add_task(function* rightLeftKeys() {
   checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 3);
 
   state = yield msg("key", "VK_DOWN");
   checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
 
   yield msg("reset");
 });
 
+add_task(function* tabKey() {
+  yield setUp();
+  yield msg("key", { key: "x", waitForSuggestions: true });
+
+  let state = yield msg("key", "VK_TAB");
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "x", ["xfoo", "xbar"], 3);
+
+  state = yield msg("key", { key: "VK_TAB", modifiers: { shiftKey: true }});
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+
+  state = yield msg("key", { key: "VK_TAB", modifiers: { shiftKey: true }});
+  checkState(state, "x", [], -1);
+
+  yield setUp();
+
+  yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+
+  for (let i = 0; i < 3; ++i) {
+    state = yield msg("key", "VK_TAB");
+  }
+  checkState(state, "x", [], -1);
+
+  yield setUp();
+
+  yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xfoo", ["xfoo", "xbar"], 0, 0);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xfoo", ["xfoo", "xbar"], 0, 1);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+
+  state = yield msg("key", "VK_UP");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xbar", [], -1);
+
+  yield msg("reset");
+});
+
+add_task(function* cycleSuggestions() {
+  yield setUp();
+  yield msg("key", { key: "x", waitForSuggestions: true });
+
+  let cycle = Task.async(function* (aSelectedButtonIndex) {
+    let modifiers = {
+      shiftKey: true,
+      accelKey: true,
+    };
+  
+    let state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+    checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+    checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+    checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+    checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+    checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+    checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+    checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+    checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+  });
+  
+  yield cycle();
+
+  // Repeat with a one-off selected.
+  let state = yield msg("key", "VK_TAB");
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+  yield cycle(0);
+
+  // Repeat with the settings button selected.
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "x", ["xfoo", "xbar"], 3);
+  yield cycle(1);
+
+  yield msg("reset");
+});
+
+add_task(function* cycleOneOffs() {
+  yield setUp();
+  yield msg("key", { key: "x", waitForSuggestions: true });
+
+  yield msg("addDuplicateOneOff");
+
+  let state = yield msg("key", "VK_DOWN");
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+  let modifiers = {
+    altKey: true,
+  };
+
+  state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+  state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+  state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+  state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+  // If the settings button is selected, pressing alt+up/down should select the
+  // last/first one-off respectively (and deselect the settings button).
+  yield msg("key", "VK_TAB");
+  yield msg("key", "VK_TAB");
+  state = yield msg("key", "VK_TAB"); // Settings button selected.
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
+
+  state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
+
+  state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+  yield msg("removeLastOneOff");
+  yield msg("reset");
+});
+
 add_task(function* mouse() {
   yield setUp();
 
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
-  for (let i = 0; i < 4; ++i) {
-    state = yield msg("mousemove", i);
-    checkState(state, "x", ["xfoo", "xbar"], i);
-  }
+  state = yield msg("mousemove", 0);
+  checkState(state, "x", ["xfoo", "xbar"], 0);
+
+  state = yield msg("mousemove", 1);
+  checkState(state, "x", ["xfoo", "xbar"], 1);
+
+  state = yield msg("mousemove", 2);
+  checkState(state, "x", ["xfoo", "xbar"], 1, 0);
+
+  state = yield msg("mousemove", 3);
+  checkState(state, "x", ["xfoo", "xbar"], 1, 1);
 
   state = yield msg("mousemove", -1);
+  checkState(state, "x", ["xfoo", "xbar"], 1);
+
+  yield msg("reset");
+  yield setUp();
+
+  state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
+  state = yield msg("mousemove", 0);
+  checkState(state, "x", ["xfoo", "xbar"], 0);
+
+  state = yield msg("mousemove", 2);
+  checkState(state, "x", ["xfoo", "xbar"], 0, 0);
+
+  state = yield msg("mousemove", -1);
+  checkState(state, "x", ["xfoo", "xbar"], 0);
+
   yield msg("reset");
 });
 
 add_task(function* formHistory() {
   yield setUp();
 
   // Type an X and add it to form history.
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
@@ -192,16 +376,44 @@ add_task(function* formHistory() {
 
   // Type an X again.  The form history entry should still be gone.
   state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   yield msg("reset");
 });
 
+add_task(function* cycleEngines() {
+  yield setUp();
+  yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+
+  let promiseEngineChange = function(newEngineName) {
+    let deferred = Promise.defer();
+    Services.obs.addObserver(function resolver(subj, topic, data) {
+      if (data != "engine-current") {
+        return;
+      }
+      SimpleTest.is(subj.name, newEngineName, "Engine cycled correctly");
+      Services.obs.removeObserver(resolver, "browser-search-engine-modified");
+      deferred.resolve();
+    }, "browser-search-engine-modified", false);
+    return deferred.promise;
+  }
+
+  let p = promiseEngineChange(TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME);
+  yield msg("key", { key: "VK_DOWN", modifiers: { accelKey: true }});
+  yield p;
+
+  p = promiseEngineChange(TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME);
+  yield msg("key", { key: "VK_UP", modifiers: { accelKey: true }});
+  yield p;
+
+  yield msg("reset");
+});
+
 add_task(function* search() {
   yield setUp();
 
   let modifiers = {};
   ["altKey", "ctrlKey", "metaKey", "shiftKey"].forEach(k => modifiers[k] = true);
 
   // Test typing a query and pressing enter.
   let p = msg("waitForSearch");
@@ -292,29 +504,67 @@ add_task(function* search() {
   eventData.searchString = "x";
   eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME;
   delete eventData.selection;
   SimpleTest.isDeeply(eventData, mesg, "Search event data");
 
   yield promiseTab();
   yield setUp();
 
+  // Test selecting a suggestion, then clicking a one-off without deselecting the
+  // suggestion.
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  p = msg("waitForSearch");
+  yield msg("mousemove", 1);
+  yield msg("mousemove", 3);
+  yield msg("click", { eltIdx: 3, modifiers: modifiers });
+  mesg = yield p;
+  eventData.searchString = "xfoo"
+  eventData.selection = {
+    index: 1,
+    kind: "mouse",
+  };
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
+  // Same as above, but with the keyboard.
+  delete modifiers.button;
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  p = msg("waitForSearch");
+  yield msg("key", "VK_DOWN");
+  yield msg("key", "VK_DOWN");
+  yield msg("key", "VK_TAB");
+  yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+  mesg = yield p;
+  eventData.selection = {
+    index: 1,
+    kind: "key",
+  };
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
   // Test searching when using IME composition.
   let state = yield msg("startComposition", { data: "" });
   checkState(state, "", [], -1);
   state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
   checkState(state, "x", [{ str: "x", type: "formHistory" },
                           { str: "xfoo", type: "formHistory" }, "xbar"], -1);
   yield msg("commitComposition");
   delete modifiers.button;
   p = msg("waitForSearch");
   yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
   mesg = yield p;
+  eventData.searchString = "x"
   eventData.originalEvent = modifiers;
   eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
+  delete eventData.selection;
   SimpleTest.isDeeply(eventData, mesg, "Search event data");
 
   yield promiseTab();
   yield setUp();
 
   state = yield msg("startComposition", { data: "" });
   checkState(state, "", [], -1);
   state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
@@ -423,36 +673,49 @@ function msg(type, data=null) {
     }
     gMsgMan.removeMessageListener(TEST_MSG, onMsg);
     deferred.resolve(msg.data.data);
   });
   return deferred.promise;
 }
 
 function checkState(actualState, expectedInputVal, expectedSuggestions,
-                    expectedSelectedIdx) {
+                    expectedSelectedIdx, expectedSelectedButtonIdx) {
   expectedSuggestions = expectedSuggestions.map(sugg => {
     return typeof(sugg) == "object" ? sugg : {
       str: sugg,
       type: "remote",
     };
   });
 
+  if (expectedSelectedIdx == -1 && expectedSelectedButtonIdx != undefined) {
+    expectedSelectedIdx = expectedSuggestions.length + expectedSelectedButtonIdx;
+  }
+  
   let expectedState = {
     selectedIndex: expectedSelectedIdx,
     numSuggestions: expectedSuggestions.length,
     suggestionAtIndex: expectedSuggestions.map(s => s.str),
     isFormHistorySuggestionAtIndex:
       expectedSuggestions.map(s => s.type == "formHistory"),
 
     tableHidden: expectedSuggestions.length == 0,
 
     inputValue: expectedInputVal,
     ariaExpanded: expectedSuggestions.length == 0 ? "false" : "true",
   };
+  if (expectedSelectedButtonIdx != undefined) {
+    expectedState.selectedButtonIndex = expectedSelectedButtonIdx;
+  }
+  else if (expectedSelectedIdx < expectedSuggestions.length) {
+    expectedState.selectedButtonIndex = -1;
+  }
+  else {
+    expectedState.selectedButtonIndex = expectedSelectedIdx - expectedSuggestions.length;
+  }
 
   SimpleTest.isDeeply(actualState, expectedState, "State");
 }
 
 var gMsgMan;
 
 function promiseTab() {
   let deferred = Promise.defer();
--- a/browser/base/content/test/general/browser_readerMode.js
+++ b/browser/base/content/test/general/browser_readerMode.js
@@ -1,21 +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/. */
 
 /**
  * Test that the reader mode button appears and works properly on
- * reader-able content, and that ReadingList button can open and close
- * its Sidebar UI.
+ * reader-able content.
  */
 const TEST_PREFS = [
   ["reader.parse-on-load.enabled", true],
-  ["browser.readinglist.enabled", true],
-  ["browser.readinglist.introShown", false],
 ];
 
 const TEST_PATH = "http://example.com/browser/browser/base/content/test/general/";
 
 let readerButton = document.getElementById("reader-mode-button");
 
 add_task(function* test_reader_button() {
   registerCleanupFunction(function() {
@@ -58,36 +55,16 @@ add_task(function* test_reader_button() 
 
   let readerUrl = gBrowser.selectedBrowser.currentURI.spec;
   ok(readerUrl.startsWith("about:reader"), "about:reader loaded after clicking reader mode button");
   is_element_visible(readerButton, "Reader mode button is present on about:reader");
 
   is(gURLBar.value, readerUrl, "gURLBar value is about:reader URL");
   is(gURLBar.textValue, url.substring("http://".length), "gURLBar is displaying original article URL");
 
-  // Readinglist button should be present, and status should be "openned", as the
-  // first time in readerMode opens the Sidebar ReadingList as a feature introduction.
-  let listButton;
-  yield promiseWaitForCondition(() =>
-    listButton = gBrowser.contentDocument.getElementById("list-button"));
-  is_element_visible(listButton, "List button is present on a reader-able page");
-  yield promiseWaitForCondition(() => listButton.classList.contains("on"));
-  ok(listButton.classList.contains("on"),
-    "List button should indicate SideBar-ReadingList open.");
-  ok(ReadingListUI.isSidebarOpen,
-    "The ReadingListUI should indicate SideBar-ReadingList open.");
-
-  // Now close the Sidebar ReadingList.
-  listButton.click();
-  yield promiseWaitForCondition(() => !listButton.classList.contains("on"));
-  ok(!listButton.classList.contains("on"),
-    "List button should now indicate SideBar-ReadingList closed.");
-  ok(!ReadingListUI.isSidebarOpen,
-    "The ReadingListUI should now indicate SideBar-ReadingList closed.");
-
   // Switch page back out of reader mode.
   readerButton.click();
   yield promiseTabLoadEvent(tab);
   is(gBrowser.selectedBrowser.currentURI.spec, url,
     "Original page loaded after clicking active reader mode button");
 
   // Load a new tab that is NOT reader-able.
   let newTab = gBrowser.selectedTab = gBrowser.addTab();
--- a/browser/base/content/test/general/browser_readerMode_hidden_nodes.js
+++ b/browser/base/content/test/general/browser_readerMode_hidden_nodes.js
@@ -1,16 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
  * Test that the reader mode button appears and works properly on
- * reader-able content, and that ReadingList button can open and close
- * its Sidebar UI.
+ * reader-able content.
  */
 const TEST_PREFS = [
   ["reader.parse-on-load.enabled", true],
   ["browser.reader.detectedFirstArticle", false],
 ];
 
 const TEST_PATH = "http://example.com/browser/browser/base/content/test/general/";
 
--- a/browser/base/content/test/general/browser_syncui.js
+++ b/browser/base/content/test/general/browser_syncui.js
@@ -1,16 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
 let {Weave} = Cu.import("resource://services-sync/main.js", {});
 let {Notifications} = Cu.import("resource://services-sync/notifications.js", {});
-// The BackStagePass allows us to get this test-only non-exported function.
-let {getInternalScheduler} = Cu.import("resource:///modules/readinglist/Scheduler.jsm", {});
 
 let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]
                    .getService(Ci.nsIStringBundleService)
                    .createBundle("chrome://weave/locale/services/sync.properties");
 
 // ensure test output sees log messages.
 Log.repository.getLogger("browserwindow.syncui").addAppender(new Log.DumpAppender());
 
@@ -32,33 +30,16 @@ add_task(function* prepare() {
   // mock out the "_needsSetup()" function so we don't short-circuit.
   let oldNeedsSetup = window.gSyncUI._needsSetup;
   window.gSyncUI._needsSetup = () => false;
   registerCleanupFunction(() => {
     window.gSyncUI._needsSetup = oldNeedsSetup;
   });
 });
 
-add_task(function* testNotProlongedRLErrorWhenDisabled() {
-  // Here we arrange for the (dead?) readinglist scheduler to have a last-synced
-  // date of long ago and the RL scheduler is disabled.
-  // gSyncUI.isProlongedReadingListError() should return false.
-  // Pretend the reading-list is in the "prolonged error" state.
-  let longAgo = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000); // 100 days ago.
-  Services.prefs.setCharPref("readinglist.scheduler.lastSync", longAgo.toString());
-
-  // It's prolonged while it's enabled.
-  Services.prefs.setBoolPref("readinglist.scheduler.enabled", true);
-  Assert.equal(gSyncUI.isProlongedReadingListError(), true);
-
-  // But false when disabled.
-  Services.prefs.setBoolPref("readinglist.scheduler.enabled", false);
-  Assert.equal(gSyncUI.isProlongedReadingListError(), false);
-});
-
 add_task(function* testProlongedSyncError() {
   let promiseNotificationAdded = promiseObserver("weave:notification:added");
   Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
 
   // Pretend we are in the "prolonged error" state.
   Weave.Status.sync = Weave.PROLONGED_SYNC_FAILURE;
   Weave.Status.login = Weave.LOGIN_SUCCEEDED;
   Services.obs.notifyObservers(null, "weave:ui:sync:error", null);
@@ -71,42 +52,16 @@ add_task(function* testProlongedSyncErro
   // Now pretend we just had a successful sync - the error notification should go away.
   let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
   Weave.Status.sync = Weave.STATUS_OK;
   Services.obs.notifyObservers(null, "weave:ui:sync:finish", null);
   yield promiseNotificationRemoved;
   Assert.equal(Notifications.notifications.length, 0, "no notifications left");
 });
 
-add_task(function* testProlongedRLError() {
-  Services.prefs.setBoolPref("readinglist.scheduler.enabled", true);
-  let promiseNotificationAdded = promiseObserver("weave:notification:added");
-  Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
-
-  // Pretend the reading-list is in the "prolonged error" state.
-  let longAgo = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000); // 100 days ago.
-  Services.prefs.setCharPref("readinglist.scheduler.lastSync", longAgo.toString());
-  getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_OTHER;
-  Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-  Services.obs.notifyObservers(null, "readinglist:sync:error", null);
-
-  let subject = yield promiseNotificationAdded;
-  let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
-  Assert.equal(notification.title, stringBundle.GetStringFromName("error.sync.title"));
-  Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
-
-  // Now pretend we just had a successful sync - the error notification should go away.
-  let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
-  Services.prefs.setCharPref("readinglist.scheduler.lastSync", Date.now().toString());
-  Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-  Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
-  yield promiseNotificationRemoved;
-  Assert.equal(Notifications.notifications.length, 0, "no notifications left");
-});
-
 add_task(function* testSyncLoginError() {
   let promiseNotificationAdded = promiseObserver("weave:notification:added");
   Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
 
   // Pretend we are in the "prolonged error" state.
   Weave.Status.sync = Weave.LOGIN_FAILED;
   Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED;
   Services.obs.notifyObservers(null, "weave:ui:sync:error", null);
@@ -150,23 +105,17 @@ add_task(function* testSyncLoginNetworkE
   Services.obs.addObserver(obs, "weave:notification:added", false);
   try {
     // notify of a display-able error - we should synchronously see our flag set.
     Weave.Status.sync = Weave.LOGIN_FAILED;
     Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED;
     Services.obs.notifyObservers(null, "weave:ui:login:error", null);
     Assert.ok(sawNotificationAdded);
 
-    // clear the notification.
-    let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
-    Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-    Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
-    yield promiseNotificationRemoved;
-
-    // cool - so reset the flag and test what should *not* show an error.
+    // reset the flag and test what should *not* show an error.
     sawNotificationAdded = false;
     Weave.Status.sync = Weave.LOGIN_FAILED;
     Weave.Status.login = Weave.LOGIN_FAILED_NETWORK_ERROR;
     Services.obs.notifyObservers(null, "weave:ui:login:error", null);
     Assert.ok(!sawNotificationAdded);
 
     // ditto for LOGIN_FAILED_SERVER_ERROR
     Weave.Status.sync = Weave.LOGIN_FAILED;
@@ -174,90 +123,16 @@ add_task(function* testSyncLoginNetworkE
     Services.obs.notifyObservers(null, "weave:ui:login:error", null);
     Assert.ok(!sawNotificationAdded);
     // we are done.
   } finally {
     Services.obs.removeObserver(obs, "weave:notification:added");
   }
 });
 
-add_task(function* testRLLoginError() {
-  let promiseNotificationAdded = promiseObserver("weave:notification:added");
-  Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
-
-  // Pretend RL is in an auth error state
-  getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
-  Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-  Services.obs.notifyObservers(null, "readinglist:sync:error", null);
-
-  let subject = yield promiseNotificationAdded;
-  let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
-  Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
-  Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
-
-  // Now pretend we just had a successful sync - the error notification should go away.
-  getInternalScheduler().state = ReadingListScheduler.STATE_OK;
-  let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
-  Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-  Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
-  yield promiseNotificationRemoved;
-  Assert.equal(Notifications.notifications.length, 0, "no notifications left");
-});
-
-// Here we put readinglist into an "authentication error" state (should see
-// the error bar reflecting this), then report a prolonged error from Sync (an
-// infobar to reflect the sync error should replace it), then resolve the sync
-// error - the authentication error from readinglist should remain.
-add_task(function* testRLLoginErrorRemains() {
-  let promiseNotificationAdded = promiseObserver("weave:notification:added");
-  Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
-
-  // Pretend RL is in an auth error state
-  getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
-  Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-  Services.obs.notifyObservers(null, "readinglist:sync:error", null);
-
-  let subject = yield promiseNotificationAdded;
-  let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
-  Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
-  Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
-
-  // Now Sync into a prolonged auth error state.
-  promiseNotificationAdded = promiseObserver("weave:notification:added");
-  Weave.Status.sync = Weave.PROLONGED_SYNC_FAILURE;
-  Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED;
-  Services.obs.notifyObservers(null, "weave:ui:sync:error", null);
-  subject = yield promiseNotificationAdded;
-  // still exactly 1 notification with the "login" title.
-  notification = subject.wrappedJSObject.object;
-  Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
-  Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
-
-  // Resolve the sync problem.
-  promiseNotificationAdded = promiseObserver("weave:notification:added");
-  Weave.Status.sync = Weave.STATUS_OK;
-  Weave.Status.login = Weave.LOGIN_SUCCEEDED;
-  Services.obs.notifyObservers(null, "weave:ui:sync:finish", null);
-
-  // Expect one notification - the RL login problem.
-  subject = yield promiseNotificationAdded;
-  // still exactly 1 notification with the "login" title.
-  notification = subject.wrappedJSObject.object;
-  Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
-  Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
-
-  // and cleanup - resolve the readinglist error.
-  getInternalScheduler().state = ReadingListScheduler.STATE_OK;
-  let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
-  Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-  Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
-  yield promiseNotificationRemoved;
-  Assert.equal(Notifications.notifications.length, 0, "no notifications left");
-});
-
 function checkButtonsStatus(shouldBeActive) {
   let button = document.getElementById("sync-button");
   let fxaContainer = document.getElementById("PanelUI-footer-fxa");
   if (shouldBeActive) {
     Assert.equal(button.getAttribute("status"), "active");
     Assert.equal(fxaContainer.getAttribute("syncstatus"), "active");
   } else {
     Assert.ok(!button.hasAttribute("status"));
@@ -282,34 +157,20 @@ add_task(function* testButtonActivities(
   yield PanelUI.show();
   try {
     testButtonActions("weave:service:login:start", "weave:service:login:finish");
     testButtonActions("weave:service:login:start", "weave:service:login:error");
 
     testButtonActions("weave:service:sync:start", "weave:service:sync:finish");
     testButtonActions("weave:service:sync:start", "weave:service:sync:error");
 
-    testButtonActions("readinglist:sync:start", "readinglist:sync:finish");
-    testButtonActions("readinglist:sync:start", "readinglist:sync:error");
-
     // and ensure the counters correctly handle multiple in-flight syncs
     Services.obs.notifyObservers(null, "weave:service:sync:start", null);
     checkButtonsStatus(true);
-    Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-    checkButtonsStatus(true);
-    Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
-    // sync is still going...
-    checkButtonsStatus(true);
-    // another reading list starts
-    Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-    checkButtonsStatus(true);
-    // The initial sync stops.
+    // sync stops.
     Services.obs.notifyObservers(null, "weave:service:sync:finish", null);
-    // RL is still going...
-    checkButtonsStatus(true);
-    // RL finishes with an error, so no longer active.
-    Services.obs.notifyObservers(null, "readinglist:sync:error", null);
+    // Button should not be active.
     checkButtonsStatus(false);
   } finally {
     PanelUI.hide();
     CustomizableUI.removeWidgetFromArea("sync-button");
   }
 });
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_urlbar_autoFill_backspaced.js
@@ -0,0 +1,148 @@
+/* This test ensures that backspacing autoFilled values still allows to
+ * confirm the remaining value.
+ */
+
+function* test_autocomplete(data) {
+  let {desc, typed, autofilled, modified, keys, action, onAutoFill} = data;
+  info(desc);
+
+  yield promiseAutocompleteResultPopup(typed);
+  is(gURLBar.value, autofilled, "autofilled value is as expected");
+  if (onAutoFill)
+    onAutoFill()
+
+  keys.forEach(key => EventUtils.synthesizeKey(key, {}));
+
+  is(gURLBar.value, modified, "backspaced value is as expected");
+
+  yield promiseSearchComplete();
+
+  ok(gURLBar.popup.richlistbox.children.length > 0, "Should get at least 1 result");
+  let result = gURLBar.popup.richlistbox.children[0];
+  let type = result.getAttribute("type");
+  let types = type.split(/\s+/);
+  ok(types.includes(action), `The type attribute "${type}" includes the expected action "${action}"`);
+
+  gURLBar.popup.hidePopup();
+  yield promisePopupHidden(gURLBar.popup);
+  gURLBar.blur();
+};
+
+add_task(function* () {
+  registerCleanupFunction(function* () {
+    Services.prefs.clearUserPref("browser.urlbar.unifiedcomplete");
+    Services.prefs.clearUserPref("browser.urlbar.autoFill");
+    gURLBar.handleRevert();
+    yield PlacesTestUtils.clearHistory();
+  });
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+
+  // Add a typed visit, so it will be autofilled.
+  yield PlacesTestUtils.addVisits({
+    uri: NetUtil.newURI("http://example.com/"),
+    transition: Ci.nsINavHistoryService.TRANSITION_TYPED
+  });
+
+  yield test_autocomplete({ desc: "DELETE the autofilled part should search",
+                            typed: "exam",
+                            autofilled: "example.com/",
+                            modified: "exam",
+                            keys: ["VK_DELETE"],
+                            action: "searchengine"
+                          });
+  yield test_autocomplete({ desc: "DELETE the final slash should visit",
+                            typed: "example.com",
+                            autofilled: "example.com/",
+                            modified: "example.com",
+                            keys: ["VK_DELETE"],
+                            action: "visiturl"
+                          });
+
+  yield test_autocomplete({ desc: "BACK_SPACE the autofilled part should search",
+                            typed: "exam",
+                            autofilled: "example.com/",
+                            modified: "exam",
+                            keys: ["VK_BACK_SPACE"],
+                            action: "searchengine"
+                          });
+  yield test_autocomplete({ desc: "BACK_SPACE the final slash should visit",
+                            typed: "example.com",
+                            autofilled: "example.com/",
+                            modified: "example.com",
+                            keys: ["VK_BACK_SPACE"],
+                            action: "visiturl"
+                          });
+
+  yield test_autocomplete({ desc: "DELETE the autofilled part, then BACK_SPACE, should search",
+                            typed: "exam",
+                            autofilled: "example.com/",
+                            modified: "exa",
+                            keys: ["VK_DELETE", "VK_BACK_SPACE"],
+                            action: "searchengine"
+                          });
+  yield test_autocomplete({ desc: "DELETE the final slash, then BACK_SPACE, should search",
+                            typed: "example.com",
+                            autofilled: "example.com/",
+                            modified: "example.co",
+                            keys: ["VK_DELETE", "VK_BACK_SPACE"],
+                            action: "visiturl"
+                          });
+
+  yield test_autocomplete({ desc: "BACK_SPACE the autofilled part, then BACK_SPACE, should search",
+                            typed: "exam",
+                            autofilled: "example.com/",
+                            modified: "exa",
+                            keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
+                            action: "searchengine"
+                          });
+  yield test_autocomplete({ desc: "BACK_SPACE the final slash, then BACK_SPACE, should search",
+                            typed: "example.com",
+                            autofilled: "example.com/",
+                            modified: "example.co",
+                            keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
+                            action: "visiturl"
+                          });
+
+  yield test_autocomplete({ desc: "BACK_SPACE after blur should search",
+                            typed: "ex",
+                            autofilled: "example.com/",
+                            modified: "e",
+                            keys: ["VK_BACK_SPACE"],
+                            action: "searchengine",
+                            onAutoFill: () => {
+                              gURLBar.blur();
+                              gURLBar.focus();
+                              gURLBar.selectionStart = 1;
+                              gURLBar.selectionEnd = 12;
+                            }
+                         });
+  yield test_autocomplete({ desc: "DELETE after blur should search",
+                            typed: "ex",
+                            autofilled: "example.com/",
+                            modified: "e",
+                            keys: ["VK_DELETE"],
+                            action: "searchengine",
+                            onAutoFill: () => {
+                              gURLBar.blur();
+                              gURLBar.focus();
+                              gURLBar.selectionStart = 1;
+                              gURLBar.selectionEnd = 12;
+                            }
+                          });
+  yield test_autocomplete({ desc: "double BACK_SPACE after blur should search",
+                            typed: "ex",
+                            autofilled: "example.com/",
+                            modified: "e",
+                            keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
+                            action: "searchengine",
+                            onAutoFill: () => {
+                              gURLBar.blur();
+                              gURLBar.focus();
+                              gURLBar.selectionStart = 2;
+                              gURLBar.selectionEnd = 12;
+                            }
+                         });
+
+  yield PlacesTestUtils.clearHistory();
+});
--- a/browser/base/content/test/general/contentSearchUI.js
+++ b/browser/base/content/test/general/contentSearchUI.js
@@ -91,18 +91,21 @@ let messageHandlers = {
                      ...gController._oneOffButtons,
                      content.document.getElementById("contentSearchSettingsButton")];
       row = allElts[itemIndex];
     }
     let event = {
       type: "mousemove",
       clickcount: 0,
     }
+    row.addEventListener("mousemove", function handler() {
+      row.removeEventListener("mousemove", handler);
+      ack("mousemove"); 
+    });
     content.synthesizeMouseAtCenter(row, event);
-    ack("mousemove");
   },
 
   click: function (arg) {
     let eltIdx = typeof(arg) == "object" ? arg.eltIdx : arg;
     let row;
     if (eltIdx == -1) {
       row = gController._table.firstChild;
     }
@@ -119,21 +122,37 @@ let messageHandlers = {
     ack("click");
   },
 
   addInputValueToFormHistory: function () {
     gController.addInputValueToFormHistory();
     ack("addInputValueToFormHistory");
   },
 
+  addDuplicateOneOff: function () {
+    let btn = gController._oneOffButtons[gController._oneOffButtons.length - 1];
+    let newBtn = btn.cloneNode(true);
+    btn.parentNode.appendChild(newBtn);
+    gController._oneOffButtons.push(newBtn);
+    ack("addDuplicateOneOff");
+  },
+
+  removeLastOneOff: function () {
+    gController._oneOffButtons.pop().remove();
+    ack("removeLastOneOff");
+  },
+
   reset: function () {
-    // Reset both the input and suggestions by select all + delete.
+    // Reset both the input and suggestions by select all + delete. If there was
+    // no text entered, this won't have any effect, so also escape to ensure the
+    // suggestions table is closed.
     gController.input.focus();
     content.synthesizeKey("a", { accelKey: true });
     content.synthesizeKey("VK_DELETE", {});
+    content.synthesizeKey("VK_ESCAPE", {});
     ack("reset");
   },
 };
 
 function ack(aType, aData) {
   sendAsyncMessage(TEST_MSG, { type: aType, data: aData || currentState() });
 }
 
@@ -160,16 +179,17 @@ function waitForContentSearchEvent(messa
     mm.removeMessageListener("ContentSearch", listener);
     cb(aMsg.data.data);
   });
 }
 
 function currentState() {
   let state = {
     selectedIndex: gController.selectedIndex,
+    selectedButtonIndex: gController.selectedButtonIndex,
     numSuggestions: gController._table.hidden ? 0 : gController.numSuggestions,
     suggestionAtIndex: [],
     isFormHistorySuggestionAtIndex: [],
 
     tableHidden: gController._table.hidden,
 
     inputValue: gController.input.value,
     ariaExpanded: gController.input.getAttribute("aria-expanded"),
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -134,27 +134,16 @@
         <toolbarbutton id="panelMenu_bookmarksToolbar"
                        label="&personalbarCmd.label;"
                        class="subviewbutton cui-withicon"
                        oncommand="PlacesCommandHook.showPlacesOrganizer('BookmarksToolbar'); PanelUI.hide();"/>
         <toolbarbutton id="panelMenu_unsortedBookmarks"
                        label="&unsortedBookmarksCmd.label;"
                        class="subviewbutton cui-withicon"
                        oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks'); PanelUI.hide();"/>
-        <toolbarseparator>
-          <observes element="readingListSidebar" attribute="hidden"/>
-        </toolbarseparator>
-        <toolbarbutton id="panelMenu_viewReadingListSidebar"
-                       label="&readingList.showSidebar.label;"
-                       class="subviewbutton"
-                       key="key_readingListSidebar"
-                       oncommand="SidebarUI.toggle('readingListSidebar'); PanelUI.hide();">
-          <observes element="readingListSidebar" attribute="checked"/>
-          <observes element="readingListSidebar" attribute="hidden"/>
-        </toolbarbutton>
         <toolbarseparator class="small-separator"/>
         <toolbaritem id="panelMenu_bookmarksMenu"
                      orient="vertical"
                      smoothscroll="false"
                      onclick="if (event.button == 1) BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
                      oncommand="BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
                      flatList="true"
                      tooltip="bhTooltip">
--- a/browser/components/loop/.eslintignore
+++ b/browser/components/loop/.eslintignore
@@ -17,13 +17,14 @@ test/node_modules
 # These are generated react files that we don't need to check
 content/js/contacts.js
 content/js/conversation.js
 content/js/conversationViews.js
 content/js/panel.js
 content/js/roomViews.js
 content/js/feedbackViews.js
 content/shared/js/textChatView.js
+content/shared/js/linkifiedTextView.js
 content/shared/js/views.js
 standalone/content/js/fxOSMarketplace.js
 standalone/content/js/standaloneRoomViews.js
 standalone/content/js/webapp.js
 ui/ui-showcase.js
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -34,16 +34,18 @@
     <script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/roomStates.js"></script>
     <script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
     <script type="text/javascript" src="loop/js/feedbackViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/textChatStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
+    <script type="text/javascript" src="loop/shared/js/linkifiedTextView.js"></script>
+    <script type="text/javascript" src="loop/shared/js/urlRegExps.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/websocket.js"></script>
     <script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/js/roomStore.js"></script>
     <script type="text/javascript" src="loop/js/roomViews.js"></script>
     <script type="text/javascript" src="loop/js/conversation.js"></script>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/linkifiedTextView.js
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var loop = loop || {};
+loop.shared = loop.shared || {};
+loop.shared.views = loop.shared.views || {};
+loop.shared.views.LinkifiedTextView = (function(mozL10n) {
+  "use strict";
+
+  /**
+   * Given a rawText property, renderer a version of that text with any
+   * links starting with http://, https://, or ftp:// as actual clickable
+   * links inside a <p> container.
+   */
+  var LinkifiedTextView = React.createClass({displayName: "LinkifiedTextView",
+    propTypes: {
+      // Call this instead of allowing the default <a> click semantics, if
+      // given.  Also causes sendReferrer and suppressTarget attributes to be
+      // ignored.
+      linkClickHandler: React.PropTypes.func,
+      // The text to be linkified.
+      rawText: React.PropTypes.string.isRequired,
+      // Should the links send a referrer?  Defaults to false.
+      sendReferrer: React.PropTypes.bool,
+      // Should we suppress target="_blank" on the link? Defaults to false.
+      // Mostly for testing use.
+      suppressTarget: React.PropTypes.bool
+    },
+
+    mixins: [
+      React.addons.PureRenderMixin
+    ],
+
+    _handleClickEvent: function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      this.props.linkClickHandler(e.currentTarget.href);
+    },
+
+    _generateLinkAttributes: function(href) {
+      var linkAttributes = {
+        href: href
+      };
+
+      if (this.props.linkClickHandler) {
+        linkAttributes.onClick = this._handleClickEvent;
+
+        // if this is specified, we short-circuit return to avoid unnecessarily
+        // creating target and rel attributes.
+        return linkAttributes;
+      }
+
+      if (!this.props.suppressTarget) {
+        linkAttributes.target = "_blank";
+      }
+
+      if (!this.props.sendReferrer) {
+        linkAttributes.rel = "noreferrer";
+      }
+
+      return linkAttributes;
+    },
+
+    /**                                                              a
+     * Parse the given string into an array of strings and React <a> elements
+     * in the order in which they should be rendered (i.e. FIFO).
+     *
+     * @param {String} s the raw string to be parsed
+     *
+     * @returns {Array} of strings and React <a> elements in order.
+     */
+    parseStringToElements: function(s) {
+      var elements = [];
+      var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
+      var reactElementsCounter = 0; // For giving keys to each ReactElement.
+
+      while (result) {
+        // If there's text preceding the first link, push it onto the array
+        // and update the string pointer.
+        if (result.index) {
+          elements.push(s.substr(0, result.index));
+          s = s.substr(result.index);
+        }
+
+        // Push the first link itself, and advance the string pointer again.
+        elements.push(
+          React.createElement("a", React.__spread({},   this._generateLinkAttributes(result[0]) , 
+            {key: reactElementsCounter++}), 
+            result[0]
+          )
+        );
+        s = s.substr(result[0].length);
+
+        // Check for another link, and perhaps continue...
+        result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
+      }
+
+      if (s) {
+        elements.push(s);
+      }
+
+      return elements;
+    },
+
+    render: function () {
+      return ( React.createElement("p", null,  this.parseStringToElements(this.props.rawText) ) );
+    }
+  });
+
+  return LinkifiedTextView;
+
+})(navigator.mozL10n || document.mozL10n);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/linkifiedTextView.jsx
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var loop = loop || {};
+loop.shared = loop.shared || {};
+loop.shared.views = loop.shared.views || {};
+loop.shared.views.LinkifiedTextView = (function(mozL10n) {
+  "use strict";
+
+  /**
+   * Given a rawText property, renderer a version of that text with any
+   * links starting with http://, https://, or ftp:// as actual clickable
+   * links inside a <p> container.
+   */
+  var LinkifiedTextView = React.createClass({
+    propTypes: {
+      // Call this instead of allowing the default <a> click semantics, if
+      // given.  Also causes sendReferrer and suppressTarget attributes to be
+      // ignored.
+      linkClickHandler: React.PropTypes.func,
+      // The text to be linkified.
+      rawText: React.PropTypes.string.isRequired,
+      // Should the links send a referrer?  Defaults to false.
+      sendReferrer: React.PropTypes.bool,
+      // Should we suppress target="_blank" on the link? Defaults to false.
+      // Mostly for testing use.
+      suppressTarget: React.PropTypes.bool
+    },
+
+    mixins: [
+      React.addons.PureRenderMixin
+    ],
+
+    _handleClickEvent: function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      this.props.linkClickHandler(e.currentTarget.href);
+    },
+
+    _generateLinkAttributes: function(href) {
+      var linkAttributes = {
+        href: href
+      };
+
+      if (this.props.linkClickHandler) {
+        linkAttributes.onClick = this._handleClickEvent;
+
+        // if this is specified, we short-circuit return to avoid unnecessarily
+        // creating target and rel attributes.
+        return linkAttributes;
+      }
+
+      if (!this.props.suppressTarget) {
+        linkAttributes.target = "_blank";
+      }
+
+      if (!this.props.sendReferrer) {
+        linkAttributes.rel = "noreferrer";
+      }
+
+      return linkAttributes;
+    },
+
+    /**                                                              a
+     * Parse the given string into an array of strings and React <a> elements
+     * in the order in which they should be rendered (i.e. FIFO).
+     *
+     * @param {String} s the raw string to be parsed
+     *
+     * @returns {Array} of strings and React <a> elements in order.
+     */
+    parseStringToElements: function(s) {
+      var elements = [];
+      var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
+      var reactElementsCounter = 0; // For giving keys to each ReactElement.
+
+      while (result) {
+        // If there's text preceding the first link, push it onto the array
+        // and update the string pointer.
+        if (result.index) {
+          elements.push(s.substr(0, result.index));
+          s = s.substr(result.index);
+        }
+
+        // Push the first link itself, and advance the string pointer again.
+        elements.push(
+          <a { ...this._generateLinkAttributes(result[0]) }
+            key={reactElementsCounter++}>
+            {result[0]}
+          </a>
+        );
+        s = s.substr(result[0].length);
+
+        // Check for another link, and perhaps continue...
+        result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
+      }
+
+      if (s) {
+        elements.push(s);
+      }
+
+      return elements;
+    },
+
+    render: function () {
+      return ( <p>{ this.parseStringToElements(this.props.rawText) }</p> );
+    }
+  });
+
+  return LinkifiedTextView;
+
+})(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -51,19 +51,25 @@ loop.shared.views.chat = (function(mozL1
       var classes = React.addons.classSet({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
         "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
+      var optionalProps = {};
+      if (navigator.mozLoop) {
+        optionalProps.linkClickHandler = navigator.mozLoop.openURL;
+      }
+
       return (
         React.createElement("div", {className: classes}, 
-          React.createElement("p", null, this.props.message), 
+          React.createElement(sharedViews.LinkifiedTextView, React.__spread({},  optionalProps, 
+            {rawText: this.props.message})), 
           React.createElement("span", {className: "text-chat-arrow"}), 
           this.props.showTimestamp ? this._renderTimestamp() : null
         )
       );
     }
   });
 
   var TextChatRoomName = React.createClass({displayName: "TextChatRoomName",
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -51,19 +51,25 @@ loop.shared.views.chat = (function(mozL1
       var classes = React.addons.classSet({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
         "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
+      var optionalProps = {};
+      if (navigator.mozLoop) {
+        optionalProps.linkClickHandler = navigator.mozLoop.openURL;
+      }
+
       return (
         <div className={classes}>
-          <p>{this.props.message}</p>
+          <sharedViews.LinkifiedTextView {...optionalProps}
+            rawText={this.props.message} />
           <span className="text-chat-arrow" />
           {this.props.showTimestamp ? this._renderTimestamp() : null}
         </div>
       );
     }
   });
 
   var TextChatRoomName = React.createClass({
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/urlRegExps.js
@@ -0,0 +1,72 @@
+// This is derived from Diego Perini's code,
+// currently available at https://gist.github.com/dperini/729294
+
+// Regular Expression for URL validation
+//
+// Original Author: Diego Perini
+// License: MIT
+//
+// Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+var loop = loop || {};
+loop.shared = loop.shared || {};
+loop.shared.urlRegExps = (function() {
+
+  "use strict";
+
+  // Some https://wiki.mozilla.org/Loop/Development/RegExpDebugging for tools
+  // if you need to debug changes to this:
+
+  var fullUrlMatch = new RegExp(
+    // Protocol identifier.
+    "(?:(?:https?|ftp)://)" +
+      // User:pass authentication.
+    "((?:\\S+(?::\\S*)?@)?" +
+    "(?:" +
+      // IP address dotted notation octets:
+      // excludes loopback network 0.0.0.0,
+      // excludes reserved space >= 224.0.0.0,
+      // excludes network & broadcast addresses,
+      // (first & last IP address of each class).
+    "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
+    "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
+    "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
+    "|" +
+      // Host name.
+    "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
+      // Domain name.
+    "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
+      // TLD identifier.
+    "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))" +
+      // Port number.
+    "(?::\\d{2,5})?" +
+      // Resource path.
+    "(?:[/?#]\\S*)?)", "i");
+
+  return {
+    fullUrlMatch: fullUrlMatch
+  };
+
+})();
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -81,18 +81,20 @@ browser.jar:
   content/browser/loop/shared/js/roomStates.js          (content/shared/js/roomStates.js)
   content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js)
   content/browser/loop/shared/js/activeRoomStore.js     (content/shared/js/activeRoomStore.js)
   content/browser/loop/shared/js/dispatcher.js          (content/shared/js/dispatcher.js)
   content/browser/loop/shared/js/models.js              (content/shared/js/models.js)
   content/browser/loop/shared/js/mixins.js              (content/shared/js/mixins.js)
   content/browser/loop/shared/js/otSdkDriver.js         (content/shared/js/otSdkDriver.js)
   content/browser/loop/shared/js/views.js               (content/shared/js/views.js)
+  content/browser/loop/shared/js/linkifiedTextView.js   (content/shared/js/linkifiedTextView.js)
   content/browser/loop/shared/js/textChatStore.js       (content/shared/js/textChatStore.js)
   content/browser/loop/shared/js/textChatView.js        (content/shared/js/textChatView.js)
+  content/browser/loop/shared/js/urlRegExps.js          (content/shared/js/urlRegExps.js)
   content/browser/loop/shared/js/utils.js               (content/shared/js/utils.js)
   content/browser/loop/shared/js/validate.js            (content/shared/js/validate.js)
   content/browser/loop/shared/js/websocket.js           (content/shared/js/websocket.js)
 
   # Shared libs
 #ifdef DEBUG
   content/browser/loop/shared/libs/react-0.12.2.js    (content/shared/libs/react-0.12.2.js)
 #else
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -139,19 +139,20 @@
     <script type="text/javascript" src="shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="shared/js/websocket.js"></script>
     <script type="text/javascript" src="shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="shared/js/store.js"></script>
     <script type="text/javascript" src="shared/js/roomStates.js"></script>
     <script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
     <script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
     <script type="text/javascript" src="shared/js/views.js"></script>
-    <script type="text/javascript" src="shared/js/feedbackViews.js"></script>
     <script type="text/javascript" src="shared/js/textChatStore.js"></script>
     <script type="text/javascript" src="shared/js/textChatView.js"></script>
+    <script type="text/javascript" src="shared/js/urlRegExps.js"></script>
+    <script type="text/javascript" src="shared/js/linkifiedTextView.js"></script>
     <script type="text/javascript" src="js/standaloneAppStore.js"></script>
     <script type="text/javascript" src="js/standaloneClient.js"></script>
     <script type="text/javascript" src="js/standaloneMozLoop.js"></script>
     <script type="text/javascript" src="js/fxOSMarketplace.js"></script>
     <script type="text/javascript" src="js/standaloneRoomViews.js"></script>
     <script type="text/javascript" src="js/standaloneMetricsStore.js"></script>
     <script type="text/javascript" src="js/webapp.js"></script>
 
--- a/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
+++ b/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
@@ -29,16 +29,18 @@ module.exports = function(config) {
     "content/shared/js/otSdkDriver.js",
     "content/shared/js/roomStates.js",
     "content/shared/js/fxOSActiveRoomStore.js",
     "content/shared/js/activeRoomStore.js",
     "content/shared/js/conversationStore.js",
     "content/shared/js/views.js",
     "content/shared/js/textChatStore.js",
     "content/shared/js/textChatView.js",
+    "content/shared/js/urlRegExps.js",
+    "content/shared/js/linkifiedTextView.js",
     "standalone/content/js/multiplexGum.js",
     "standalone/content/js/standaloneAppStore.js",
     "standalone/content/js/standaloneClient.js",
     "standalone/content/js/standaloneMozLoop.js",
     "standalone/content/js/fxOSMarketplace.js",
     "standalone/content/js/standaloneRoomViews.js",
     "standalone/content/js/standaloneMetricsStore.js",
     "standalone/content/js/webapp.js",
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -58,16 +58,18 @@
   <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/roomStates.js"></script>
   <script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
   <script src="../../content/shared/js/conversationStore.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/textChatStore.js"></script>
   <script src="../../content/shared/js/textChatView.js"></script>
+  <script src="../../content/shared/js/urlRegExps.js"></script>
+  <script src="../../content/shared/js/linkifiedTextView.js"></script>
 
   <!-- Test scripts -->
   <script src="models_test.js"></script>
   <script src="mixins_test.js"></script>
   <script src="utils_test.js"></script>
   <script src="crypto_test.js"></script>
   <script src="views_test.js"></script>
   <script src="websocket_test.js"></script>
@@ -75,16 +77,18 @@
   <script src="dispatcher_test.js"></script>
   <script src="activeRoomStore_test.js"></script>
   <script src="fxOSActiveRoomStore_test.js"></script>
   <script src="conversationStore_test.js"></script>
   <script src="otSdkDriver_test.js"></script>
   <script src="store_test.js"></script>
   <script src="textChatStore_test.js"></script>
   <script src="textChatView_test.js"></script>
+  <script src="linkifiedTextView_test.js"></script>
+
   <script>
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
         chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
       });
     });
 
     describe("Unexpected Warnings Check", function() {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/linkifiedTextView_test.js
@@ -0,0 +1,378 @@
+/*
+ * Many of these tests are ported from Autolinker.js:
+ *
+ * https://github.com/gregjacobs/Autolinker.js/blob/master/tests/AutolinkerSpec.js
+ *
+ * which is released under the MIT license.  Thanks to Greg Jacobs for his hard
+ * work there.
+ *
+ * The MIT License (MIT)
+ * Original Work Copyright (c) 2014 Gregory Jacobs (http://greg-jacobs.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to the
+ * following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+describe("loop.shared.views.LinkifiedTextView", function () {
+  "use strict";
+
+  var expect = chai.expect;
+  var LinkifiedTextView = loop.shared.views.LinkifiedTextView;
+  var TestUtils = React.addons.TestUtils;
+
+  describe("LinkifiedTextView", function () {
+    function renderToMarkup(string, extraProps) {
+      return React.renderToStaticMarkup(
+        React.createElement(
+          LinkifiedTextView,
+          _.extend({rawText: string}, extraProps)));
+    }
+
+    describe("#render", function() {
+      function testRender(testData) {
+        it(testData.desc, function() {
+          var markup = renderToMarkup(testData.rawText,
+            {suppressTarget: true, sendReferrer: true});
+
+          expect(markup).to.equal(testData.markup);
+        });
+      }
+
+      function testSkip(testData) {
+        it.skip(testData.desc, function() {
+          var markup = renderToMarkup(testData.rawText,
+            {suppressTarget: true, sendReferrer: true});
+
+          expect(markup).to.equal(testData.markup);
+        });
+      }
+
+      describe("this.props.suppressTarget", function() {
+        it("should make links w/o a target attr if suppressTarget is true",
+          function() {
+            var markup = renderToMarkup("http://example.com", {suppressTarget: true});
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com" rel="noreferrer">http://example.com</a></p>');
+          });
+
+        it("should make links with target=_blank if suppressTarget is not given",
+          function() {
+            var markup = renderToMarkup("http://example.com", {});
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
+          });
+      });
+
+      describe("this.props.sendReferrer", function() {
+        it("should make links w/o rel=noreferrer if sendReferrer is true",
+          function() {
+            var markup = renderToMarkup("http://example.com", {sendReferrer: true});
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com" target="_blank">http://example.com</a></p>');
+          });
+
+        it("should make links with rel=noreferrer if sendReferrer is not given",
+          function() {
+            var markup = renderToMarkup("http://example.com", {});
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
+          });
+      });
+
+      describe("this.props.linkClickHandler", function () {
+        function mountTestComponent(string, extraProps) {
+          return TestUtils.renderIntoDocument(
+            React.createElement(
+              LinkifiedTextView,
+              _.extend({rawText: string}, extraProps)));
+        }
+
+        it("should be called when a generated link is clicked", function () {
+          var fakeUrl = "http://example.com";
+          var linkClickHandler = sinon.stub();
+          var comp = mountTestComponent(fakeUrl, {linkClickHandler: linkClickHandler});
+
+          TestUtils.Simulate.click(comp.getDOMNode().querySelector("a"));
+
+          sinon.assert.calledOnce(linkClickHandler);
+        });
+
+        it("should cause sendReferrer and suppressTarget props to be ignored",
+          function() {
+            var fakeUrl = "http://example.com";
+            var linkClickHandler = function() {};
+
+            var markup = renderToMarkup("http://example.com", {
+              linkClickHandler: linkClickHandler,
+              sendReferrer: false,
+              suppressTarget: false
+            });
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com">http://example.com</a></p>');
+          });
+
+        describe("#_handleClickEvent", function () {
+          var fakeEvent;
+          var fakeUrl = "http://example.com";
+
+          beforeEach(function() {
+            fakeEvent = {
+              currentTarget: { href: fakeUrl },
+              preventDefault: sinon.stub(),
+              stopPropagation: sinon.stub()
+            };
+          });
+
+          it("should call preventDefault on the given event", function () {
+            function linkClickHandler() {}
+            var comp = mountTestComponent(
+              fakeUrl, {linkClickHandler: linkClickHandler});
+
+            comp._handleClickEvent(fakeEvent);
+
+            sinon.assert.calledOnce(fakeEvent.preventDefault);
+            sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
+          });
+
+          it("should call stopPropagation on the given event", function () {
+            function linkClickHandler() {}
+            var comp = mountTestComponent(
+              fakeUrl, {linkClickHandler: linkClickHandler});
+
+            comp._handleClickEvent(fakeEvent);
+
+            sinon.assert.calledOnce(fakeEvent.stopPropagation);
+            sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
+          });
+
+          it("should call this.props.linkClickHandler with event.currentTarget.href", function () {
+            var linkClickHandler = sinon.stub();
+            var comp = mountTestComponent(
+              fakeUrl, {linkClickHandler: linkClickHandler});
+
+            comp._handleClickEvent(fakeEvent);
+
+            sinon.assert.calledOnce(linkClickHandler);
+            sinon.assert.calledWithExactly(linkClickHandler, fakeUrl);
+          });
+        });
+      });
+
+      // Note that these are really integration tests with the parser and React.
+      // Since we're depending on that integration to provide us with security
+      // against various injection problems, it feels fairly important.  That
+      // said, these tests are not terribly robust in the face of markup changes
+      // in the code, and over time, some of them may want to be pushed down
+      // to only be unit tests against the parser or against
+      // parseStringToElements.  We may also want both unit and integration
+      // testing for some subset of these.
+      var tests = [
+        {
+          desc: "should only add a container to a string with no URLs",
+          rawText: "This is a test.",
+          markup: "<p>This is a test.</p>"
+        },
+        {
+          desc: "should linkify a string containing only a URL",
+          rawText: "http://example.com/",
+          markup: '<p><a href="http://example.com/">http://example.com/</a></p>'
+        },
+        {
+          desc: "should linkify a URL with text preceding it",
+          rawText: "This is a link to http://example.com",
+          markup: '<p>This is a link to <a href="http://example.com">http://example.com</a></p>'
+        },
+        {
+          desc: "should linkify a URL with text before and after",
+          rawText: "Look at http://example.com near the bottom",
+          markup: '<p>Look at <a href="http://example.com">http://example.com</a> near the bottom</p>'
+        },
+        {
+          desc: "should linkify an http URL",
+          rawText: "This is an http://example.com test",
+          markup: '<p>This is an <a href="http://example.com">http://example.com</a> test</p>'
+        },
+        {
+          desc: "should linkify an https URL",
+          rawText: "This is an https://example.com test",
+          markup: '<p>This is an <a href="https://example.com">https://example.com</a> test</p>'
+        },
+        {
+          desc: "should not linkify a data URL",
+          rawText: "This is an data:image/png;base64,iVBORw0KGgoAAA test",
+          markup: "<p>This is an data:image/png;base64,iVBORw0KGgoAAA test</p>"
+        },
+        {
+          desc: "should linkify URLs with a port number",
+          rawText: "Joe went to http://example.com:8000 today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000">http://example.com:8000</a> today.</p>'
+        },
+        {
+          desc: "should linkify URLs with a port number and a trailing slash",
+          rawText: "Joe went to http://example.com:8000/ today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000/">http://example.com:8000/</a> today.</p>'
+        },
+        {
+          desc: "should linkify URLs with a port number and a path",
+          rawText: "Joe went to http://example.com:8000/mysite/page today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000/mysite/page">http://example.com:8000/mysite/page</a> today.</p>'
+        },
+        {
+          desc: "should linkify URLs with a port number and a query string",
+          rawText: "Joe went to http://example.com:8000?page=index today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000?page=index">http://example.com:8000?page=index</a> today.</p>'
+        },
+        {
+          desc: "should linkify a URL with a port number and a hash string",
+          rawText: "Joe went to http://example.com:8000#page=index today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000#page=index">http://example.com:8000#page=index</a> today.</p>'
+        },
+        {
+          desc: "should NOT include preceding ':' intros without a space",
+          rawText: "the link:http://example.com/",
+          markup: '<p>the link:<a href="http://example.com/">http://example.com/</a></p>'
+        },
+        {
+          desc: "should NOT autolink URLs with 'javascript:' URI scheme",
+          rawText: "do not link javascript:window.alert('hi') please",
+          markup: "<p>do not link javascript:window.alert(&#x27;hi&#x27;) please</p>"
+        },
+        {
+          desc: "should NOT autolink URLs with the 'JavAscriPt:' scheme",
+          rawText: "do not link JavAscriPt:window.alert('hi') please",
+          markup: "<p>do not link JavAscriPt:window.alert(&#x27;hi&#x27;) please</p>"
+        },
+        {
+          desc: "should NOT autolink possible URLs with the 'vbscript:' URI scheme",
+          rawText: "do not link vbscript:window.alert('hi') please",
+          markup: "<p>do not link vbscript:window.alert(&#x27;hi&#x27;) please</p>"
+        },
+        {
+          desc: "should NOT autolink URLs with the 'vBsCriPt:' URI scheme",
+          rawText: "do not link vBsCriPt:window.alert('hi') please",
+          markup: "<p>do not link vBsCriPt:window.alert(&#x27;hi&#x27;) please</p>"
+        },
+        {
+          desc: "should NOT autolink a string in the form of 'version:1.0'",
+          rawText: "version:1.0",
+          markup: "<p>version:1.0</p>"
+        },
+        {
+          desc: "should linkify an ftp URL",
+          rawText: "This is an ftp://example.com test",
+          markup: '<p>This is an <a href="ftp://example.com">ftp://example.com</a> test</p>'
+        },
+
+        // We don't want to include trailing dots in URLs, even though those
+        // are valid DNS names, as that should match user intent more of the
+        // time, as well as avoid this stuff:
+        //
+        // http://saynt2day.blogspot.it/2013/03/danger-of-trailing-dot-in-domain-name.html
+        //
+        {
+          desc: "should linkify 'http://example.com.', w/o a trailing dot",
+          rawText: "Joe went to http://example.com.",
+          markup: '<p>Joe went to <a href="http://example.com">http://example.com</a>.</p>'
+        },
+        // XXX several more tests like this we could port from Autolinkify.js
+        // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+        {
+          desc: "should exclude invalid chars after domain part",
+          rawText: "Joe went to http://www.example.com's today",
+          markup: '<p>Joe went to <a href="http://www.example.com">http://www.example.com</a>&#x27;s today</p>'
+        },
+        {
+          desc: "should not linkify protocol-relative URLs",
+          rawText: "//C/Programs",
+          markup: "<p>//C/Programs</p>"
+        },
+        // do a few tests to convince ourselves that, when our code is handled
+        // HTML in the input box, the integration of our code with React should
+        // cause that to rendered to appear as HTML source code, rather than
+        // getting injected into our real HTML DOM
+        {
+          desc: "should linkify simple HTML include an href properly escaped",
+          rawText: '<p>Joe went to <a href="http://www.example.com">example</a></p>',
+          markup: '<p>&lt;p&gt;Joe went to &lt;a href=&quot;<a href="http://www.example.com">http://www.example.com</a>&quot;&gt;example&lt;/a&gt;&lt;/p&gt;</p>'
+        },
+        {
+          desc: "should linkify HTML with nested tags and resource path properly escaped",
+          rawText: '<a href="http://example.com"><img src="http://example.com" /></a>',
+          markup: '<p>&lt;a href=&quot;<a href="http://example.com">http://example.com</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com">http://example.com</a>&quot; /&gt;&lt;/a&gt;</p>'
+         }
+      ];
+
+      var skippedTests = [
+
+        // XXX lots of tests similar to below we could port:
+        // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+        {
+          desc: "should link localhost URLs with an allowed URL scheme",
+          rawText: "Joe went to http://localhost today",
+          markup: '<p>Joe went to <a href="http://localhost">localhost</a></p> today'
+        },
+        // XXX lots of tests similar to below we could port:
+        // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+        {
+          desc: "should not include a ? if at the end of a URL",
+          rawText: "Did Joe go to http://example.com?",
+          markup: '<p>Did Joe go to <a href="http://example.com">http://example.com</a>?</p>'
+        },
+        {
+          desc: "should linkify 'check out http://example.com/monkey.', w/o trailing dots",
+          rawText: "check out http://example.com/monkey...",
+          markup: '<p>check out <a href="http://example.com/monkey">http://example.com/monkey</a>...</p>'
+        },
+        // another variant of eating too many trailing characters, it includes
+        // the trailing ", which it shouldn't.  Makes links inside pasted HTML
+        // source be slightly broken. Not key for our target users, I suspect,
+        // but still...
+        {
+          desc: "should linkify HTML with nested tags and a resource path properly escaped",
+          rawText: '<a href="http://example.com"><img src="http://example.com/someImage.jpg" /></a>',
+          markup: '<p>&lt;a href=&quot;<a href="http://example.com">http://example.com</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com/someImage.jpg&quot;">http://example.com/someImage.jpg&quot;</a> /&gt;&lt;/a&gt;</p>'
+        },
+        // XXX handle domains without schemes (bug 1186245)
+        // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+        {
+          desc: "should linkify a.museum (known TLD), but not abc.qqqq",
+          rawText: "a.museum should be linked, but abc.qqqq should not",
+          markup: '<p><a href="http://a.museum">a.museum</a> should be linked, but abc.qqqq should not</p>'
+        },
+        {
+          desc: "should linkify example.xyz (known TLD), but not example.etc (unknown TLD)",
+          rawText: "example.xyz should be linked, example.etc should not",
+          rawMarkup: '<><a href="http://example.xyz">example.xyz</a> should be linked, example.etc should not</p>'
+        }
+      ];
+
+      tests.forEach(testRender);
+
+      // XXX Over time, we'll want to make these pass..
+      // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+
+      skippedTests.forEach(testSkip);
+
+    });
+  });
+});
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -245,16 +245,31 @@ describe("loop.shared.views.TextChatView
         type: CHAT_MESSAGE_TYPES.RECEIVED,
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: "foo"
       });
       var node = view.getDOMNode();
 
       expect(node.querySelector(".text-chat-entry-timestamp")).to.not.eql(null);
     });
+
+    // note that this is really an integration test to be sure that we don't
+    // inadvertently regress using LinkifiedTextView.
+    it("should linkify a URL starting with http", function (){
+      view = mountTestComponent({
+        showTimestamp: true,
+        timestamp: "2015-06-23T22:48:39.738Z",
+        type: CHAT_MESSAGE_TYPES.RECEIVED,
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Check out http://example.com and see what you think..."
+      });
+      var node = view.getDOMNode();
+
+      expect(node.querySelector("a")).to.not.eql(null);
+    });
   });
 
   describe("TextChatView", function() {
     var view, fakeServer;
 
     function mountTestComponent(extraProps) {
       var props = _.extend({
         dispatcher: dispatcher,
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -53,16 +53,18 @@
     <script src="../content/shared/js/conversationStore.js"></script>
     <script src="../content/shared/js/roomStates.js"></script>
     <script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
     <script src="../content/shared/js/activeRoomStore.js"></script>
     <script src="../content/shared/js/views.js"></script>
     <script src="../content/shared/js/textChatStore.js"></script>
     <script src="../content/js/feedbackViews.js"></script>
     <script src="../content/shared/js/textChatView.js"></script>
+    <script src="../content/shared/js/urlRegExps.js"></script>
+    <script src="../content/shared/js/linkifiedTextView.js"></script>
     <script src="../content/js/roomStore.js"></script>
     <script src="../content/js/roomViews.js"></script>
     <script src="../content/js/conversationViews.js"></script>
     <script src="../content/js/client.js"></script>
     <script src="../standalone/content/js/multiplexGum.js"></script>
     <script src="../standalone/content/js/webapp.js"></script>
     <script src="../standalone/content/js/standaloneRoomViews.js"></script>
     <script src="../standalone/content/js/fxOSMarketplace.js"></script>
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -81,19 +81,21 @@
     ]);
   };
 
   MockSDK.prototype = {
     setupStreamElements: function() {
       // Dummy function to stop warnings.
     },
 
-    sendTextChatMessage: function(message) {
+    sendTextChatMessage: function(actionData) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
-        message: message.message
+        contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+        message: actionData.message,
+        receivedTimestamp: actionData.sentTimestamp
       }));
     }
   };
 
   var mockSDK = new MockSDK();
 
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
@@ -391,51 +393,46 @@
 
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Rheet!",
     sentTimestamp: "2015-06-23T22:21:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Hi there",
-    receivedTimestamp: "2015-06-23T22:21:45.590Z"
-  }));
-  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
-    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Hello",
     receivedTimestamp: "2015-06-23T23:24:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+    message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
+    "linewrappingissuesifthecssiswrong",
+    sentTimestamp: "2015-06-23T22:23:45.590Z"
+  }));
+  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Check out this menu from DNA Pizza:" +
     " http://example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/" +
     "%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
     sentTimestamp: "2015-06-23T22:23:45.590Z"
   }));
-  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
-    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
-    "linewrappingissuesifthecssiswrong",
-    sentTimestamp: "2015-06-23T22:23:45.590Z"
-  }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "That avocado monkey-brains pie sounds tasty!",
     receivedTimestamp: "2015-06-23T22:25:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "What time should we meet?",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
-  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Cool",
-    sentTimestamp: "2015-06-23T22:27:45.590Z"
+    message: "8:00 PM",
+    receivedTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStores[0],
     textChatStore: textChatStore
   });
 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -81,19 +81,21 @@
     ]);
   };
 
   MockSDK.prototype = {
     setupStreamElements: function() {
       // Dummy function to stop warnings.
     },
 
-    sendTextChatMessage: function(message) {
+    sendTextChatMessage: function(actionData) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
-        message: message.message
+        contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+        message: actionData.message,
+        receivedTimestamp: actionData.sentTimestamp
       }));
     }
   };
 
   var mockSDK = new MockSDK();
 
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
@@ -391,51 +393,46 @@
 
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Rheet!",
     sentTimestamp: "2015-06-23T22:21:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Hi there",
-    receivedTimestamp: "2015-06-23T22:21:45.590Z"
-  }));
-  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
-    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Hello",
     receivedTimestamp: "2015-06-23T23:24:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+    message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
+    "linewrappingissuesifthecssiswrong",
+    sentTimestamp: "2015-06-23T22:23:45.590Z"
+  }));
+  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Check out this menu from DNA Pizza:" +
     " http://example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/" +
     "%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
     sentTimestamp: "2015-06-23T22:23:45.590Z"
   }));
-  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
-    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
-    "linewrappingissuesifthecssiswrong",
-    sentTimestamp: "2015-06-23T22:23:45.590Z"
-  }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "That avocado monkey-brains pie sounds tasty!",
     receivedTimestamp: "2015-06-23T22:25:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "What time should we meet?",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
-  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Cool",
-    sentTimestamp: "2015-06-23T22:27:45.590Z"
+    message: "8:00 PM",
+    receivedTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStores[0],
     textChatStore: textChatStore
   });
 
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -12,17 +12,16 @@ DIRS += [
     'extensions',
     'feeds',
     'loop',
     'migration',
     'places',
     'pocket',
     'preferences',
     'privatebrowsing',
-    'readinglist',
     'search',
     'sessionstore',
     'shell',
     'selfsupport',
     'tabview',
     'uitour',
     'translation',
 ]
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -13,19 +13,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/FxAccounts.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "fxaMigrator",
   "resource://services-sync/FxaMigrator.jsm");
 
 const PAGE_NO_ACCOUNT = 0;
 const PAGE_HAS_ACCOUNT = 1;
 const PAGE_NEEDS_UPDATE = 2;
-const PAGE_PLEASE_WAIT = 3;
-const FXA_PAGE_LOGGED_OUT = 4;
-const FXA_PAGE_LOGGED_IN = 5;
+const FXA_PAGE_LOGGED_OUT = 3;
+const FXA_PAGE_LOGGED_IN = 4;
 
 // Indexes into the "login status" deck.
 // We are in a successful verified state - everything should work!
 const FXA_LOGIN_VERIFIED = 0;
 // We have logged in to an unverified account.
 const FXA_LOGIN_UNVERIFIED = 1;
 // We are logged in locally, but the server rejected our credentials.
 const FXA_LOGIN_FAILED = 2;
@@ -63,17 +62,17 @@ let gSyncPane = {
 
     if (xps.ready) {
       this._init();
       return;
     }
 
     // it may take some time before we can determine what provider to use
     // and the state of that provider, so show the "please wait" page.
-    this.page = PAGE_PLEASE_WAIT;
+    this._showLoadPage(xps);
 
     let onUnload = function () {
       window.removeEventListener("unload", onUnload, false);
       try {
         Services.obs.removeObserver(onReady, "weave:service:ready");
       } catch (e) {}
     };
 
@@ -84,16 +83,40 @@ let gSyncPane = {
     }.bind(this);
 
     Services.obs.addObserver(onReady, "weave:service:ready", false);
     window.addEventListener("unload", onUnload, false);
 
     xps.ensureLoaded();
   },
 
+  _showLoadPage: function (xps) {
+    let username;
+    try {
+      username = Services.prefs.getCharPref("services.sync.username");
+    } catch (e) {}
+    if (!username) {
+      this.page = FXA_PAGE_LOGGED_OUT;
+    } else if (xps.fxAccountsEnabled) {
+      // Use cached values while we wait for the up-to-date values
+      let cachedComputerName;
+      try {
+        cachedComputerName = Services.prefs.getCharPref("services.sync.client.name");
+      }
+      catch (e) {
+        cachedComputerName = "";
+      }
+      document.getElementById("fxaEmailAddress1").textContent = username;
+      document.getElementById("fxaSyncComputerName").value = cachedComputerName;
+      this.page = FXA_PAGE_LOGGED_IN;
+    } else { // Old Sync
+      this.page = PAGE_HAS_ACCOUNT;
+    }
+  },
+
   _init: function () {
     let topics = ["weave:service:login:error",
                   "weave:service:login:finish",
                   "weave:service:start-over:finish",
                   "weave:service:setup-complete",
                   "weave:service:logout:finish",
                   FxAccountsCommon.ONVERIFIED_NOTIFICATION,
                   FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
@@ -290,29 +313,23 @@ let gSyncPane = {
     // service.fxAccountsEnabled is false iff sync is already configured for
     // the legacy provider.
     if (service.fxAccountsEnabled) {
       let displayNameLabel = document.getElementById("fxaDisplayName");
       let fxaEmailAddress1Label = document.getElementById("fxaEmailAddress1");
       fxaEmailAddress1Label.hidden = false;
       displayNameLabel.hidden = true;
 
-      // unhide the reading-list engine if readinglist is enabled (note we do
-      // it here as it must remain disabled for legacy sync users)
-      if (Services.prefs.getBoolPref("browser.readinglist.enabled")) {
-        document.getElementById("readinglist-engine").removeAttribute("hidden");
-      }
-
       let profileInfoEnabled;
       try {
         profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled");
       } catch (ex) {}
 
       // determine the fxa status...
-      this.page = PAGE_PLEASE_WAIT;
+      this._showLoadPage(service);
 
       fxAccounts.getSignedInUser().then(data => {
         if (!data) {
           this.page = FXA_PAGE_LOGGED_OUT;
           return false;
         }
         this.page = FXA_PAGE_LOGGED_IN;
         // We are logged in locally, but maybe we are in a state where the
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -19,20 +19,16 @@
               name="services.sync.engine.tabs"
               type="bool"/>
   <preference id="engine.prefs"
               name="services.sync.engine.prefs"
               type="bool"/>
   <preference id="engine.passwords"
               name="services.sync.engine.passwords"
               type="bool"/>
-  <!-- non Sync-Engine engines -->
-  <preference id="engine.readinglist"
-              name="readinglist.scheduler.enabled"
-              type="bool"/>
 </preferences>
 
 <script type="application/javascript"
         src="chrome://browser/content/preferences/in-content/sync.js"/>
 <script type="application/javascript"
         src="chrome://browser/content/sync/utils.js"/>
 
 <hbox id="header-sync"
@@ -199,22 +195,16 @@
       </label>
     </hbox>
     <label id="loginErrorStartOver" class="text-link">
       &unlinkDevice.label;
     </label>
   </vbox>
 
   <!-- These panels are for the Firefox Accounts identity provider -->
-  <vbox id="fxaDeterminingStatus" align="center">
-    <spacer flex="1"/>
-    <label>&determiningAcctStatus.label;</label>
-    <spacer flex="1"/>
-  </vbox>
-
   <vbox id="noFxaAccount" align="start">
     <label>&welcome.description;</label>
     <label id="noFxaSignUp" class="text-link">
       &welcome.createAccount.label;
     </label>
     <label id="noFxaSignIn" class="text-link">
       &welcome.signIn.label;
     </label>
@@ -301,21 +291,16 @@
           <checkbox label="&engine.passwords.label;"
                     accesskey="&engine.passwords.accesskey;"
                     preference="engine.passwords"/>
         </vbox>
         <vbox align="start">
           <checkbox label="&engine.history.label;"
                     accesskey="&engine.history.accesskey;"
                     preference="engine.history"/>
-          <checkbox id="readinglist-engine"
-                    label="&engine.readinglist.label;"
-                    accesskey="&engine.readinglist.accesskey;"
-                    preference="engine.readinglist"
-                    hidden="true"/>
           <checkbox label="&engine.addons.label;"
                     accesskey="&engine.addons.accesskey;"
                     preference="engine.addons"/>
           <checkbox label="&engine.prefs.label;"
                     accesskey="&engine.prefs.accesskey;"
                     preference="engine.prefs"/>
         </vbox>
         <spacer/>
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -151,21 +151,16 @@ let gSyncPane = {
     Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
 
     let service = Components.classes["@mozilla.org/weave/service;1"]
                   .getService(Components.interfaces.nsISupports)
                   .wrappedJSObject;
     // service.fxAccountsEnabled is false iff sync is already configured for
     // the legacy provider.
     if (service.fxAccountsEnabled) {
-      // unhide the reading-list engine if readinglist is enabled (note we do
-      // it here as it must remain disabled for legacy sync users)
-      if (Services.prefs.getBoolPref("browser.readinglist.enabled")) {
-        document.getElementById("readinglist-engine").removeAttribute("hidden");
-      }
       // determine the fxa status...
       this.page = PAGE_PLEASE_WAIT;
       fxAccounts.getSignedInUser().then(data => {
         if (!data) {
           this.page = FXA_PAGE_LOGGED_OUT;
           return;
         }
         this.page = FXA_PAGE_LOGGED_IN;
--- a/browser/components/preferences/sync.xul
+++ b/browser/components/preferences/sync.xul
@@ -23,18 +23,16 @@
 
     <preferences id="syncEnginePrefs">
       <preference id="engine.addons"    name="services.sync.engine.addons"    type="bool"/>
       <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/>
       <preference id="engine.history"   name="services.sync.engine.history"   type="bool"/>
       <preference id="engine.tabs"      name="services.sync.engine.tabs"      type="bool"/>
       <preference id="engine.prefs"     name="services.sync.engine.prefs"     type="bool"/>
       <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/>
-      <!-- non Sync-Engine engines -->
-      <preference id="engine.readinglist" name="readinglist.scheduler.enabled" type="bool"/>
     </preferences>
 
 
     <script type="application/javascript"
             src="chrome://browser/content/preferences/sync.js"/>
     <script type="application/javascript"
             src="chrome://browser/content/sync/utils.js"/>
 
@@ -296,22 +294,16 @@
                 <checkbox label="&engine.passwords.label;"
                           accesskey="&engine.passwords.accesskey;"
                           onsynctopreference="gSyncPane.onPreferenceChanged();"
                           preference="engine.passwords"/>
                 <checkbox label="&engine.history.label;"
                           accesskey="&engine.history.accesskey;"
                           onsynctopreference="gSyncPane.onPreferenceChanged(this);"
                           preference="engine.history"/>
-                <!-- onpreferencechanged not needed for the readinglist engine -->
-                <checkbox id="readinglist-engine"
-                          label="&engine.readinglist.label;"
-                          accesskey="&engine.readinglist.accesskey;"
-                          preference="engine.readinglist"
-                          hidden="true"/>
                 <checkbox label="&engine.addons.label;"
                           accesskey="&engine.addons.accesskey;"
                           onsynctopreference="gSyncPane.onPreferenceChanged();"
                           preference="engine.addons"/>
                 <checkbox label="&engine.prefs.label;"
                           accesskey="&engine.prefs.accesskey;"
                           onsynctopreference="gSyncPane.onPreferenceChanged();"
                           preference="engine.prefs"/>
deleted file mode 100644
--- a/browser/components/readinglist/ReadingList.jsm
+++ /dev/null
@@ -1,1120 +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.EXPORTED_SYMBOLS = [
-  "ReadingList",
-];
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "SQLiteStore",
-  "resource:///modules/readinglist/SQLiteStore.jsm");
-
-// We use Sync's "Utils" module for the device name, which is unfortunate,
-// but let's give it a better name here.
-XPCOMUtils.defineLazyGetter(this, "SyncUtils", function() {
-  const {Utils} = Cu.import("resource://services-sync/util.js", {});
-  return Utils;
-});
-
-let log = Log.repository.getLogger("readinglist.api");
-
-
-// Each ReadingListItem has a _record property, an object containing the raw
-// data from the server and local store.  These are the names of the properties
-// in that object.
-//
-// Not important, but FYI: The order that these are listed in follows the order
-// that the server doc lists the fields in the article data model, more or less:
-// http://readinglist.readthedocs.org/en/latest/model.html
-const ITEM_RECORD_PROPERTIES = `
-  guid
-  serverLastModified
-  url
-  preview
-  title
-  resolvedURL
-  resolvedTitle
-  excerpt
-  archived
-  deleted
-  favorite
-  isArticle
-  wordCount
-  unread
-  addedBy
-  addedOn
-  storedOn
-  markedReadBy
-  markedReadOn
-  readPosition
-  syncStatus
-`.trim().split(/\s+/);
-
-// Each local item has a syncStatus indicating the state of the item in relation
-// to the sync server.  See also Sync.jsm.
-const SYNC_STATUS_SYNCED = 0;
-const SYNC_STATUS_NEW = 1;
-const SYNC_STATUS_CHANGED_STATUS = 2;
-const SYNC_STATUS_CHANGED_MATERIAL = 3;
-const SYNC_STATUS_DELETED = 4;
-
-// These options are passed as the "control" options to store methods and filter
-// out all records in the store with syncStatus SYNC_STATUS_DELETED.
-const STORE_OPTIONS_IGNORE_DELETED = {
-  syncStatus: [
-    SYNC_STATUS_SYNCED,
-    SYNC_STATUS_NEW,
-    SYNC_STATUS_CHANGED_STATUS,
-    SYNC_STATUS_CHANGED_MATERIAL,
-  ],
-};
-
-// Changes to the following item properties are considered "status," or
-// "status-only," changes, in relation to the sync server.  Changes to other
-// properties are considered "material" changes.  See also Sync.jsm.
-const SYNC_STATUS_PROPERTIES_STATUS = `
-  favorite
-  markedReadBy
-  markedReadOn
-  readPosition
-  unread
-`.trim().split(/\s+/);
-
-function ReadingListError(message) {
-  this.message = message;
-  this.name = this.constructor.name;
-  this.stack = (new Error()).stack;
-
-  // Consumers can set this to an Error that this ReadingListError wraps.
-  this.originalError = null;
-}
-ReadingListError.prototype = new Error();
-ReadingListError.prototype.constructor = ReadingListError;
-
-function ReadingListExistsError(message) {
-  message = message || "The item already exists";
-  ReadingListError.call(this, message);
-}
-ReadingListExistsError.prototype = new ReadingListError();
-ReadingListExistsError.prototype.constructor = ReadingListExistsError;
-
-function ReadingListDeletedError(message) {
-  message = message || "The item has been deleted";
-  ReadingListError.call(this, message);
-}
-ReadingListDeletedError.prototype = new ReadingListError();
-ReadingListDeletedError.prototype.constructor = ReadingListDeletedError;
-
-/**
- * A reading list contains ReadingListItems.
- *
- * A list maintains only one copy of an item per URL.  So if for example you use
- * an iterator to get two references to items with the same URL, your references
- * actually refer to the same JS object.
- *
- * Options Objects
- * ---------------
- *
- * Some methods on ReadingList take an "optsList", a variable number of
- * arguments, each of which is an "options object".  Options objects let you
- * control the items that the method acts on.
- *
- * Each options object is a simple object with properties whose names are drawn
- * from ITEM_RECORD_PROPERTIES.  For an item to match an options object, the
- * properties of the item must match all the properties in the object.  For
- * example, an object { guid: "123" } matches any item whose GUID is 123.  An
- * object { guid: "123", title: "foo" } matches any item whose GUID is 123 *and*
- * whose title is foo.
- *
- * You can pass multiple options objects as separate arguments.  For an item to
- * match multiple objects, its properties must match all the properties in at
- * least one of the objects.  For example, a list of objects { guid: "123" } and
- * { title: "foo" } matches any item whose GUID is 123 *or* whose title is
- * foo.
- *
- * The properties in an options object can be arrays, not only scalars.  When a
- * property is an array, then for an item to match, its corresponding property
- * must have a value that matches any value in the array.  For example, an
- * options object { guid: ["123", "456"] } matches any item whose GUID is either
- * 123 *or* 456.
- *
- * In addition to properties with names from ITEM_RECORD_PROPERTIES, options
- * objects can also have the following special properties:
- *
- *   * sort: The name of a property to sort on.
- *   * descending: A boolean, true to sort descending, false to sort ascending.
- *     If `sort` is given but `descending` isn't, the sort is ascending (since
- *     `descending` is falsey).
- *   * limit: Limits the number of matching items to this number.
- *   * offset: Starts matching items at this index in the results.
- *
- * Since you can pass multiple options objects in a list, you can include these
- * special properties in any number of the objects in the list, but it doesn't
- * really make sense to do so.  The last property in the list is the one that's
- * used.
- *
- * @param store Backing storage for the list.  See SQLiteStore.jsm for what this
- *        object's interface should look like.
- */
-function ReadingListImpl(store) {
-  this._store = store;
-  this._itemsByNormalizedURL = new Map();
-  this._iterators = new Set();
-  this._listeners = new Set();
-}
-
-ReadingListImpl.prototype = {
-
-  Error: {
-    Error: ReadingListError,
-    Exists: ReadingListExistsError,
-    Deleted: ReadingListDeletedError,
-  },
-
-  ItemRecordProperties: ITEM_RECORD_PROPERTIES,
-
-  SyncStatus: {
-    SYNCED: SYNC_STATUS_SYNCED,
-    NEW: SYNC_STATUS_NEW,
-    CHANGED_STATUS: SYNC_STATUS_CHANGED_STATUS,
-    CHANGED_MATERIAL: SYNC_STATUS_CHANGED_MATERIAL,
-    DELETED: SYNC_STATUS_DELETED,
-  },
-
-  SyncStatusProperties: {
-    STATUS: SYNC_STATUS_PROPERTIES_STATUS,
-  },
-
-  /**
-   * Yields the number of items in the list.
-   *
-   * @param optsList A variable number of options objects that control the
-   *        items that are matched.  See Options Objects.
-   * @return Promise<number> The number of matching items in the list.  Rejected
-   *         with an Error on error.
-   */
-  count: Task.async(function* (...optsList) {
-    return (yield this._store.count(optsList, STORE_OPTIONS_IGNORE_DELETED));
-  }),
-
-  /**
-   * Checks whether a given URL is in the ReadingList already.
-   *
-   * @param {String/nsIURI} url - URL to check.
-   * @returns {Promise} Promise that is fulfilled with a boolean indicating
-   *                    whether the URL is in the list or not.
-   */
-  hasItemForURL: Task.async(function* (url) {
-    url = normalizeURI(url);
-
-    // This is used on every tab switch and page load of the current tab, so we
-    // want it to be quick and avoid a DB query whenever possible.
-
-    // First check if any cached items have a direct match.
-    if (this._itemsByNormalizedURL.has(url)) {
-      return true;
-    }
-
-    // Then check if any cached items may have a different resolved URL
-    // that matches.
-    for (let itemWeakRef of this._itemsByNormalizedURL.values()) {
-      let item = itemWeakRef.get();
-      if (item && item.resolvedURL == url) {
-        return true;
-      }
-    }
-
-    // Finally, fall back to the DB.
-    let count = yield this.count({url: url}, {resolvedURL: url});
-    return (count > 0);
-  }),
-
-  /**
-   * Enumerates the items in the list that match the given options.
-   *
-   * @param callback Called for each item in the enumeration.  It's passed a
-   *        single object, a ReadingListItem.  It may return a promise; if so,
-   *        the callback will not be called for the next item until the promise
-   *        is resolved.
-   * @param optsList A variable number of options objects that control the
-   *        items that are matched.  See Options Objects.
-   * @return Promise<null> Resolved when the enumeration completes *and* the
-   *         last promise returned by the callback is resolved.  Rejected with
-   *         an Error on error.
-   */
-  forEachItem: Task.async(function* (callback, ...optsList) {
-    let thisCallback = record => callback(this._itemFromRecord(record));
-    yield this._forEachRecord(thisCallback, optsList, STORE_OPTIONS_IGNORE_DELETED);
-  }),
-
-  /**
-   * Enumerates the GUIDs for previously synced items that are marked as being
-   * locally deleted.
-   */
-  forEachSyncedDeletedGUID: Task.async(function* (callback, ...optsList) {
-    let thisCallback = record => callback(record.guid);
-    yield this._forEachRecord(thisCallback, optsList, {
-      syncStatus: SYNC_STATUS_DELETED,
-    });
-  }),
-
-  /**
-   * See forEachItem.
-   *
-   * @param storeOptions An options object passed to the store as the "control"
-   *        options.
-   */
-  _forEachRecord: Task.async(function* (callback, optsList, storeOptions) {
-    let promiseChain = Promise.resolve();
-    yield this._store.forEachItem(record => {
-      promiseChain = promiseChain.then(() => {
-        return new Promise((resolve, reject) => {
-          let promise = callback(record);
-          if (promise instanceof Promise) {
-            return promise.then(resolve, reject);
-          }
-          resolve();
-          return undefined;
-        });
-      });
-    }, optsList, storeOptions);
-    yield promiseChain;
-  }),
-
-  /**
-   * Returns a new ReadingListItemIterator that can be used to enumerate items
-   * in the list.
-   *
-   * @param optsList A variable number of options objects that control the
-   *        items that are matched.  See Options Objects.
-   * @return A new ReadingListItemIterator.
-   */
-  iterator(...optsList) {
-    let iter = new ReadingListItemIterator(this, ...optsList);
-    this._iterators.add(Cu.getWeakReference(iter));
-    return iter;
-  },
-
-  /**
-   * Adds an item to the list that isn't already present.
-   *
-   * The given object represents a new item, and the properties of the object
-   * are those in ITEM_RECORD_PROPERTIES.  It may have as few or as many
-   * properties that you want to set, but it must have a `url` property.
-   *
-   * It's an error to call this with an object whose `url` or `guid` properties
-   * are the same as those of items that are already present in the list.  The
-   * returned promise is rejected in that case.
-   *
-   * @param record A simple object representing an item.
-   * @return Promise<ReadingListItem> Resolved with the new item when the list
-   *         is updated.  Rejected with an Error on error.
-   */
-  addItem: Task.async(function* (record) {
-    record = normalizeRecord(record);
-    if (!record.url) {
-      throw new ReadingListError("The item to be added must have a url");
-    }
-    if (!("addedOn" in record)) {
-      record.addedOn = Date.now();
-    }
-    if (!("addedBy" in record)) {
-      try {
-        record.addedBy = Services.prefs.getCharPref("services.sync.client.name");
-      } catch (ex) {
-        record.addedBy = SyncUtils.getDefaultDeviceName();
-      }
-    }
-    if (!("syncStatus" in record)) {
-      record.syncStatus = SYNC_STATUS_NEW;
-    }
-
-    log.debug("Adding item with guid: ${guid}, url: ${url}", record);
-    yield this._store.addItem(record);
-    log.trace("Added item with guid: ${guid}, url: ${url}", record);
-    this._invalidateIterators();
-    let item = this._itemFromRecord(record);
-    this._callListeners("onItemAdded", item);
-    let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
-    mm.broadcastAsyncMessage("Reader:Added", item.toJSON());
-    return item;
-  }),
-
-  /**
-   * Updates the properties of an item that belongs to the list.
-   *
-   * The passed-in item may have as few or as many properties that you want to
-   * set; only the properties that are present are updated.  The item must have
-   * a `url`, however.
-   *
-   * It's an error to call this for an item that doesn't belong to the list.
-   * The returned promise is rejected in that case.
-   *
-   * @param item The ReadingListItem to update.
-   * @return Promise<null> Resolved when the list is updated.  Rejected with an
-   *         Error on error.
-   */
-  updateItem: Task.async(function* (item) {
-    if (item._deleted) {
-      throw new ReadingListDeletedError("The item to be updated has been deleted");
-    }
-    if (!item._record.url) {
-      throw new ReadingListError("The item to be updated must have a url");
-    }
-    this._ensureItemBelongsToList(item);
-    log.debug("Updating item with guid: ${guid}, url: ${url}", item._record);
-    yield this._store.updateItem(item._record);
-    log.trace("Finished updating item with guid: ${guid}, url: ${url}", item._record);
-    this._invalidateIterators();
-    this._callListeners("onItemUpdated", item);
-  }),
-
-  /**
-   * Deletes an item from the list.  The item must have a `url`.
-   *
-   * It's an error to call this for an item that doesn't belong to the list.
-   * The returned promise is rejected in that case.
-   *
-   * @param item The ReadingListItem to delete.
-   * @return Promise<null> Resolved when the list is updated.  Rejected with an
-   *         Error on error.
-   */
-  deleteItem: Task.async(function* (item) {
-    if (item._deleted) {
-      throw new ReadingListDeletedError("The item has already been deleted");
-    }
-    this._ensureItemBelongsToList(item);
-
-    log.debug("Deleting item with guid: ${guid}, url: ${url}");
-
-    // If the item is new and therefore hasn't been synced yet, delete it from
-    // the store.  Otherwise mark it as deleted but don't actually delete it so
-    // that its status can be synced.
-    if (item._record.syncStatus == SYNC_STATUS_NEW) {
-      log.debug("Item is new, truly deleting it", item._record);
-      yield this._store.deleteItemByURL(item.url);
-    }
-    else {
-      log.debug("Item has been synced, only marking it as deleted",
-                item._record);
-      // To prevent data leakage, only keep the record fields needed to sync
-      // the deleted status: guid and syncStatus.
-      let newRecord = {};
-      for (let prop of ITEM_RECORD_PROPERTIES) {
-        newRecord[prop] = null;
-      }
-      newRecord.guid = item._record.guid;
-      newRecord.syncStatus = SYNC_STATUS_DELETED;
-      yield this._store.updateItemByGUID(newRecord);
-    }
-
-    log.trace("Finished deleting item with guid: ${guid}, url: ${url}", item._record);
-    item.list = null;
-    item._deleted = true;
-    // failing to remove the item from the map points at something bad!
-    if (!this._itemsByNormalizedURL.delete(item.url)) {
-      log.error("Failed to remove item from the map", item);
-    }
-    this._invalidateIterators();
-    let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
-    mm.broadcastAsyncMessage("Reader:Removed", item.toJSON());
-    this._callListeners("onItemDeleted", item);
-  }),
-
-  /**
-   * Finds the first item that matches the given options.
-   *
-   * @param optsList See Options Objects.
-   * @return The first matching item, or null if there are no matching items.
-   */
-  item: Task.async(function* (...optsList) {
-    return (yield this.iterator(...optsList).items(1))[0] || null;
-  }),
-
-  /**
-   * Find any item that matches a given URL - either the item's URL, or its
-   * resolved URL.
-   *
-   * @param {String/nsIURI} uri - URI to match against. This will be normalized.
-   * @return The first matching item, or null if there are no matching items.
-   */
-  itemForURL: Task.async(function* (uri) {
-    let url = normalizeURI(uri);
-    return (yield this.item({ url: url }, { resolvedURL: url }));
-  }),
-
-  /**
-   * Add to the ReadingList the page that is loaded in a given browser.
-   *
-   * @param {<xul:browser>} browser - Browser element for the document,
-   * used to get metadata about the article.
-   * @param {nsIURI/string} url - url to add to the reading list.
-   * @return {Promise} Promise that is fullfilled with the added item.
-   */
-  addItemFromBrowser: Task.async(function* (browser, url) {
-    let metadata = yield this.getMetadataFromBrowser(browser);
-    let record = {
-      url: url,
-      title: metadata.title,
-      resolvedURL: metadata.url,
-      excerpt: metadata.description,
-    };
-
-    if (metadata.previews.length > 0) {
-      record.preview = metadata.previews[0];
-    }
-
-    return (yield this.addItem(record));
-  }),
-
-  /**
-   * Get page metadata from the content document in a given <xul:browser>.
-   * @see PageMetadata.jsm
-   *
-   * @param {<xul:browser>} browser - Browser element for the document.
-   * @returns {Promise} Promise that is fulfilled with an object describing the metadata.
-   */
-  getMetadataFromBrowser(browser) {
-    let mm = browser.messageManager;
-    return new Promise(resolve => {
-      function handleResult(msg) {
-        mm.removeMessageListener("PageMetadata:PageDataResult", handleResult);
-        resolve(msg.json);
-      }
-      mm.addMessageListener("PageMetadata:PageDataResult", handleResult);
-      mm.sendAsyncMessage("PageMetadata:GetPageData");
-    });
-  },
-
-  /**
-   * Adds a listener that will be notified when the list changes.  Listeners
-   * are objects with the following optional methods:
-   *
-   *   onItemAdded(item)
-   *   onItemUpdated(item)
-   *   onItemDeleted(item)
-   *
-   * @param listener A listener object.
-   */
-  addListener(listener) {
-    this._listeners.add(listener);
-  },
-
-  /**
-   * Removes a listener from the list.
-   *
-   * @param listener A listener object.
-   */
-  removeListener(listener) {
-    this._listeners.delete(listener);
-  },
-
-  /**
-   * Call this when you're done with the list.  Don't use it afterward.
-   */
-  destroy: Task.async(function* () {
-    yield this._store.destroy();
-    for (let itemWeakRef of this._itemsByNormalizedURL.values()) {
-      let item = itemWeakRef.get();
-      if (item) {
-        item.list = null;
-      }
-    }
-    this._itemsByNormalizedURL.clear();
-  }),
-
-  // The list's backing store.
-  _store: null,
-
-  // A Map mapping *normalized* URL strings to nsIWeakReferences that refer to
-  // ReadingListItems.
-  _itemsByNormalizedURL: null,
-
-  // A Set containing nsIWeakReferences that refer to valid iterators produced
-  // by the list.
-  _iterators: null,
-
-  // A Set containing listener objects.
-  _listeners: null,
-
-  /**
-   * Returns the ReadingListItem represented by the given record object.  If
-   * the item doesn't exist yet, it's created first.
-   *
-   * @param record A simple object with *normalized* item record properties.
-   * @return The ReadingListItem.
-   */
-  _itemFromRecord(record) {
-    if (!record.url) {
-      throw new Error("record must have a URL");
-    }
-    let itemWeakRef = this._itemsByNormalizedURL.get(record.url);
-    let item = itemWeakRef ? itemWeakRef.get() : null;
-    if (item) {
-      item._record = record;
-    }
-    else {
-      item = new ReadingListItem(record);
-      item.list = this;
-      this._itemsByNormalizedURL.set(record.url, Cu.getWeakReference(item));
-    }
-    return item;
-  },
-
-  /**
-   * Marks all the list's iterators as invalid, meaning it's not safe to use
-   * them anymore.
-   */
-  _invalidateIterators() {
-    for (let iterWeakRef of this._iterators) {
-      let iter = iterWeakRef.get();
-      if (iter) {
-        iter.invalidate();
-      }
-    }
-    this._iterators.clear();
-  },
-
-  /**
-   * Calls a method on all listeners.
-   *
-   * @param methodName The name of the method to call.
-   * @param item This item will be passed to the listeners.
-   */
-  _callListeners(methodName, item) {
-    for (let listener of this._listeners) {
-      if (methodName in listener) {
-        try {
-          listener[methodName](item);
-        }
-        catch (err) {
-          Cu.reportError(err);
-        }
-      }
-    }
-  },
-
-  _ensureItemBelongsToList(item) {
-    if (!item || !item._ensureBelongsToList) {
-      throw new ReadingListError("The item is not a ReadingListItem");
-    }
-    item._ensureBelongsToList();
-  },
-};
-
-
-let _unserializable = () => {}; // See comments in the ReadingListItem ctor.
-
-/**
- * An item in a reading list.
- *
- * Each item belongs to a list, and it's an error to use an item with a
- * ReadingList that the item doesn't belong to.
- *
- * @param record A simple object with the properties of the item, as few or many
- *        as you want.  This will be normalized.
- */
-function ReadingListItem(record={}) {
-  this._record = record;
-  this._deleted = false;
-
-  // |this._unserializable| works around a problem when sending one of these
-  // items via a message manager. If |this.list| is set, the item can't be
-  // transferred directly, so .toJSON is implicitly called and the object
-  // returned via that is sent. However, once the item is deleted and |this.list|
-  // is null, the item *can* be directly serialized - so the message handler
-  // sees the "raw" object - ie, it sees "_record" etc.
-  // We work around this problem by *always* having an unserializable property
-  // on the object - this way the implicit .toJSON call is always made, even
-  // when |this.list| is null.
-  this._unserializable = _unserializable;
-}
-
-ReadingListItem.prototype = {
-
-  // Be careful when caching properties.  If you cache a property that depends
-  // on a mutable _record property, then you need to recache your property after
-  // _record is set.
-
-  /**
-   * Item's unique ID.
-   * @type string
-   */
-  get id() {
-    if (!this._id) {
-      this._id = hash(this.url);
-    }
-    return this._id;
-  },
-
-  /**
-   * The item's server-side GUID. This is set by the remote server and therefore is not
-   * guaranteed to be set for local items.
-   * @type string
-   */
-  get guid() {
-    return this._record.guid || undefined;
-  },
-
-  /**
-   * The item's URL.
-   * @type string
-   */
-  get url() {
-    return this._record.url || undefined;
-  },
-
-  /**
-   * The item's URL as an nsIURI.
-   * @type nsIURI
-   */
-  get uri() {
-    if (!this._uri) {
-      this._uri = this._record.url ?
-                  Services.io.newURI(this._record.url, "", null) :
-                  undefined;
-    }
-    return this._uri;
-  },
-
-  /**
-   * The item's resolved URL.
-   * @type string
-   */
-  get resolvedURL() {
-    return this._record.resolvedURL || undefined;
-  },
-  set resolvedURL(val) {
-    this._updateRecord({ resolvedURL: val });
-  },
-
-  /**
-   * The item's resolved URL as an nsIURI.  The setter takes an nsIURI or a
-   * string spec.
-   * @type nsIURI
-   */
-  get resolvedURI() {
-    return this._record.resolvedURL ?
-           Services.io.newURI(this._record.resolvedURL, "", null) :
-           undefined;
-  },
-  set resolvedURI(val) {
-    this._updateRecord({ resolvedURL: val });
-  },
-
-  /**
-   * The item's title.
-   * @type string
-   */
-  get title() {
-    return this._record.title || undefined;
-  },
-  set title(val) {
-    this._updateRecord({ title: val });
-  },
-
-  /**
-   * The item's resolved title.
-   * @type string
-   */
-  get resolvedTitle() {
-    return this._record.resolvedTitle || undefined;
-  },
-  set resolvedTitle(val) {
-    this._updateRecord({ resolvedTitle: val });
-  },
-
-  /**
-   * The item's excerpt.
-   * @type string
-   */
-  get excerpt() {
-    return this._record.excerpt || undefined;
-  },
-  set excerpt(val) {
-    this._updateRecord({ excerpt: val });
-  },
-
-  /**
-   * The item's archived status.
-   * @type boolean
-   */
-  get archived() {
-    return !!this._record.archived;
-  },
-  set archived(val) {
-    this._updateRecord({ archived: !!val });
-  },
-
-  /**
-   * Whether the item is a favorite.
-   * @type boolean
-   */
-  get favorite() {
-    return !!this._record.favorite;
-  },
-  set favorite(val) {
-    this._updateRecord({ favorite: !!val });
-  },
-
-  /**
-   * Whether the item is an article.
-   * @type boolean
-   */
-  get isArticle() {
-    return !!this._record.isArticle;
-  },
-  set isArticle(val) {
-    this._updateRecord({ isArticle: !!val });
-  },
-
-  /**
-   * The item's word count.
-   * @type integer
-   */
-  get wordCount() {
-    return this._record.wordCount || undefined;
-  },
-  set wordCount(val) {
-    this._updateRecord({ wordCount: val });
-  },
-
-  /**
-   * Whether the item is unread.
-   * @type boolean
-   */
-  get unread() {
-    return !!this._record.unread;
-  },
-  set unread(val) {
-    this._updateRecord({ unread: !!val });
-  },
-
-  /**
-   * The date the item was added.
-   * @type Date
-   */
-  get addedOn() {
-    return this._record.addedOn ?
-           new Date(this._record.addedOn) :
-           undefined;
-  },
-  set addedOn(val) {
-    this._updateRecord({ addedOn: val.valueOf() });
-  },
-
-  /**
-   * The date the item was stored.
-   * @type Date
-   */
-  get storedOn() {
-    return this._record.storedOn ?
-           new Date(this._record.storedOn) :
-           undefined;
-  },
-  set storedOn(val) {
-    this._updateRecord({ storedOn: val.valueOf() });
-  },
-
-  /**
-   * The GUID of the device that marked the item read.
-   * @type string
-   */
-  get markedReadBy() {
-    return this._record.markedReadBy || undefined;
-  },
-  set markedReadBy(val) {
-    this._updateRecord({ markedReadBy: val });
-  },
-
-  /**
-   * The date the item marked read.
-   * @type Date
-   */
-  get markedReadOn() {
-    return this._record.markedReadOn ?
-           new Date(this._record.markedReadOn) :
-           undefined;
-  },
-  set markedReadOn(val) {
-    this._updateRecord({ markedReadOn: val.valueOf() });
-  },
-
-  /**
-   * The item's read position.
-   * @param integer
-   */
-  get readPosition() {
-    return this._record.readPosition || undefined;
-  },
-  set readPosition(val) {
-    this._updateRecord({ readPosition: val });
-  },
-
-  /**
-   * The URL to a preview image.
-   * @type string
-   */
-   get preview() {
-     return this._record.preview || undefined;
-   },
-
-  /**
-   * Deletes the item from its list.
-   *
-   * @return Promise<null> Resolved when the list has been updated.
-   */
-  delete: Task.async(function* () {
-    if (this._deleted) {
-      throw new ReadingListDeletedError("The item has already been deleted");
-    }
-    this._ensureBelongsToList();
-    yield this.list.deleteItem(this);
-  }),
-
-  toJSON() {
-    return this._record;
-  },
-
-  /**
-   * Do not use this at all unless you know what you're doing.  Use the public
-   * getters and setters, above, instead.
-   *
-   * A simple object that contains the item's normalized data in the same format
-   * that the local store and server use.  Records passed in by the consumer are
-   * not normalized, but everywhere else, records are always normalized unless
-   * otherwise stated.  The setter normalizes the passed-in value, so it will
-   * throw an error if the value is not a valid record.
-   *
-   * This object should reflect the item's representation in the local store, so
-   * when calling the setter, be careful that it doesn't drift away from the
-   * store's record.  If you set it, you should also call updateItem() around
-   * the same time.
-   */
-  get _record() {
-    return this.__record;
-  },
-  set _record(val) {
-    this.__record = normalizeRecord(val);
-  },
-
-  /**
-   * Updates the item's record.  This calls the _record setter, so it will throw
-   * an error if the partial record is not valid.
-   *
-   * @param partialRecord An object containing any of the record properties.
-   */
-  _updateRecord(partialRecord) {
-    let record = this._record;
-
-    // The syncStatus flag can change from SYNCED to either CHANGED_STATUS or
-    // CHANGED_MATERIAL, or from CHANGED_STATUS to CHANGED_MATERIAL.
-    if (record.syncStatus == SYNC_STATUS_SYNCED ||
-        record.syncStatus == SYNC_STATUS_CHANGED_STATUS) {
-      let allStatusChanges = Object.keys(partialRecord).every(prop => {
-        return SYNC_STATUS_PROPERTIES_STATUS.indexOf(prop) >= 0;
-      });
-      record.syncStatus = allStatusChanges ? SYNC_STATUS_CHANGED_STATUS :
-                          SYNC_STATUS_CHANGED_MATERIAL;
-    }
-
-    for (let prop in partialRecord) {
-      record[prop] = partialRecord[prop];
-    }
-    this._record = record;
-  },
-
-  _ensureBelongsToList() {
-    if (!this.list) {
-      throw new ReadingListError("The item must belong to a list");
-    }
-  },
-};
-
-/**
- * An object that enumerates over items in a list.
- *
- * You can enumerate items a chunk at a time by passing counts to forEach() and
- * items().  An iterator remembers where it left off, so for example calling
- * forEach() with a count of 10 will enumerate the first 10 items, and then
- * calling it again with 10 will enumerate the next 10 items.
- *
- * It's possible for an iterator's list to be modified between calls to
- * forEach() and items().  If that happens, the iterator is no longer safe to
- * use, so it's invalidated.  You can check whether an iterator is invalid by
- * getting its `invalid` property.  Attempting to use an invalid iterator will
- * throw an error.
- *
- * @param list The ReadingList to enumerate.
- * @param optsList A variable number of options objects that control the items
- *        that are matched.  See Options Objects.
- */
-function ReadingListItemIterator(list, ...optsList) {
-  this.list = list;
-  this.index = 0;
-  this.optsList = optsList;
-}
-
-ReadingListItemIterator.prototype = {
-
-  /**
-   * True if it's not safe to use the iterator.  Attempting to use an invalid
-   * iterator will throw an error.
-   */
-  invalid: false,
-
-  /**
-   * Enumerates the items in the iterator starting at its current index.  The
-   * iterator is advanced by the number of items enumerated.
-   *
-   * @param callback Called for each item in the enumeration.  It's passed a
-   *        single object, a ReadingListItem.  It may return a promise; if so,
-   *        the callback will not be called for the next item until the promise
-   *        is resolved.
-   * @param count The maximum number of items to enumerate.  Pass -1 to
-   *        enumerate them all.
-   * @return Promise<null> Resolved when the enumeration completes *and* the
-   *         last promise returned by the callback is resolved.
-   */
-  forEach: Task.async(function* (callback, count=-1) {
-    this._ensureValid();
-    let optsList = clone(this.optsList);
-    optsList.push({
-      offset: this.index,
-      limit: count,
-    });
-    yield this.list.forEachItem(item => {
-      this.index++;
-      return callback(item);
-    }, ...optsList);
-  }),
-
-  /**
-   * Gets an array of items in the iterator starting at its current index.  The
-   * iterator is advanced by the number of items fetched.
-   *
-   * @param count The maximum number of items to get.
-   * @return Promise<array> The fetched items.
-   */
-  items: Task.async(function* (count) {
-    this._ensureValid();
-    let optsList = clone(this.optsList);
-    optsList.push({
-      offset: this.index,
-      limit: count,
-    });
-    let items = [];
-    yield this.list.forEachItem(item => items.push(item), ...optsList);
-    this.index += items.length;
-    return items;
-  }),
-
-  /**
-   * Invalidates the iterator.  You probably don't want to call this unless
-   * you're a ReadingList.
-   */
-  invalidate() {
-    this.invalid = true;
-  },
-
-  _ensureValid() {
-    if (this.invalid) {
-      throw new ReadingListError("The iterator has been invalidated");
-    }
-  },
-};
-
-
-/**
- * Normalizes the properties of a record object, which represents a
- * ReadingListItem.  Throws an error if the record contains properties that
- * aren't in ITEM_RECORD_PROPERTIES.
- *
- * @param record A non-normalized record object.
- * @return The new normalized record.
- */
-function normalizeRecord(nonNormalizedRecord) {
-  let record = {};
-  for (let prop in nonNormalizedRecord) {
-    if (ITEM_RECORD_PROPERTIES.indexOf(prop) < 0) {
-      throw new ReadingListError("Unrecognized item property: " + prop);
-    }
-    switch (prop) {
-    case "url":
-    case "resolvedURL":
-      if (nonNormalizedRecord[prop]) {
-        record[prop] = normalizeURI(nonNormalizedRecord[prop]);
-      }
-      else {
-        record[prop] = nonNormalizedRecord[prop];
-      }
-      break;
-    default:
-      record[prop] = nonNormalizedRecord[prop];
-      break;
-    }
-  }
-  return record;
-}
-
-/**
- * Normalize a URI, stripping away extraneous parts we don't want to store
- * or compare against.
- *
- * @param {nsIURI/String} uri - URI to normalize.
- * @returns {String} String spec of a cloned and normalized version of the
- *          input URI.
- */
-function normalizeURI(uri) {
-  if (typeof uri == "string") {
-    try {
-      uri = Services.io.newURI(uri, "", null);
-    } catch (ex) {
-      return uri;
-    }
-  }
-  uri = uri.cloneIgnoringRef();
-  try {
-    uri.userPass = "";
-  } catch (ex) {} // Not all nsURI impls (eg, nsSimpleURI) support .userPass
-  return uri.spec;
-};
-
-function hash(str) {
-  let hasher = Cc["@mozilla.org/security/hash;1"].
-               createInstance(Ci.nsICryptoHash);
-  hasher.init(Ci.nsICryptoHash.MD5);
-  let stream = Cc["@mozilla.org/io/string-input-stream;1"].
-               createInstance(Ci.nsIStringInputStream);
-  stream.data = str;
-  hasher.updateFromStream(stream, -1);
-  let binaryStr = hasher.finish(false);
-  let hexStr =
-    [("0" + binaryStr.charCodeAt(i).toString(16)).slice(-2) for (i in binaryStr)].
-    join("");
-  return hexStr;
-}
-
-function clone(obj) {
-  return Cu.cloneInto(obj, {}, { cloneFunctions: false });
-}
-
-Object.defineProperty(this, "ReadingList", {
-  get() {
-    if (!this._singleton) {
-      let store = new SQLiteStore("reading-list.sqlite");
-      this._singleton = new ReadingListImpl(store);
-    }
-    return this._singleton;
-  },
-});
deleted file mode 100644
--- a/browser/components/readinglist/SQLiteStore.jsm
+++ /dev/null
@@ -1,466 +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.EXPORTED_SYMBOLS = [
-  "SQLiteStore",
-];
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
-  "resource:///modules/readinglist/ReadingList.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
-  "resource://gre/modules/Sqlite.jsm");
-
-/**
- * A SQLite Reading List store backed by a database on disk.  The database is
- * created if it doesn't exist.
- *
- * @param pathRelativeToProfileDir The path of the database file relative to
- *        the profile directory.
- */
-this.SQLiteStore = function SQLiteStore(pathRelativeToProfileDir) {
-  this.pathRelativeToProfileDir = pathRelativeToProfileDir;
-};
-
-this.SQLiteStore.prototype = {
-
-  /**
-   * Yields the number of items in the store that match the given options.
-   *
-   * @param userOptsList A variable number of options objects that control the
-   *        items that are matched.  See Options Objects in ReadingList.jsm.
-   * @param controlOpts A single options object.  Use this to filter out items
-   *        that don't match it -- in other words, to override the user options.
-   *        See Options Objects in ReadingList.jsm.
-   * @return Promise<number> The number of matching items in the store.
-   *         Rejected with an Error on error.
-   */
-  count: Task.async(function* (userOptsList=[], controlOpts={}) {
-    let [sql, args] = sqlWhereFromOptions(userOptsList, controlOpts);
-    let count = 0;
-    let conn = yield this._connectionPromise;
-    yield conn.executeCached(`
-      SELECT COUNT(*) AS count FROM items ${sql};
-    `, args, row => count = row.getResultByName("count"));
-    return count;
-  }),
-
-  /**
-   * Enumerates the items in the store that match the given options.
-   *
-   * @param callback Called for each item in the enumeration.  It's passed a
-   *        single object, an item.
-   * @param userOptsList A variable number of options objects that control the
-   *        items that are matched.  See Options Objects in ReadingList.jsm.
-   * @param controlOpts A single options object.  Use this to filter out items
-   *        that don't match it -- in other words, to override the user options.
-   *        See Options Objects in ReadingList.jsm.
-   * @return Promise<null> Resolved when the enumeration completes.  Rejected
-   *         with an Error on error.
-   */
-  forEachItem: Task.async(function* (callback, userOptsList=[], controlOpts={}) {
-    let [sql, args] = sqlWhereFromOptions(userOptsList, controlOpts);
-    let colNames = ReadingList.ItemRecordProperties;
-    let conn = yield this._connectionPromise;
-    yield conn.executeCached(`
-      SELECT ${colNames} FROM items ${sql};
-    `, args, row => callback(itemFromRow(row)));
-  }),
-
-  /**
-   * Adds an item to the store that isn't already present.  See
-   * ReadingList.prototype.addItem.
-   *
-   * @param items A simple object representing an item.
-   * @return Promise<null> Resolved when the store is updated.  Rejected with an
-   *         Error on error.
-   */
-  addItem: Task.async(function* (item) {
-    let colNames = [];
-    let paramNames = [];
-    for (let propName in item) {
-      colNames.push(propName);
-      paramNames.push(`:${propName}`);
-    }
-    let conn = yield this._connectionPromise;
-    try {
-      yield conn.executeCached(`
-        INSERT INTO items (${colNames}) VALUES (${paramNames});
-      `, item);
-    }
-    catch (err) {
-      throwExistsError(err);
-    }
-  }),
-
-  /**
-   * Updates the properties of an item that's already present in the store.  See
-   * ReadingList.prototype.updateItem.
-   *
-   * @param item The item to update.  It must have a `url`.
-   * @return Promise<null> Resolved when the store is updated.  Rejected with an
-   *         Error on error.
-   */
-  updateItem: Task.async(function* (item) {
-    yield this._updateItem(item, "url");
-  }),
-
-  /**
-   * Same as updateItem, but the item is keyed off of its `guid` instead of its
-   * `url`.
-   *
-   * @param item The item to update.  It must have a `guid`.
-   * @return Promise<null> Resolved when the store is updated.  Rejected with an
-   *         Error on error.
-   */
-  updateItemByGUID: Task.async(function* (item) {
-    yield this._updateItem(item, "guid");
-  }),
-
-  /**
-   * Deletes an item from the store by its URL.
-   *
-   * @param url The URL string of the item to delete.
-   * @return Promise<null> Resolved when the store is updated.  Rejected with an
-   *         Error on error.
-   */
-  deleteItemByURL: Task.async(function* (url) {
-    let conn = yield this._connectionPromise;
-    yield conn.executeCached(`
-      DELETE FROM items WHERE url = :url;
-    `, { url: url });
-  }),
-
-  /**
-   * Deletes an item from the store by its GUID.
-   *
-   * @param guid The GUID string of the item to delete.
-   * @return Promise<null> Resolved when the store is updated.  Rejected with an
-   *         Error on error.
-   */
-  deleteItemByGUID: Task.async(function* (guid) {
-    let conn = yield this._connectionPromise;
-    yield conn.executeCached(`
-      DELETE FROM items WHERE guid = :guid;
-    `, { guid: guid });
-  }),
-
-  /**
-   * Call this when you're done with the store.  Don't use it afterward.
-   */
-  destroy() {
-    if (!this._destroyPromise) {
-      this._destroyPromise = Task.spawn(function* () {
-        let conn = yield this._connectionPromise;
-        yield conn.close();
-        this.__connectionPromise = Promise.reject("Store destroyed");
-      }.bind(this));
-    }
-    return this._destroyPromise;
-  },
-
-  /**
-   * Promise<Sqlite.OpenedConnection>
-   */
-  get _connectionPromise() {
-    if (!this.__connectionPromise) {
-      this.__connectionPromise = this._createConnection();
-    }
-    return this.__connectionPromise;
-  },
-
-  /**
-   * Creates the database connection.
-   *
-   * @return Promise<Sqlite.OpenedConnection>
-   */
-  _createConnection: Task.async(function* () {
-    let conn = yield Sqlite.openConnection({
-      path: this.pathRelativeToProfileDir,
-      sharedMemoryCache: false,
-    });
-    Sqlite.shutdown.addBlocker("readinglist/SQLiteStore: Destroy",
-                               this.destroy.bind(this));
-    yield conn.execute(`
-      PRAGMA locking_mode = EXCLUSIVE;
-    `);
-    yield this._checkSchema(conn);
-    return conn;
-  }),
-
-  /**
-   * Updates the properties of an item that's already present in the store.  See
-   * ReadingList.prototype.updateItem.
-   *
-   * @param item The item to update.  It must have the property named by
-   *        keyProp.
-   * @param keyProp The item is keyed off of this property.
-   * @return Promise<null> Resolved when the store is updated.  Rejected with an
-   *         Error on error.
-   */
-  _updateItem: Task.async(function* (item, keyProp) {
-    let assignments = [];
-    for (let propName in item) {
-      assignments.push(`${propName} = :${propName}`);
-    }
-    let conn = yield this._connectionPromise;
-    if (!item[keyProp]) {
-      throw new ReadingList.Error.Error("Item must have " + keyProp);
-    }
-    try {
-      yield conn.executeCached(`
-        UPDATE items SET ${assignments} WHERE ${keyProp} = :${keyProp};
-      `, item);
-    }
-    catch (err) {
-      throwExistsError(err);
-    }
-  }),
-
-  // The current schema version.
-  _schemaVersion: 1,
-
-  _checkSchema: Task.async(function* (conn) {
-    let version = parseInt(yield conn.getSchemaVersion());
-    for (; version < this._schemaVersion; version++) {
-      let meth = `_migrateSchema${version}To${version + 1}`;
-      yield this[meth](conn);
-    }
-    yield conn.setSchemaVersion(this._schemaVersion);
-  }),
-
-  _migrateSchema0To1: Task.async(function* (conn) {
-    yield conn.execute(`
-      PRAGMA journal_mode = wal;
-    `);
-    // 524288 bytes = 512 KiB
-    yield conn.execute(`
-      PRAGMA journal_size_limit = 524288;
-    `);
-    // Not important, but FYI: The order that these columns are listed in
-    // follows the order that the server doc lists the fields in the article
-    // data model, more or less:
-    // http://readinglist.readthedocs.org/en/latest/model.html
-    yield conn.execute(`
-      CREATE TABLE items (
-        id INTEGER PRIMARY KEY AUTOINCREMENT,
-        guid TEXT UNIQUE,
-        serverLastModified INTEGER,
-        url TEXT UNIQUE,
-        preview TEXT,
-        title TEXT,
-        resolvedURL TEXT UNIQUE,
-        resolvedTitle TEXT,
-        excerpt TEXT,
-        archived BOOLEAN,
-        deleted BOOLEAN,
-        favorite BOOLEAN,
-        isArticle BOOLEAN,
-        wordCount INTEGER,
-        unread BOOLEAN,
-        addedBy TEXT,
-        addedOn INTEGER,
-        storedOn INTEGER,
-        markedReadBy TEXT,
-        markedReadOn INTEGER,
-        readPosition INTEGER,
-        syncStatus INTEGER
-      );
-    `);
-    yield conn.execute(`
-      CREATE INDEX items_addedOn ON items (addedOn);
-    `);
-    yield conn.execute(`
-      CREATE INDEX items_unread ON items (unread);
-    `);
-  }),
-};
-
-/**
- * Returns a simple object whose properties are the
- * ReadingList.ItemRecordProperties lifted from the given row.
- *
- * @param row A mozIStorageRow.
- * @return The item.
- */
-function itemFromRow(row) {
-  let item = {};
-  for (let name of ReadingList.ItemRecordProperties) {
-    item[name] = row.getResultByName(name);
-  }
-  return item;
-}
-
-/**
- * If the given Error indicates that a unique constraint failed, then wraps that
- * error in a ReadingList.Error.Exists and throws it.  Otherwise throws the
- * given error.
- *
- * @param err An Error object.
- */
-function throwExistsError(err) {
-  let match =
-    /UNIQUE constraint failed: items\.([a-zA-Z0-9_]+)/.exec(err.message);
-  if (match) {
-    let newErr = new ReadingList.Error.Exists(
-      "An item with the following property already exists: " + match[1]
-    );
-    newErr.originalError = err;
-    err = newErr;
-  }
-  throw err;
-}
-
-/**
- * Returns the back part of a SELECT statement generated from the given list of
- * options.
- *
- * @param userOptsList A variable number of options objects that control the
- *        items that are matched.  See Options Objects in ReadingList.jsm.
- * @param controlOpts A single options object.  Use this to filter out items
- *        that don't match it -- in other words, to override the user options.
- *        See Options Objects in ReadingList.jsm.
- * @return An array [sql, args].  sql is a string of SQL.  args is an object
- *         that contains arguments for all the parameters in sql.
- */
-function sqlWhereFromOptions(userOptsList, controlOpts) {
-  // We modify the options objects in userOptsList, which were passed in by the
-  // store client, so clone them first.
-  userOptsList = Cu.cloneInto(userOptsList, {}, { cloneFunctions: false });
-
-  let sort;
-  let sortDir;
-  let limit;
-  let offset;
-  for (let opts of userOptsList) {
-    if ("sort" in opts) {
-      sort = opts.sort;
-      delete opts.sort;
-    }
-    if ("descending" in opts) {
-      if (opts.descending) {
-        sortDir = "DESC";
-      }
-      delete opts.descending;
-    }
-    if ("limit" in opts) {
-      limit = opts.limit;
-      delete opts.limit;
-    }
-    if ("offset" in opts) {
-      offset = opts.offset;
-      delete opts.offset;
-    }
-  }
-
-  let fragments = [];
-
-  if (sort) {
-    sortDir = sortDir || "ASC";
-    fragments.push(`ORDER BY ${sort} ${sortDir}`);
-  }
-  if (limit) {
-    fragments.push(`LIMIT ${limit}`);
-    if (offset) {
-      fragments.push(`OFFSET ${offset}`);
-    }
-  }
-
-  let args = {};
-  let mainExprs = [];
-
-  let controlSQLExpr = sqlExpressionFromOptions([controlOpts], args);
-  if (controlSQLExpr) {
-    mainExprs.push(`(${controlSQLExpr})`);
-  }
-
-  let userSQLExpr = sqlExpressionFromOptions(userOptsList, args);
-  if (userSQLExpr) {
-    mainExprs.push(`(${userSQLExpr})`);
-  }
-
-  if (mainExprs.length) {
-    let conjunction = mainExprs.join(" AND ");
-    fragments.unshift(`WHERE ${conjunction}`);
-  }
-
-  let sql = fragments.join(" ");
-  return [sql, args];
-}
-
-/**
- * Returns a SQL expression generated from the given options list.  Each options
- * object in the list generates a subexpression, and all the subexpressions are
- * OR'ed together to produce the final top-level expression.  (e.g., an optsList
- * with three options objects would generate an expression like "(guid = :guid
- * OR (title = :title AND unread = :unread) OR resolvedURL = :resolvedURL)".)
- *
- * All the properties of the options objects are assumed to refer to columns in
- * the database.  If they don't, your SQL query will fail.
- *
- * @param optsList See Options Objects in ReadingList.jsm.
- * @param args An object that will hold the SQL parameters.  It will be
- *        modified.
- * @return A string of SQL.  Also, args will contain arguments for all the
- *         parameters in the SQL.
- */
-function sqlExpressionFromOptions(optsList, args) {
-  let disjunctions = [];
-  for (let opts of optsList) {
-    let conjunctions = [];
-    for (let key in opts) {
-      if (Array.isArray(opts[key])) {
-        // Convert arrays to IN expressions.  e.g., { guid: ['a', 'b', 'c'] }
-        // becomes "guid IN (:guid, :guid_1, :guid_2)".  The guid_i arguments
-        // are added to opts.
-        let array = opts[key];
-        let params = [];
-        for (let i = 0; i < array.length; i++) {
-          let paramName = uniqueParamName(args, key);
-          params.push(`:${paramName}`);
-          args[paramName] = array[i];
-        }
-        conjunctions.push(`${key} IN (${params})`);
-      }
-      else {
-        let paramName = uniqueParamName(args, key);
-        conjunctions.push(`${key} = :${paramName}`);
-        args[paramName] = opts[key];
-      }
-    }
-    let conjunction = conjunctions.join(" AND ");
-    if (conjunction) {
-      disjunctions.push(`(${conjunction})`);
-    }
-  }
-  let disjunction = disjunctions.join(" OR ");
-  return disjunction;
-}
-
-/**
- * Returns a version of the given name such that it doesn't conflict with the
- * name of any property in args.  e.g., if name is "foo" but args already has
- * properties named "foo", "foo1", and "foo2", then "foo3" is returned.
- *
- * @param args An object.
- * @param name The name you want to use.
- * @return A unique version of the given name.
- */
-function uniqueParamName(args, name) {
-  if (name in args) {
-    for (let i = 1; ; i++) {
-      let newName = `${name}_${i}`;
-      if (!(newName in args)) {
-        return newName;
-      }
-    }
-  }
-  return name;
-}
deleted file mode 100644
--- a/browser/components/readinglist/Scheduler.jsm
+++ /dev/null
@@ -1,409 +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;"
-
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import('resource://gre/modules/Task.jsm');
-
-
-XPCOMUtils.defineLazyModuleGetter(this, 'LogManager',
-  'resource://services-common/logmanager.js');
-
-XPCOMUtils.defineLazyModuleGetter(this, 'Log',
-  'resource://gre/modules/Log.jsm');
-
-XPCOMUtils.defineLazyModuleGetter(this, 'Preferences',
-  'resource://gre/modules/Preferences.jsm');
-
-XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout',
-  'resource://gre/modules/Timer.jsm');
-XPCOMUtils.defineLazyModuleGetter(this, 'clearTimeout',
-  'resource://gre/modules/Timer.jsm');
-
-// The main readinglist module.
-XPCOMUtils.defineLazyModuleGetter(this, 'ReadingList',
-  'resource:///modules/readinglist/ReadingList.jsm');
-
-// The "engine"
-XPCOMUtils.defineLazyModuleGetter(this, 'Sync',
-  'resource:///modules/readinglist/Sync.jsm');
-
-// FxAccountsCommon.js doesn't use a "namespace", so create one here.
-XPCOMUtils.defineLazyGetter(this, "fxAccountsCommon", function() {
-  let namespace = {};
-  Cu.import("resource://gre/modules/FxAccountsCommon.js", namespace);
-  return namespace;
-});
-
-this.EXPORTED_SYMBOLS = ["ReadingListScheduler"];
-
-// A list of "external" observer topics that may cause us to change when we
-// sync.
-const OBSERVERS = [
-  // We don't sync when offline and restart when online.
-  "network:offline-status-changed",
-  // FxA notifications also cause us to check if we should sync.
-  "fxaccounts:onverified",
-  // some notifications the engine might send if we have been requested to backoff.
-  "readinglist:backoff-requested",
-  // request to sync now
-  "readinglist:user-sync",
-
-];
-
-let prefs = new Preferences("readinglist.scheduler.");
-
-// A helper to manage our interval values.
-let intervals = {
-  // Getters for our intervals.
-  _fixupIntervalPref(prefName, def) {
-    // All pref values are seconds, but we return ms.
-    return prefs.get(prefName, def) * 1000;
-  },
-
-  // How long after startup do we do an initial sync?
-  get initial() this._fixupIntervalPref("initial", 10), // 10 seconds.
-  // Every interval after the first.
-  get schedule() this._fixupIntervalPref("schedule", 2 * 60 * 60), // 2 hours
-  // Initial retry after an error (exponentially backed-off to .schedule)
-  get retry() this._fixupIntervalPref("retry", 2 * 60), // 2 mins
-};
-
-// This is the implementation, but it's not exposed directly.
-function InternalScheduler(readingList = null) {
-  // oh, I don't know what logs yet - let's guess!
-  let logs = [
-    "browserwindow.syncui",
-    "FirefoxAccounts",
-    "readinglist.api",
-    "readinglist.scheduler",
-    "readinglist.serverclient",
-    "readinglist.sync",
-  ];
-
-  this._logManager = new LogManager("readinglist.", logs, "readinglist");
-  this.log = Log.repository.getLogger("readinglist.scheduler");
-  this.log.info("readinglist scheduler created.")
-  this.state = this.STATE_OK;
-  this.readingList = readingList || ReadingList; // hook point for tests.
-
-  // don't this.init() here, but instead at the module level - tests want to
-  // add hooks before it is called.
-}
-
-InternalScheduler.prototype = {
-  // When the next scheduled sync should happen.  If we can sync, there will
-  // be a timer set to fire then. If we can't sync there will not be a timer,
-  // but it will be set to fire then as soon as we can.
-  _nextScheduledSync: null,
-  // The time when the most-recent "backoff request" expires - we will never
-  // schedule a new timer before this.
-  _backoffUntil: 0,
-  // Our current timer.
-  _timer: null,
-  // Our timer fires a promise - _timerRunning is true until it resolves or
-  // rejects.
-  _timerRunning: false,
-  // Our sync engine - XXX - maybe just a callback?
-  _engine: Sync,
-  // Our current "error backoff" timeout. zero if no error backoff is in
-  // progress and incremented after successive errors until a max is reached.
-  _currentErrorBackoff: 0,
-
-  // Our state variable and constants.
-  state: null,
-  STATE_OK: "ok",
-  STATE_ERROR_AUTHENTICATION: "authentication error",
-  STATE_ERROR_OTHER: "other error",
-
-  init() {
-    this.log.info("scheduler initialzing");
-    this._setupRLListener();
-    this._observe = this.observe.bind(this);
-    for (let notification of OBSERVERS) {
-      Services.obs.addObserver(this._observe, notification, false);
-    }
-    this._nextScheduledSync = Date.now() + intervals.initial;
-    this._setupTimer();
-  },
-
-  _setupRLListener() {
-    let maybeSync = () => {
-      if (this._timerRunning) {
-        // If a sync is currently running it is possible it will miss the change
-        // just made, so tell the timer the next sync should be 1 ms after
-        // it completes (we don't use zero as that has special meaning re backoffs)
-        this._maybeReschedule(1);
-      } else {
-        // Do the sync now.
-        this._syncNow();
-      }
-    };
-    let listener = {
-      onItemAdded: maybeSync,
-      onItemUpdated: maybeSync,
-      onItemDeleted: maybeSync,
-    }
-    this.readingList.addListener(listener);
-  },
-
-  // Note: only called by tests.
-  finalize() {
-    this.log.info("scheduler finalizing");
-    this._clearTimer();
-    for (let notification of OBSERVERS) {
-      Services.obs.removeObserver(this._observe, notification);
-    }
-    this._observe = null;
-  },
-
-  observe(subject, topic, data) {
-    this.log.debug("observed ${}", topic);
-    switch (topic) {
-      case "readinglist:backoff-requested": {
-        // The subject comes in as a string, a number of seconds.
-        let interval = parseInt(data, 10);
-        if (isNaN(interval)) {
-          this.log.warn("Backoff request had non-numeric value", data);
-          return;
-        }
-        this.log.info("Received a request to backoff for ${} seconds", interval);
-        this._backoffUntil = Date.now() + interval * 1000;
-        this._maybeReschedule(0);
-        break;
-      }
-      case "readinglist:user-sync":
-        this._syncNow();
-        break;
-      case "fxaccounts:onverified":
-        // If we were in an authentication error state, reset that now.
-        if (this.state == this.STATE_ERROR_AUTHENTICATION) {
-          this.state = this.STATE_OK;
-        }
-        // and sync now.
-        this._syncNow();
-        break;
-
-      // The rest just indicate that now is probably a good time to check if
-      // we can sync as normal using whatever schedule was previously set.
-      default:
-        break;
-    }
-    // When observers fire we ignore the current sync error state as the
-    // notification may indicate it's been resolved.
-    this._setupTimer(true);
-  },
-
-  // Is the current error state such that we shouldn't schedule a new sync.
-  _isBlockedOnError() {
-    // this needs more thought...
-    return this.state == this.STATE_ERROR_AUTHENTICATION;
-  },
-
-  // canSync indicates if we can currently sync.
-  _canSync(ignoreBlockingErrors = false) {
-    if (!prefs.get("enabled")) {
-      this.log.info("canSync=false - syncing is disabled");
-      return false;
-    }
-    if (Services.io.offline) {
-      this.log.info("canSync=false - we are offline");
-      return false;
-    }
-    if (!ignoreBlockingErrors && this._isBlockedOnError()) {
-      this.log.info("canSync=false - we are in a blocked error state", this.state);
-      return false;
-    }
-    this.log.info("canSync=true");
-    return true;
-  },
-
-  // _setupTimer checks the current state and the environment to see when
-  // we should next sync and creates the timer with the appropriate delay.
-  _setupTimer(ignoreBlockingErrors = false) {
-    if (!this._canSync(ignoreBlockingErrors)) {
-      this._clearTimer();
-      return;
-    }
-    if (this._timer) {
-      let when = new Date(this._nextScheduledSync);
-      let delay = this._nextScheduledSync - Date.now();
-      this.log.info("checkStatus - already have a timer - will fire in ${delay}ms at ${when}",
-                    {delay, when});
-      return;
-    }
-    if (this._timerRunning) {
-      this.log.info("checkStatus - currently syncing");
-      return;
-    }
-    // no timer and we can sync, so start a new one.
-    let now = Date.now();
-    let delay = Math.max(0, this._nextScheduledSync - now);
-    let when = new Date(now + delay);
-    this.log.info("next scheduled sync is in ${delay}ms (at ${when})", {delay, when})
-    this._timer = this._setTimeout(delay);
-  },
-
-  // Something (possibly naively) thinks the next sync should happen in
-  // delay-ms. If there's a backoff in progress, ignore the requested delay
-  // and use the back-off. If there's already a timer scheduled for earlier
-  // than delay, let the earlier timer remain. Otherwise, use the requested
-  // delay.
-  _maybeReschedule(delay) {
-    // If there's no delay specified and there's nothing currently scheduled,
-    // it means a backoff request while the sync is actually running - there's
-    // no need to do anything here - the next reschedule after the sync
-    // completes will take the backoff into account.
-    if (!delay && !this._nextScheduledSync) {
-      this.log.debug("_maybeReschedule ignoring a backoff request while running");
-      return;
-    }
-    let now = Date.now();
-    if (!this._nextScheduledSync) {
-      this._nextScheduledSync = now + delay;
-    }
-    // If there is something currently scheduled before the requested delay,
-    // keep the existing value (eg, if we have a timer firing in 1 second, and
-    // get a notification that says we should sync in 2 seconds, we keep the 1
-    // second value)
-    this._nextScheduledSync = Math.min(this._nextScheduledSync, now + delay);
-    // But we still need to honor a backoff.
-    this._nextScheduledSync = Math.max(this._nextScheduledSync, this._backoffUntil);
-    // And always create a new timer next time _setupTimer is called.
-    this._clearTimer();
-  },
-
-  // callback for when the timer fires.
-  _doSync() {
-    this.log.debug("starting sync");
-    this._timer = null;
-    this._timerRunning = true;
-    // flag that there's no new schedule yet, so a request coming in while
-    // we are running does the right thing.
-    this._nextScheduledSync = 0;
-    Services.obs.notifyObservers(null, "readinglist:sync:start", null);
-    this._engine.start().then(() => {
-      this.log.info("Sync completed successfully");
-      // Write a pref in the same format used to services/sync to indicate
-      // the last success.
-      prefs.set("lastSync", new Date().toString());
-      this.state = this.STATE_OK;
-      this._logManager.resetFileLog().then(result => {
-        if (result == this._logManager.ERROR_LOG_WRITTEN) {
-          Cu.reportError("Reading List sync encountered an error - see about:sync-log for the log file.");
-        }
-      });
-      Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
-      this._currentErrorBackoff = 0; // error retry interval is reset on success.
-      return intervals.schedule;
-    }).catch(err => {
-      // This isn't ideal - we really should have _canSync() check this - but
-      // that requires a refactor to turn _canSync() into a promise-based
-      // function.
-      if (err.message == fxAccountsCommon.ERROR_NO_ACCOUNT ||
-          err.message == fxAccountsCommon.ERROR_UNVERIFIED_ACCOUNT) {
-        // make everything look like success.
-        this._currentErrorBackoff = 0; // error retry interval is reset on success.
-        this.log.info("Can't sync due to FxA account state " + err.message);
-        this.state = this.STATE_OK;
-        this._logManager.resetFileLog().then(result => {
-          if (result == this._logManager.ERROR_LOG_WRITTEN) {
-            Cu.reportError("Reading List sync encountered an error - see about:sync-log for the log file.");
-          }
-        });
-        Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
-        // it's unfortunate that we are probably going to hit this every
-        // 2 hours, but it should be invisible to the user.
-        return intervals.schedule;
-      }
-      this.state = err.message == fxAccountsCommon.ERROR_AUTH_ERROR ?
-                   this.STATE_ERROR_AUTHENTICATION : this.STATE_ERROR_OTHER;
-      this.log.error("Sync failed, now in state '${state}': ${err}",
-                     {state: this.state, err});
-      this._logManager.resetFileLog();
-      Services.obs.notifyObservers(null, "readinglist:sync:error", null);
-      // We back-off on error retries until it hits our normally scheduled interval.
-      this._currentErrorBackoff = this._currentErrorBackoff == 0 ? intervals.retry :
-                                  Math.min(intervals.schedule, this._currentErrorBackoff * 2);
-      return this._currentErrorBackoff;
-    }).then(nextDelay => {
-      this._timerRunning = false;
-      // ensure a new timer is setup for the appropriate next time.
-      this._maybeReschedule(nextDelay);
-      this._setupTimer();
-      this._onAutoReschedule(); // just for tests...
-    }).catch(err => {
-      // We should never get here, but better safe than sorry...
-      this.log.error("Failed to reschedule after sync completed", err);
-    });
-  },
-
-  _clearTimer() {
-    if (this._timer) {
-      clearTimeout(this._timer);
-      this._timer = null;
-    }
-  },
-
-  // A function to "sync now", but not allowing it to start if one is
-  // already running, and rescheduling the timer.
-  // To call this, just send a "readinglist:user-sync" notification.
-  _syncNow() {
-    if (!prefs.get("enabled")) {
-      this.log.info("syncNow() but syncing is disabled - ignoring");
-      return;
-    }
-
-    if (this._timerRunning) {
-      this.log.info("syncNow() but a sync is already in progress - ignoring");
-      return;
-    }
-    this._clearTimer();
-    this._doSync();
-  },
-
-  // A couple of hook-points for testing.
-  // xpcshell tests hook this so (a) it can check the expected delay is set
-  // and (b) to ignore the delay and set a timeout of 0 so the test is fast.
-  _setTimeout(delay) {
-    return setTimeout(() => this._doSync(), delay);
-  },
-  // xpcshell tests hook this to make sure that the correct state etc exist
-  // after a sync has been completed and a new timer created (or not).
-  _onAutoReschedule() {},
-};
-
-let internalScheduler = new InternalScheduler();
-internalScheduler.init();
-
-// The public interface into this module is tiny, so a simple object that
-// delegates to the implementation.
-let ReadingListScheduler = {
-  get STATE_OK() internalScheduler.STATE_OK,
-  get STATE_ERROR_AUTHENTICATION() internalScheduler.STATE_ERROR_AUTHENTICATION,
-  get STATE_ERROR_OTHER() internalScheduler.STATE_ERROR_OTHER,
-
-  get state() internalScheduler.state,
-};
-
-// These functions are exposed purely for tests, which manage to grab them
-// via a BackstagePass.
-function createTestableScheduler(readingList) {
-  // kill the "real" scheduler as we don't want it listening to notifications etc.
-  if (internalScheduler) {
-    internalScheduler.finalize();
-    internalScheduler = null;
-  }
-  // No .init() call - that's up to the tests after hooking.
-  return new InternalScheduler(readingList);
-}
-
-// mochitests want the internal state of the real scheduler for various things.
-function getInternalScheduler() {
-  return internalScheduler;
-}
deleted file mode 100644
--- a/browser/components/readinglist/ServerClient.jsm
+++ /dev/null
@@ -1,178 +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/. */
-
-// The client used to access the ReadingList server.
-
-"use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "RESTRequest", "resource://services-common/rest.js");
-XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
-XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
-
-let log = Log.repository.getLogger("readinglist.serverclient");
-
-const OAUTH_SCOPE = "readinglist"; // The "scope" on the oauth token we request.
-
-this.EXPORTED_SYMBOLS = [
-  "ServerClient",
-];
-
-// utf-8 joy. rest.js, which we use for the underlying requests, does *not*
-// encode the request as utf-8 even though it wants to know the encoding.
-// It does, however, explicitly decode the response.  This seems insane, but is
-// what it is.
-// The end result being we need to utf-8 the request and let the response take
-// care of itself.
-function objectToUTF8Json(obj) {
-  // FTR, unescape(encodeURIComponent(JSON.stringify(obj))) also works ;)
-  return CommonUtils.encodeUTF8(JSON.stringify(obj));
-}
-
-function ServerClient(fxa = fxAccounts) {
-  this.fxa = fxa;
-}
-
-ServerClient.prototype = {
-
-  request(options) {
-    return this._request(options.path, options.method, options.body, options.headers);
-  },
-
-  get serverURL() {
-    return Services.prefs.getCharPref("readinglist.server");
-  },
-
-  _getURL(path) {
-    let result = this.serverURL;
-    // we expect the path to have a leading slash, so remove any trailing
-    // slashes on the pref.
-    if (result.endsWith("/")) {
-      result = result.slice(0, -1);
-    }
-    return result + path;
-  },
-
-  // Hook points for testing.
-  _getToken() {
-    // Assume token-caching is in place - if it's not we should avoid doing
-    // this each request.
-    return this.fxa.getOAuthToken({scope: OAUTH_SCOPE});
-  },
-
-  _removeToken(token) {
-    return this.fxa.removeCachedOAuthToken({token});
-  },
-
-  // Converts an error from the RESTRequest object to an error we export.
-  _convertRestError(error) {
-    return error; // XXX - errors?
-  },
-
-  // Converts an error from a try/catch handler to an error we export.
-  _convertJSError(error) {
-    return error; // XXX - errors?
-  },
-
-  /*
-   * Perform a request - handles authentication
-   */
-  _request: Task.async(function* (path, method, body, headers) {
-    let token = yield this._getToken();
-    let response = yield this._rawRequest(path, method, body, headers, token);
-    log.debug("initial request got status ${status}", response);
-    if (response.status == 401) {
-      // an auth error - assume our token has expired or similar.
-      this._removeToken(token);
-      token = yield this._getToken();
-      response = yield this._rawRequest(path, method, body, headers, token);
-      log.debug("retry of request got status ${status}", response);
-    }
-    return response;
-  }),
-
-  /*
-   * Perform a request *without* abstractions such as auth etc
-   *
-   * On success (which *includes* non-200 responses) returns an object like:
-   * {
-   *   status: 200, # http status code
-   *   headers: {}, # header values keyed by header name.
-   *   body: {},    # parsed json
-   }
-   */
-
-  _rawRequest(path, method, body, headers, oauthToken) {
-    return new Promise((resolve, reject) => {
-      let url = this._getURL(path);
-      log.debug("dispatching request to", url);
-      let request = new RESTRequest(url);
-      method = method.toUpperCase();
-
-      request.setHeader("Accept", "application/json");
-      request.setHeader("Content-Type", "application/json; charset=utf-8");
-      request.setHeader("Authorization", "Bearer " + oauthToken);
-      // and additional header specified for this request.
-      if (headers) {
-        for (let [headerName, headerValue] in Iterator(headers)) {
-          log.trace("Caller specified header: ${headerName}=${headerValue}", {headerName, headerValue});
-          request.setHeader(headerName, headerValue);
-        }
-      }
-
-      request.onComplete = error => {
-        // Although the server API docs say the "Backoff" header is on
-        // successful responses while "Retry-After" is on error responses, we
-        // just look for them both in both cases (as the scheduler makes no
-        // distinction)
-        let response = request.response;
-        if (response && response.headers) {
-          let backoff = response.headers["backoff"] || response.headers["retry-after"];
-          if (backoff) {
-            let numeric = backoff.toLowerCase() == "none" ? 0 :
-                          parseInt(backoff, 10);
-            if (isNaN(numeric)) {
-              log.info("Server requested unrecognized backoff", backoff);
-            } else if (numeric > 0) {
-              log.info("Server requested backoff", numeric);
-              Services.obs.notifyObservers(null, "readinglist:backoff-requested", String(numeric));
-            }
-          }
-        }
-        if (error) {
-          return reject(this._convertRestError(error));
-        }
-
-        log.debug("received response status: ${status} ${statusText}", response);
-        // Handle response status codes we know about
-        let result = {
-          status: response.status,
-          headers: response.headers
-        };
-        try {
-          if (response.body) {
-            result.body = JSON.parse(response.body);
-          }
-        } catch (e) {
-          log.debug("Response is not JSON. First 1024 chars: |${body}|",
-                    { body: response.body.substr(0, 1024) });
-          // We don't reject due to this (and don't even make a huge amount of
-          // log noise - eg, a 50X error from a load balancer etc may not write
-          // JSON.
-        }
-
-        resolve(result);
-      }
-      // We are assuming the body has already been decoded and thus contains
-      // unicode, but the server expects utf-8. encodeURIComponent does that.
-      request.dispatch(method, objectToUTF8Json(body));
-    });
-  },
-};
deleted file mode 100644
--- a/browser/components/readinglist/Sync.jsm
+++ /dev/null
@@ -1,664 +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.EXPORTED_SYMBOLS = [
-  "Sync",
-];
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
-  "resource://gre/modules/Preferences.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
-  "resource:///modules/readinglist/ReadingList.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ServerClient",
-  "resource:///modules/readinglist/ServerClient.jsm");
-
-// The maximum number of sub-requests per POST /batch supported by the server.
-// See http://readinglist.readthedocs.org/en/latest/api/batch.html.
-const BATCH_REQUEST_LIMIT = 25;
-
-// The Last-Modified header of server responses is stored here.
-const SERVER_LAST_MODIFIED_HEADER_PREF = "readinglist.sync.serverLastModified";
-
-// Maps local record properties to server record properties.
-const SERVER_PROPERTIES_BY_LOCAL_PROPERTIES = {
-  guid: "id",
-  serverLastModified: "last_modified",
-  url: "url",
-  preview: "preview",
-  title: "title",
-  resolvedURL: "resolved_url",
-  resolvedTitle: "resolved_title",
-  excerpt: "excerpt",
-  archived: "archived",
-  deleted: "deleted",
-  favorite: "favorite",
-  isArticle: "is_article",
-  wordCount: "word_count",
-  unread: "unread",
-  addedBy: "added_by",
-  addedOn: "added_on",
-  storedOn: "stored_on",
-  markedReadBy: "marked_read_by",
-  markedReadOn: "marked_read_on",
-  readPosition: "read_position",
-};
-
-// Local record properties that can be uploaded in new items.
-const NEW_RECORD_PROPERTIES = `
-  url
-  title
-  resolvedURL
-  resolvedTitle
-  excerpt
-  favorite
-  isArticle
-  wordCount
-  unread
-  addedBy
-  addedOn
-  markedReadBy
-  markedReadOn
-  readPosition
-  preview
-`.trim().split(/\s+/);
-
-// Local record properties that can be uploaded in changed items.
-const MUTABLE_RECORD_PROPERTIES = `
-  title
-  resolvedURL
-  resolvedTitle
-  excerpt
-  favorite
-  isArticle
-  wordCount
-  unread
-  markedReadBy
-  markedReadOn
-  readPosition
-  preview
-`.trim().split(/\s+/);
-
-let log = Log.repository.getLogger("readinglist.sync");
-
-
-/**
- * An object that syncs reading list state with a server.  To sync, make a new
- * SyncImpl object and then call start() on it.
- *
- * @param readingList The ReadingList to sync.
- */
-function SyncImpl(readingList) {
-  this.list = readingList;
-  this._client = new ServerClient();
-}
-
-/**
- * This implementation uses the sync algorithm described here:
- * https://github.com/mozilla-services/readinglist/wiki/Client-phases
- * The "phases" mentioned in the methods below refer to the phases in that
- * document.
- */
-SyncImpl.prototype = {
-
-  /**
-   * Starts sync, if it's not already started.
-   *
-   * @return Promise<null> this.promise, i.e., a promise that will be resolved
-   *         when sync completes, rejected on error.
-   */
-  start() {
-    if (!this.promise) {
-      this.promise = Task.spawn(function* () {
-        try {
-          yield this._start();
-        } finally {
-          delete this.promise;
-        }
-      }.bind(this));
-    }
-    return this.promise;
-  },
-
-  /**
-   * A Promise<null> that will be non-null when sync is ongoing.  Resolved when
-   * sync completes, rejected on error.
-   */
-  promise: null,
-
-  /**
-   * See the document linked above that describes the sync algorithm.
-   */
-  _start: Task.async(function* () {
-    log.info("Starting sync");
-    yield this._logDiagnostics();
-    yield this._uploadStatusChanges();
-    yield this._uploadNewItems();
-    yield this._uploadDeletedItems();
-    yield this._downloadModifiedItems();
-
-    // TODO: "Repeat [this phase] until no conflicts occur," says the doc.
-    yield this._uploadMaterialChanges();
-
-    log.info("Sync done");
-  }),
-
-  /**
-   * Phase 0 - for debugging we log some stuff about the local store before
-   * we start syncing.
-   * We only do this when the log level is "Trace" or lower as the info (a)
-   * may be expensive to generate, (b) generate alot of output and (c) may
-   * contain private information.
-   */
-  _logDiagnostics: Task.async(function* () {
-    // Sadly our log is likely to have Log.Level.All, so loop over our
-    // appenders looking for the effective level.
-    let smallestLevel = log.appenders.reduce(
-      (prev, appender) => Math.min(prev, appender.level),
-      Log.Level.Error);
-
-    if (smallestLevel > Log.Level.Trace) {
-      return;
-    }
-
-    let localItems = [];
-    yield this.list.forEachItem(localItem => localItems.push(localItem));
-    log.trace("Have " + localItems.length + " local item(s)");
-    for (let localItem of localItems) {
-      // We need to use .record so we get access to a couple of the "internal" fields.
-      let record = localItem._record;
-      let redacted = {};
-      for (let attr of ["guid", "url", "resolvedURL", "serverLastModified", "syncStatus"]) {
-        redacted[attr] = record[attr];
-      }
-      log.trace(JSON.stringify(redacted));
-    }
-    // and the GUIDs of deleted items.
-    let deletedGuids = []
-    yield this.list.forEachSyncedDeletedGUID(guid => deletedGuids.push(guid));
-    // This might be a huge line, but that's OK.
-    log.trace("Have ${num} deleted item(s): ${deletedGuids}", {num: deletedGuids.length, deletedGuids});
-  }),
-
-  /**
-   * Phase 1 part 1
-   *
-   * Uploads not-new items with status-only changes.  By design, status-only
-   * changes will never conflict with what's on the server.
-   */
-  _uploadStatusChanges: Task.async(function* () {
-    log.debug("Phase 1 part 1: Uploading status changes");
-    yield this._uploadChanges(ReadingList.SyncStatus.CHANGED_STATUS,
-                              ReadingList.SyncStatusProperties.STATUS);
-  }),
-
-  /**
-   * There are two phases for uploading changed not-new items: one for items
-   * with status-only changes, one for items with material changes.  The two
-   * work similarly mechanically, and this method is a helper for both.
-   *
-   * @param syncStatus Local items matching this sync status will be uploaded.
-   * @param localProperties An array of local record property names.  The
-   *        uploaded item records will include only these properties.
-   */
-  _uploadChanges: Task.async(function* (syncStatus, localProperties) {
-    // Get local items that match the given syncStatus.
-    let requests = [];
-    yield this.list.forEachItem(localItem => {
-      requests.push({
-        path: "/articles/" + localItem.guid,
-        body: serverRecordFromLocalItem(localItem, localProperties),
-      });
-    }, { syncStatus: syncStatus });
-    if (!requests.length) {
-      log.debug("No local changes to upload");
-      return;
-    }
-
-    // Send the request.
-    let request = {
-      body: {
-        defaults: {
-          method: "PATCH",
-        },
-        requests: requests,
-      },
-    };
-    let batchResponse = yield this._postBatch(request);
-    if (batchResponse.status != 200) {
-      this._handleUnexpectedResponse(true, "uploading changes", batchResponse);
-      return;
-    }
-
-    // Update local items based on the response.
-    for (let response of batchResponse.body.responses) {
-      if (response.status == 404) {
-        // item deleted
-        yield this._deleteItemForGUID(response.body.id);
-        continue;
-      }
-      if (response.status == 409) {
-        // "Conflict": A change violated a uniqueness constraint.  Mark the item
-        // as having material changes, and reconcile and upload it in the
-        // material-changes phase.
-        // TODO
-        continue;
-      }
-      if (response.status != 200) {
-        this._handleUnexpectedResponse(false, "uploading a change", response);
-        continue;
-      }
-      // Don't assume the local record and the server record aren't materially
-      // different.  Reconcile the differences.
-      // TODO
-
-      let item = yield this._itemForGUID(response.body.id);
-      yield this._updateItemWithServerRecord(item, response.body);
-    }
-  }),
-
-  /**
-   * Phase 1 part 2
-   *
-   * Uploads new items.
-   */
-  _uploadNewItems: Task.async(function* () {
-    log.debug("Phase 1 part 2: Uploading new items");
-
-    // Get new local items.
-    let requests = [];
-    yield this.list.forEachItem(localItem => {
-      requests.push({
-        body: serverRecordFromLocalItem(localItem, NEW_RECORD_PROPERTIES),
-      });
-    }, { syncStatus: ReadingList.SyncStatus.NEW });
-    if (!requests.length) {
-      log.debug("No new local items to upload");
-      return;
-    }
-
-    // Send the request.
-    let request = {
-      body: {
-        defaults: {
-          method: "POST",
-          path: "/articles",
-        },
-        requests: requests,
-      },
-    };
-    let batchResponse = yield this._postBatch(request);
-    if (batchResponse.status != 200) {
-      this._handleUnexpectedResponse(true, "uploading new items", batchResponse);
-      return;
-    }
-
-    // Update local items based on the response.
-    for (let response of batchResponse.body.responses) {
-      if (response.status == 303) {
-        // "See Other": An item with the URL already exists.  Mark the item as
-        // having material changes, and reconcile and upload it in the
-        // material-changes phase.
-        // TODO
-        continue;
-      }
-      // Note that the server seems to return a 200 if an identical item already
-      // exists, but we shouldn't be uploading identical items in this phase in
-      // normal usage. But if something goes wrong locally (eg, we upload but
-      // get some error even though the upload worked) we will see this.
-      // So allow 200 but log a warning.
-      if (response.status == 200) {
-        log.debug("Attempting to upload a new item found the server already had it", response);
-        // but we still process it.
-      } else if (response.status != 201) {
-        this._handleUnexpectedResponse(false, "uploading a new item", response);
-        continue;
-      }
-      let item = yield this.list.itemForURL(response.body.url);
-      yield this._updateItemWithServerRecord(item, response.body);
-    }
-  }),
-
-  /**
-   * Phase 1 part 3
-   *
-   * Uploads deleted synced items.
-   */
-  _uploadDeletedItems: Task.async(function* () {
-    log.debug("Phase 1 part 3: Uploading deleted items");
-
-    // Get deleted synced local items.
-    let requests = [];
-    yield this.list.forEachSyncedDeletedGUID(guid => {
-      requests.push({
-        path: "/articles/" + guid,
-      });
-    });
-    if (!requests.length) {
-      log.debug("No local deleted synced items to upload");
-      return;
-    }
-
-    // Send the request.
-    let request = {
-      body: {
-        defaults: {
-          method: "DELETE",
-        },
-        requests: requests,
-      },
-    };
-    let batchResponse = yield this._postBatch(request);
-    if (batchResponse.status != 200) {
-      this._handleUnexpectedResponse(true, "uploading deleted items", batchResponse);
-      return;
-    }
-
-    // Delete local items based on the response.
-    for (let response of batchResponse.body.responses) {
-      // A 404 means the item was already deleted on the server, which is OK.
-      // We still need to make sure it's deleted locally, though.
-      if (response.status != 200 && response.status != 404) {
-        this._handleUnexpectedResponse(false, "uploading a deleted item", response);
-        continue;
-      }
-      yield this._deleteItemForGUID(response.body.id);
-    }
-  }),
-
-  /**
-   * Phase 2
-   *
-   * Downloads items that were modified since the last sync.
-   */
-  _downloadModifiedItems: Task.async(function* () {
-    log.debug("Phase 2: Downloading modified items");
-
-    // Get modified items from the server.
-    let path = "/articles";
-    if (this._serverLastModifiedHeader) {
-      path += "?_since=" + this._serverLastModifiedHeader;
-    }
-    let request = {
-      method: "GET",
-      path: path,
-    };
-    let response = yield this._sendRequest(request);
-    if (response.status != 200) {
-      this._handleUnexpectedResponse(true, "downloading modified items", response);
-      return;
-    }
-
-    // Update local items based on the response.
-    for (let serverRecord of response.body.items) {
-      if (serverRecord.deleted) {
-        // _deleteItemForGUID is a no-op if no item exists with the GUID.
-        yield this._deleteItemForGUID(serverRecord.id);
-        continue;
-      }
-      let localItem = yield this._itemForGUID(serverRecord.id);
-      if (localItem) {
-        if (localItem.serverLastModified == serverRecord.last_modified) {
-          // We just uploaded this item in the new-items phase.
-          continue;
-        }
-        // The local item may have materially changed.  In that case, don't
-        // overwrite the local changes with the server record.  Instead, mark
-        // the item as having material changes and reconcile and upload it in
-        // the material-changes phase.
-        // TODO
-
-        yield this._updateItemWithServerRecord(localItem, serverRecord);
-        continue;
-      }
-      // A potentially new item.  addItem() will fail here when an item was
-      // added to the local list between the time we uploaded new items and
-      // now.
-      let localRecord = localRecordFromServerRecord(serverRecord);
-      try {
-        yield this.list.addItem(localRecord);
-      } catch (ex) {
-        if (ex instanceof ReadingList.Error.Exists) {
-          log.debug("Tried to add an item that already exists.");
-        } else {
-          log.error("Error adding an item from server record ${serverRecord} ${ex}",
-                    { serverRecord, ex });
-        }
-      }
-    }
-
-    // Now that changes have been successfully applied, advance the server
-    // last-modified timestamp so that next time we fetch items starting from
-    // the current point.  Response header names are lowercase.
-    if (response.headers && "last-modified" in response.headers) {
-      this._serverLastModifiedHeader = response.headers["last-modified"];
-    }
-  }),
-
-  /**
-   * Phase 3 (material changes)
-   *
-   * Uploads not-new items with material changes.
-   */
-  _uploadMaterialChanges: Task.async(function* () {
-    log.debug("Phase 3: Uploading material changes");
-    yield this._uploadChanges(ReadingList.SyncStatus.CHANGED_MATERIAL,
-                              MUTABLE_RECORD_PROPERTIES);
-  }),
-
-  /**
-   * Gets the local ReadingListItem with the given GUID.
-   *
-   * @param guid The item's GUID.
-   * @return The matching ReadingListItem.
-   */
-  _itemForGUID: Task.async(function* (guid) {
-    return (yield this.list.item({ guid: guid }));
-  }),
-
-  /**
-   * Updates the given local ReadingListItem with the given server record.  The
-   * local item's sync status is updated to reflect the fact that the item has
-   * been synced and is up to date.
-   *
-   * @param item A local ReadingListItem.
-   * @param serverRecord A server record representing the item.
-   */
-  _updateItemWithServerRecord: Task.async(function* (localItem, serverRecord) {
-    if (!localItem) {
-      // The item may have been deleted from the local list between the time we
-      // saw that it needed updating and now.
-      log.debug("Tried to update a null local item from server record",
-                serverRecord);
-      return;
-    }
-    localItem._record = localRecordFromServerRecord(serverRecord);
-    try {
-      yield this.list.updateItem(localItem);
-    } catch (ex) {
-      // The item may have been deleted from the local list after we fetched it.
-      if (ex instanceof ReadingList.Error.Deleted) {
-        log.debug("Tried to update an item that was deleted from server record",
-                  serverRecord);
-      } else {
-        log.error("Error updating an item from server record ${serverRecord} ${ex}",
-                  { serverRecord, ex });
-      }
-    }
-  }),
-
-  /**
-   * Truly deletes the local ReadingListItem with the given GUID.
-   *
-   * @param guid The item's GUID.
-   */
-  _deleteItemForGUID: Task.async(function* (guid) {
-    let item = yield this._itemForGUID(guid);
-    if (item) {
-      // If item is non-null, then it hasn't been deleted locally.  Therefore
-      // it's important to delete it through its list so that the list and its
-      // consumers are notified properly.  Set the syncStatus to NEW so that the
-      // list truly deletes the item.
-      item._record.syncStatus = ReadingList.SyncStatus.NEW;
-      try {
-        yield this.list.deleteItem(item);
-      } catch (ex) {
-        log.error("Failed delete local item with id ${guid} ${ex}",
-                  { guid, ex });
-      }
-      return;
-    }
-    // If item is null, then it may not actually exist locally, or it may have
-    // been synced and then deleted so that it's marked as being deleted.  In
-    // that case, try to delete it directly from the store.  As far as the list
-    // is concerned, the item has already been deleted.
-    log.debug("Item not present in list, deleting it by GUID instead");
-    try {
-      this.list._store.deleteItemByGUID(guid);
-    } catch (ex) {
-      log.error("Failed to delete local item with id ${guid} ${ex}",
-                { guid, ex });
-    }
-  }),
-
-  /**
-   * Sends a request to the server.
-   *
-   * @param req The request object: { method, path, body, headers }.
-   * @return Promise<response> Resolved with the server's response object:
-   *         { status, body, headers }.
-   */
-  _sendRequest: Task.async(function* (req) {
-    log.debug("Sending request", req);
-    let response = yield this._client.request(req);
-    log.debug("Received response", response);
-    return response;
-  }),
-
-  /**
-   * The server limits the number of sub-requests in POST /batch'es to
-   * BATCH_REQUEST_LIMIT.  This method takes an arbitrarily big batch request
-   * and breaks it apart into many individual batch requests in order to stay
-   * within the limit.
-   *
-   * @param bigRequest The same type of request object that _sendRequest takes.
-   *        Since it's a POST /batch request, its `body` should have a
-   *        `requests` property whose value is an array of sub-requests.
-   *        `method` and `path` are automatically filled.
-   * @return Promise<response> Resolved when all requests complete with 200s, or
-   *         when the first response that is not a 200 is received.  In the
-   *         first case, the resolved response is a combination of all the
-   *         server responses, and response.body.responses contains the sub-
-   *         responses for all the sub-requests in bigRequest.  In the second
-   *         case, the resolved response is the non-200 response straight from
-   *         the server.
-   */
-  _postBatch: Task.async(function* (bigRequest) {
-    log.debug("Sending batch requests");
-    let allSubResponses = [];
-    let remainingSubRequests = bigRequest.body.requests;
-    while (remainingSubRequests.length) {
-      let request = Object.assign({}, bigRequest);
-      request.method = "POST";
-      request.path = "/batch";
-      request.body.requests =
-        remainingSubRequests.splice(0, BATCH_REQUEST_LIMIT);
-      let response = yield this._sendRequest(request);
-      if (response.status != 200) {
-        return response;
-      }
-      allSubResponses = allSubResponses.concat(response.body.responses);
-    }
-    let bigResponse = {
-      status: 200,
-      body: {
-        responses: allSubResponses,
-      },
-    };
-    log.debug("All batch requests successfully sent");
-    return bigResponse;
-  }),
-
-  _handleUnexpectedResponse(isTopLevel, contextMsgFragment, response) {
-    log.error(`Unexpected response ${contextMsgFragment}`, response);
-    // We want to throw in some cases so the sync engine knows there was an
-    // error and retries using the error schedule. 401 implies an auth issue
-    // (possibly transient, possibly not) - but things like 404 might just
-    // relate to a single item and need not throw.  Any 5XX implies a
-    // (hopefully transient) server error.
-    if (isTopLevel && (response.status == 401 || response.status >= 500)) {
-      throw new Error("Sync aborted due to " + response.status + " server response.");
-    }
-  },
-
-  // TODO: Wipe this pref when user logs out.
-  get _serverLastModifiedHeader() {
-    if (!("__serverLastModifiedHeader" in this)) {
-      this.__serverLastModifiedHeader =
-        Preferences.get(SERVER_LAST_MODIFIED_HEADER_PREF, undefined);
-    }
-    return this.__serverLastModifiedHeader;
-  },
-  set _serverLastModifiedHeader(val) {
-    this.__serverLastModifiedHeader = val;
-    Preferences.set(SERVER_LAST_MODIFIED_HEADER_PREF, val);
-  },
-};
-
-
-/**
- * Translates a local ReadingListItem into a server record.
- *
- * @param localItem The local ReadingListItem.
- * @param localProperties An array of local item property names.  Only these
- *        properties will be included in the server record.
- * @return The server record.
- */
-function serverRecordFromLocalItem(localItem, localProperties) {
-  let serverRecord = {};
-  for (let localProp of localProperties) {
-    let serverProp = SERVER_PROPERTIES_BY_LOCAL_PROPERTIES[localProp];
-    if (localProp in localItem._record) {
-      serverRecord[serverProp] = localItem._record[localProp];
-    }
-  }
-  return serverRecord;
-}
-
-/**
- * Translates a server record into a local record.  The returned local record's
- * syncStatus will reflect the fact that the local record is up-to-date synced.
- *
- * @param serverRecord The server record.
- * @return The local record.
- */
-function localRecordFromServerRecord(serverRecord) {
-  let localRecord = {
-    // Mark the record as being up-to-date synced.
-    syncStatus: ReadingList.SyncStatus.SYNCED,
-  };
-  for (let localProp in SERVER_PROPERTIES_BY_LOCAL_PROPERTIES) {
-    let serverProp = SERVER_PROPERTIES_BY_LOCAL_PROPERTIES[localProp];
-    if (serverProp in serverRecord) {
-      localRecord[localProp] = serverRecord[serverProp];
-    }
-  }
-  return localRecord;
-}
-
-Object.defineProperty(this, "Sync", {
-  get() {
-    if (!this._singleton) {
-      this._singleton = new SyncImpl(ReadingList);
-    }
-    return this._singleton;
-  },
-});
deleted file mode 100644
--- a/browser/components/readinglist/jar.mn
+++ /dev/null
@@ -1,7 +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/.
-
-browser.jar:
-  content/browser/readinglist/sidebar.xhtml
-  content/browser/readinglist/sidebar.js
deleted file mode 100644
--- a/browser/components/readinglist/moz.build
+++ /dev/null
@@ -1,24 +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/.
-
-JAR_MANIFESTS += ['jar.mn']
-
-EXTRA_JS_MODULES.readinglist += [
-    'ReadingList.jsm',
-    'Scheduler.jsm',
-    'ServerClient.jsm',
-    'SQLiteStore.jsm',
-    'Sync.jsm',
-]
-
-TESTING_JS_MODULES += [
-    'test/ReadingListTestUtils.jsm',
-]
-
-BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
-
-XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
-
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Reading List')
deleted file mode 100644
--- a/browser/components/readinglist/sidebar.js
+++ /dev/null
@@ -1,484 +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";
-
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource:///modules/readinglist/ReadingList.jsm");
-
-let log = Cu.import("resource://gre/modules/Log.jsm", {})
-            .Log.repository.getLogger("readinglist.sidebar");
-
-
-let RLSidebar = {
-  /**
-   * Container element for all list item elements.
-   * @type {Element}
-   */
-  list: null,
-
-  /**
-   * A promise that's resolved when building the initial list completes.
-   * @type {Promise}
-   */
-  listPromise: null,
-
-  /**
-   * <template> element used for constructing list item elements.
-   * @type {Element}
-   */
-  itemTemplate: null,
-
-  /**
-   * Map of ReadingList Item objects, keyed by their ID.
-   * @type {Map}
-   */
-  itemsById: new Map(),
-  /**
-   * Map of list item elements, keyed by their corresponding Item's ID.
-   * @type {Map}
-   */
-  itemNodesById: new Map(),
-
-  /**
-   * Initialize the sidebar UI.
-   */
-  init() {
-    log.debug("Initializing");
-
-    addEventListener("unload", () => this.uninit());
-
-    this.list = document.getElementById("list");
-    this.emptyListInfo = document.getElementById("emptyListInfo");
-    this.itemTemplate = document.getElementById("item-template");
-
-    // click events for middle-clicks are not sent to DOM nodes, only to the document.
-    document.addEventListener("click", event => this.onClick(event));
-
-    this.list.addEventListener("mousemove", event => this.onListMouseMove(event));
-    this.list.addEventListener("keydown", event => this.onListKeyDown(event), true);
-
-    window.addEventListener("message", event => this.onMessage(event));
-
-    this.listPromise = this.ensureListItems();
-    ReadingList.addListener(this);
-
-    Services.prefs.setBoolPref("browser.readinglist.sidebarEverOpened", true);
-
-    let initEvent = new CustomEvent("Initialized", {bubbles: true});
-    document.documentElement.dispatchEvent(initEvent);
-  },
-
-  /**
-   * Un-initialize the sidebar UI.
-   */
-  uninit() {
-    log.debug("Shutting down");
-
-    ReadingList.removeListener(this);
-  },
-
-  /**
-   * Handle an item being added to the ReadingList.
-   * TODO: We may not want to show this new item right now.
-   * TODO: We should guard against the list growing here.
-   *
-   * @param {ReadinglistItem} item - Item that was added.
-   */
-  onItemAdded(item, append = false) {
-    log.trace(`onItemAdded: ${item}`);
-
-    let itemNode = document.importNode(this.itemTemplate.content, true).firstElementChild;
-    this.updateItem(item, itemNode);
-    // XXX Inserting at the top by default is a temp hack that will stop
-    // working once we start including items received from sync.
-    if (append)
-      this.list.appendChild(itemNode);
-    else
-      this.list.insertBefore(itemNode, this.list.firstChild);
-    this.itemNodesById.set(item.id, itemNode);
-    this.itemsById.set(item.id, item);
-
-    this.emptyListInfo.hidden = true;
-    window.requestAnimationFrame(() => {
-      window.requestAnimationFrame(() => {
-        itemNode.classList.add('visible');
-      });
-    });
-  },
-
-  /**
-   * Handle an item being deleted from the ReadingList.
-   * @param {ReadingListItem} item - Item that was deleted.
-   */
-  onItemDeleted(item) {
-    log.trace(`onItemDeleted: ${item}`);
-
-    let itemNode = this.itemNodesById.get(item.id);
-
-    this.itemNodesById.delete(item.id);
-    this.itemsById.delete(item.id);
-
-    itemNode.addEventListener('transitionend', (event) => {
-      if (event.propertyName == "max-height") {
-        itemNode.remove();
-
-        // TODO: ensureListItems doesn't yet cope with needing to add one item.
-        //this.ensureListItems();
-
-        this.emptyListInfo.hidden = (this.numItems > 0);
-      }
-    }, false);
-
-    itemNode.classList.remove('visible');
-  },
-
-  /**
-   * Handle an item in the ReadingList having any of its properties changed.
-   * @param {ReadingListItem} item - Item that was updated.
-   */
-  onItemUpdated(item) {
-    log.trace(`onItemUpdated: ${item}`);
-
-    let itemNode = this.itemNodesById.get(item.id);
-    if (!itemNode)
-      return;
-
-    this.updateItem(item, itemNode);
-  },
-
-  /**
-   * Update the element representing an item, ensuring it's in sync with the
-   * underlying data.
-   * @param {ReadingListItem} item - Item to use as a source.
-   * @param {Element} itemNode - Element to update.
-   */
-  updateItem(item, itemNode) {
-    itemNode.setAttribute("id", "item-" + item.id);
-    itemNode.setAttribute("title", `${item.title}\n${item.url}`);
-
-    itemNode.querySelector(".item-title").textContent = item.title;
-
-    let domain = item.uri.spec;
-    try {
-      domain = item.uri.host;
-    }
-    catch (err) {}
-    itemNode.querySelector(".item-domain").textContent = domain;
-
-    let thumb = itemNode.querySelector(".item-thumb-container");
-    if (item.preview) {
-      thumb.style.backgroundImage = "url(" + item.preview + ")";
-    } else {
-      thumb.style.removeProperty("background-image");
-    }
-    thumb.classList.toggle("preview-available", !!item.preview);
-  },
-
-  /**
-   * Ensure that the list is populated with the correct items.
-   */
-  ensureListItems: Task.async(function* () {
-    yield ReadingList.forEachItem(item => {
-      // TODO: Should be batch inserting via DocumentFragment
-      try {
-        this.onItemAdded(item, true);
-      } catch (e) {
-        log.warn("Error adding item", e);
-      }
-    }, {sort: "addedOn", descending: true});
-    this.emptyListInfo.hidden = (this.numItems > 0);
-  }),
-
-  /**
-   * Get the number of items currently displayed in the list.
-   * @type {number}
-   */
-  get numItems() {
-    return this.list.childElementCount;
-  },
-
-  /**
-   * The list item displayed in the current tab.
-   * @type {Element}
-   */
-  get activeItem() {
-    return document.querySelector("#list > .item.active");
-  },
-
-  set activeItem(node) {
-    if (node && node.parentNode != this.list) {
-      log.error(`Unable to set activeItem to invalid node ${node}`);
-      return;
-    }
-
-    log.trace(`Setting activeItem: ${node ? node.id : null}`);
-
-    if (node && node.classList.contains("active")) {
-      return;
-    }
-
-    let prevItem = document.querySelector("#list > .item.active");
-    if (prevItem) {
-      prevItem.classList.remove("active");
-    }
-
-    if (node) {
-      node.classList.add("active");
-    }
-
-    let event = new CustomEvent("ActiveItemChanged", {bubbles: true});
-    this.list.dispatchEvent(event);
-  },
-
-  /**
-   * The list item selected with the keyboard.
-   * @type {Element}
-   */
-  get selectedItem() {
-    return document.querySelector("#list > .item.selected");
-  },
-
-  set selectedItem(node) {
-    if (node && node.parentNode != this.list) {
-      log.error(`Unable to set selectedItem to invalid node ${node}`);
-      return;
-    }
-
-    log.trace(`Setting selectedItem: ${node ? node.id : null}`);
-
-    let prevItem = document.querySelector("#list > .item.selected");
-    if (prevItem) {
-      prevItem.classList.remove("selected");
-    }
-
-    if (node) {
-      node.classList.add("selected");
-      let itemId = this.getItemIdFromNode(node);
-      this.list.setAttribute("aria-activedescendant", "item-" + itemId);
-    } else {
-      this.list.removeAttribute("aria-activedescendant");
-    }
-
-    let event = new CustomEvent("SelectedItemChanged", {bubbles: true});
-    this.list.dispatchEvent(event);
-  },
-
-  /**
-   * The index of the currently selected item in the list.
-   * @type {number}
-   */
-  get selectedIndex() {
-    for (let i = 0; i < this.numItems; i++) {
-      let item = this.list.children.item(i);
-      if (!item) {
-        break;
-      }
-      if (item.classList.contains("selected")) {
-        return i;
-      }
-    }
-    return -1;
-  },
-
-  set selectedIndex(index) {
-    log.trace(`Setting selectedIndex: ${index}`);
-
-    if (index == -1) {
-      this.selectedItem = null;
-      return;
-    }
-
-    let item = this.list.children.item(index);
-    if (!item) {
-      log.warn(`Unable to set selectedIndex to invalid index ${index}`);
-      return;
-    }
-    this.selectedItem = item;
-  },
-
-  /**
-   * Open a given URL. The event is used to determine where it should be opened
-   * (current tab, new tab, new window).
-   * @param {string} url - URL to open.
-   * @param {Event} event - KeyEvent or MouseEvent that triggered this action.
-   */
-  openURL(url, event) {
-    log.debug(`Opening page ${url}`);
-
-    let mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIWebNavigation)
-                           .QueryInterface(Ci.nsIDocShellTreeItem)
-                           .rootTreeItem
-                           .QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindow);
-
-    let currentUrl = mainWindow.gBrowser.currentURI.spec;
-    if (currentUrl.startsWith("about:reader"))
-      url = "about:reader?url=" + encodeURIComponent(url);
-
-    mainWindow.openUILink(url, event);
-  },
-
-  /**
-   * Get the ID of the Item associated with a given list item element.
-   * @param {element} node - List item element to get an ID for.
-   * @return {string} Assocated Item ID.
-   */
-  getItemIdFromNode(node) {
-    let id = node.getAttribute("id");
-    if (id && id.startsWith("item-")) {
-      return id.slice(5);
-    }
-
-    return null;
-  },
-
-  /**
-   * Get the Item associated with a given list item element.
-   * @param {element} node - List item element to get an Item for.
-   * @return {string} Associated Item.
-   */
-  getItemFromNode(node) {
-    let itemId = this.getItemIdFromNode(node);
-    if (!itemId) {
-      return null;
-    }
-
-    return this.itemsById.get(itemId);
-  },
-
-  /**
-   * Open the active item in the list.
-   * @param {Event} event - Event triggering this.
-   */
-  openActiveItem(event) {
-    let itemNode = this.activeItem;
-    if (!itemNode) {
-      return;
-    }
-
-    let item = this.getItemFromNode(itemNode);
-    this.openURL(item.url, event);
-  },
-
-  /**
-   * Find the parent item element, from a given child element.
-   * @param {Element} node - Child element.
-   * @return {Element} Element for the item, or null if not found.
-   */
-  findParentItemNode(node) {
-    while (node && node != this.list && node != document.documentElement &&
-           !node.classList.contains("item")) {
-      node = node.parentNode;
-    }
-
-    if (node != this.list && node != document.documentElement) {
-      return node;
-    }
-
-    return null;
-  },
-
-  /**
-   * Handle a click event on the sidebar.
-   * @param {Event} event - Triggering event.
-   */
-  onClick(event) {
-    let itemNode = this.findParentItemNode(event.target);
-    if (!itemNode)
-      return;
-
-    if (event.target.classList.contains("remove-button")) {
-      ReadingList.deleteItem(this.getItemFromNode(itemNode));
-      return;
-    }
-
-    this.activeItem = itemNode;
-    this.openActiveItem(event);
-  },
-
-  /**
-   * Handle a mousemove event over the list box:
-   * If the hovered item isn't the selected one, clear the selection.
-   * @param {Event} event - Triggering event.
-   */
-  onListMouseMove(event) {
-    let itemNode = this.findParentItemNode(event.target);
-    if (itemNode != this.selectedItem)
-      this.selectedItem = null;
-  },
-
-  /**
-   * Handle a keydown event on the list box.
-   * @param {Event} event - Triggering event.
-   */
-  onListKeyDown(event) {
-    if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
-      // TODO: Refactor this so we pass a direction to a generic method.
-      // See autocomplete.xml's getNextIndex
-      event.preventDefault();
-
-      if (!this.numItems) {
-        return;
-      }
-      let index = this.selectedIndex + 1;
-      if (index >= this.numItems) {
-        index = 0;
-      }
-
-      this.selectedIndex = index;
-      this.selectedItem.focus();
-    } else if (event.keyCode == KeyEvent.DOM_VK_UP) {
-      event.preventDefault();
-
-      if (!this.numItems) {
-        return;
-      }
-      let index = this.selectedIndex - 1;
-      if (index < 0) {
-        index = this.numItems - 1;
-      }
-
-      this.selectedIndex = index;
-      this.selectedItem.focus();
-    } else if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
-      let selectedItem = this.selectedItem;
-      if (selectedItem) {
-        this.activeItem = selectedItem;
-        this.openActiveItem(event);
-      }
-    }
-  },
-
-  /**
-   * Handle a message, typically sent from browser-readinglist.js
-   * @param {Event} event - Triggering event.
-   */
-  onMessage(event) {
-    let msg = event.data;
-
-    if (msg.topic != "UpdateActiveItem") {
-      return;
-    }
-
-    if (!msg.url) {
-      this.activeItem = null;
-    } else {
-      ReadingList.itemForURL(msg.url).then(item => {
-        let node;
-        if (item && (node = this.itemNodesById.get(item.id))) {
-          this.activeItem = node;
-        }
-      });
-    }
-  }
-};
-
-
-addEventListener("DOMContentLoaded", () => RLSidebar.init());
deleted file mode 100644
--- a/browser/components/readinglist/sidebar.xhtml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<!DOCTYPE html [
-  <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
-  %browserDTD;
-]>
-<html xmlns="http://www.w3.org/1999/xhtml">
-  <head>
-    <script src="chrome://browser/content/readinglist/sidebar.js" type="application/javascript;version=1.8"></script>
-    <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/readinglist/sidebar.css"/>
-    <title>&readingList.label;</title>
-  </head>
-
-  <body role="application">
-    <template id="item-template">
-      <div class="item" role="option" tabindex="-1">
-        <div class="item-thumb-container"></div>
-        <div class="item-summary-container">
-          <div class="item-title-lines">
-            <p class="item-title"/>
-            <button class="remove-button" title="&readingList.sidebar.delete.tooltip;"/>
-          </div>
-          <div class="item-domain"></div>
-        </div>
-      </div>
-    </template>
-
-    <div id="emptyListInfo" hidden="true">&readingList.sidebar.emptyText;</div>
-    <div id="list" role="listbox" tabindex="1"></div>
-  </body>
-</html>
deleted file mode 100644
--- a/browser/components/readinglist/test/ReadingListTestUtils.jsm
+++ /dev/null
@@ -1,169 +0,0 @@
-"use strict";
-
-this.EXPORTED_SYMBOLS = [
-  "ReadingListTestUtils",
-];
-
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource:///modules/readinglist/ReadingList.jsm");
-
-
-/** Preference name controlling whether the ReadingList feature is enabled/disabled. */
-const PREF_RL_ENABLED = "browser.readinglist.enabled";
-
-
-/**
- * Utilities for testing the ReadingList sidebar.
- */
-function SidebarUtils(window, assert) {
-  this.window = window;
-  this.Assert = assert;
-}
-
-SidebarUtils.prototype = {
-  /**
-   * Reference to the RLSidebar object controlling the ReadingList sidebar UI.
-   * @type {object}
-   */
-  get RLSidebar() {
-    return this.window.SidebarUI.browser.contentWindow.RLSidebar;
-  },
-
-  /**
-   * Reference to the list container element in the sidebar.
-   * @type {Element}
-   */
-  get list() {
-    return this.RLSidebar.list;
-  },
-
-  /**
-   * Opens the sidebar and waits until it finishes building its list.
-   * @return {Promise} Resolved when the sidebar's list is ready.
-   */
-  showSidebar: Task.async(function* () {
-    yield this.window.ReadingListUI.showSidebar();
-    yield this.RLSidebar.listPromise;
-  }),
-
-  /**
-   * Check that the number of elements in the list matches the expected count.
-   * @param {number} count - Expected number of items.
-   */
-  expectNumItems(count) {
-    this.Assert.equal(this.list.childElementCount, count,
-                      "Should have expected number of items in the sidebar list");
-  },
-
-  /**
-   * Check all items in the sidebar list, ensuring the DOM matches the data.
-   */
-  checkAllItems() {
-    for (let itemNode of this.list.children) {
-      this.checkSidebarItem(itemNode);
-    }
-  },
-
-  /**
-   * Run a series of sanity checks for an element in the list associated with
-   * an Item, ensuring the DOM matches the data.
-   */
-  checkItem(node) {
-    let item = this.RLSidebar.getItemFromNode(node);
-
-    this.Assert.ok(node.classList.contains("item"),
-                   "Node should have .item class");
-    this.Assert.equal(node.id, "item-" + item.id,
-                      "Node should have correct ID");
-    this.Assert.equal(node.getAttribute("title"), item.title + "\n" + item.url.spec,
-                      "Node should have correct title attribute");
-    this.Assert.equal(node.querySelector(".item-title").textContent, item.title,
-                      "Node's title element's text should match item title");
-
-    let domain = item.uri.spec;
-    try {
-      domain = item.uri.host;
-    }
-    catch (err) {}
-    this.Assert.equal(node.querySelector(".item-domain").textContent, domain,
-                      "Node's domain element's text should match item title");
-  },
-
-  expectSelectedId(itemId) {
-    let selectedItem = this.RLSidebar.selectedItem;
-    if (itemId == null) {
-      this.Assert.equal(selectedItem, null, "Should have no selected item");
-    } else {
-      this.Assert.notEqual(selectedItem, null, "selectedItem should not be null");
-      let selectedId = this.RLSidebar.getItemIdFromNode(selectedItem);
-      this.Assert.equal(itemId, selectedId, "Should have currect item selected");
-    }
-  },
-
-  expectActiveId(itemId) {
-    let activeItem = this.RLSidebar.activeItem;
-    if (itemId == null) {
-      this.Assert.equal(activeItem, null, "Should have no active item");
-    } else {
-      this.Assert.notEqual(activeItem, null, "activeItem should not be null");
-      let activeId = this.RLSidebar.getItemIdFromNode(activeItem);
-      this.Assert.equal(itemId, activeId, "Should have correct item active");
-    }
-  },
-};
-
-
-/**
- * Utilities for testing the ReadingList.
- */
-this.ReadingListTestUtils = {
-  /**
-   * Whether the ReadingList feature is enabled or not.
-   * @type {boolean}
-   */
-  get enabled() {
-    return Preferences.get(PREF_RL_ENABLED, false);
-  },
-  set enabled(value) {
-    Preferences.set(PREF_RL_ENABLED, !!value);
-  },
-
-  /**
-   * Utilities for testing the ReadingList sidebar.
-   */
-  SidebarUtils: SidebarUtils,
-
-  /**
-   * Synthetically add an item to the ReadingList.
-   * @param {object|[object]} data - Object or array of objects to pass to the
-   *                                 Item constructor.
-   * @return {Promise} Promise that gets fulfilled with the item or items added.
-   */
-  addItem(data) {
-    if (Array.isArray(data)) {
-      let promises = [];
-      for (let itemData of data) {
-        promises.push(this.addItem(itemData));
-      }
-      return Promise.all(promises);
-    }
-    return ReadingList.addItem(data);
-  },
-
-  /**
-   * Cleanup all data, resetting to a blank state.
-   */
-  cleanup: Task.async(function *() {
-    Preferences.reset(PREF_RL_ENABLED);
-    let items = [];
-    yield ReadingList.forEachItem(i => items.push(i));
-    for (let item of items) {
-      yield ReadingList.deleteItem(item);
-    }
-  }),
-};
deleted file mode 100644
--- a/browser/components/readinglist/test/browser/browser.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[DEFAULT]
-support-files =
-  head.js
-
-[browser_ui_enable_disable.js]
-[browser_sidebar_list.js]
-;[browser_sidebar_mouse_nav.js]
deleted file mode 100644
--- a/browser/components/readinglist/test/browser/browser_sidebar_list.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * This tests the basic functionality of the sidebar to list items.
- */
-
-add_task(function*() {
-  registerCleanupFunction(function*() {
-    ReadingListUI.hideSidebar();
-    yield RLUtils.cleanup();
-  });
-
-  RLUtils.enabled = true;
-
-  yield RLSidebarUtils.showSidebar();
-  let RLSidebar = RLSidebarUtils.RLSidebar;
-  let sidebarDoc = SidebarUI.browser.contentDocument;
-  Assert.equal(RLSidebar.numItems, 0, "Should start with no items");
-  Assert.equal(RLSidebar.activeItem, null, "Should start with no active item");
-  Assert.equal(RLSidebar.activeItem, null, "Should start with no selected item");
-
-  info("Adding first item");
-  yield RLUtils.addItem({
-    url: "http://example.com/article1",
-    title: "Article 1",
-  });
-  RLSidebarUtils.expectNumItems(1);
-
-  info("Adding more items");
-  yield RLUtils.addItem([{
-    url: "http://example.com/article2",
-    title: "Article 2",
-  }, {
-    url: "http://example.com/article3",
-    title: "Article 3",
-  }]);
-  RLSidebarUtils.expectNumItems(3);
-
-  info("Closing sidebar");
-  ReadingListUI.hideSidebar();
-
-  info("Adding another item");
-  yield RLUtils.addItem({
-    url: "http://example.com/article4",
-    title: "Article 4",
-  });
-
-  info("Re-opening sidebar");
-  yield RLSidebarUtils.showSidebar();
-  RLSidebarUtils.expectNumItems(4);
-});
deleted file mode 100644
--- a/browser/components/readinglist/test/browser/browser_sidebar_mouse_nav.js
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * Test mouse navigation for selecting items in the sidebar.
- */
-
-
-function mouseInteraction(mouseEvent, responseEvent, itemNode) {
-  let eventPromise = BrowserTestUtils.waitForEvent(RLSidebarUtils.list, responseEvent);
-  let details = {};
-  if (mouseEvent != "click") {
-    details.type = mouseEvent;
-  }
-
-  EventUtils.synthesizeMouseAtCenter(itemNode, details, itemNode.ownerDocument.defaultView);
-  return eventPromise;
-}
-
-add_task(function*() {
-  registerCleanupFunction(function*() {
-    ReadingListUI.hideSidebar();
-    yield RLUtils.cleanup();
-  });
-
-  RLUtils.enabled = true;
-
-  let itemData = [{
-    url: "http://example.com/article1",
-    title: "Article 1",
-  }, {
-    url: "http://example.com/article2",
-    title: "Article 2",
-  }, {
-    url: "http://example.com/article3",
-    title: "Article 3",
-  }, {
-    url: "http://example.com/article4",
-    title: "Article 4",
-  }, {
-    url: "http://example.com/article5",
-    title: "Article 5",
-  }];
-  info("Adding initial mock data");
-  yield RLUtils.addItem(itemData);
-
-  info("Fetching items");
-  let items = yield ReadingList.iterator({ sort: "url" }).items(itemData.length);
-
-  info("Opening sidebar");
-  yield RLSidebarUtils.showSidebar();
-  RLSidebarUtils.expectNumItems(5);
-  RLSidebarUtils.expectSelectedId(null);
-  RLSidebarUtils.expectActiveId(null);
-
-  info("Mouse move over item 1");
-  yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]);
-  RLSidebarUtils.expectSelectedId(items[0].id);
-  RLSidebarUtils.expectActiveId(null);
-
-  info("Mouse move over item 2");
-  yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[1]);
-  RLSidebarUtils.expectSelectedId(items[1].id);
-  RLSidebarUtils.expectActiveId(null);
-
-  info("Mouse move over item 5");
-  yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[4]);
-  RLSidebarUtils.expectSelectedId(items[4].id);
-  RLSidebarUtils.expectActiveId(null);
-
-  info("Mouse move over item 1 again");
-  yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]);
-  RLSidebarUtils.expectSelectedId(items[0].id);
-  RLSidebarUtils.expectActiveId(null);
-
-  info("Mouse click on item 1");
-  yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[0]);
-  RLSidebarUtils.expectSelectedId(items[0].id);
-  RLSidebarUtils.expectActiveId(items[0].id);
-
-  info("Mouse click on item 3");
-  yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[2]);
-  RLSidebarUtils.expectSelectedId(items[2].id);
-  RLSidebarUtils.expectActiveId(items[2].id);
-});
deleted file mode 100644
--- a/browser/components/readinglist/test/browser/browser_ui_enable_disable.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Test enabling/disabling the entire ReadingList feature via the
- * browser.readinglist.enabled preference.
- */
-
-function checkRLState() {
-  let enabled = RLUtils.enabled;
-  info("Checking ReadingList UI is " + (enabled ? "enabled" : "disabled"));
-
-  let sidebarBroadcaster = document.getElementById("readingListSidebar");
-  let sidebarMenuitem = document.getElementById("menu_readingListSidebar");
-
-  let bookmarksMenubarItem = document.getElementById("menu_readingList");
-  let bookmarksMenubarSeparator = document.getElementById("menu_readingListSeparator");
-
-  if (enabled) {
-    Assert.notEqual(sidebarBroadcaster.getAttribute("hidden"), "true",
-                    "Sidebar broadcaster should not be hidden");
-    Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true",
-                    "Sidebar menuitem should be visible");
-
-    // Currently disabled on OSX.
-    if (bookmarksMenubarItem) {
-      Assert.notEqual(bookmarksMenubarItem.getAttribute("hidden"), "true",
-                      "RL bookmarks submenu in menubar should not be hidden");
-      Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true",
-                      "RL bookmarks separator in menubar should be visible");
-    }
-  } else {
-    Assert.equal(sidebarBroadcaster.getAttribute("hidden"), "true",
-                 "Sidebar broadcaster should be hidden");
-    Assert.equal(sidebarMenuitem.getAttribute("hidden"), "true",
-                 "Sidebar menuitem should be hidden");
-    Assert.equal(ReadingListUI.isSidebarOpen, false,
-                 "ReadingListUI should not think sidebar is open");
-
-    // Currently disabled on OSX.
-    if (bookmarksMenubarItem) {
-      Assert.equal(bookmarksMenubarItem.getAttribute("hidden"), "true",
-                      "RL bookmarks submenu in menubar should not be hidden");
-      Assert.equal(sidebarMenuitem.getAttribute("hidden"), "true",
-                      "RL bookmarks separator in menubar should be visible");
-    }
-  }
-
-  if (!enabled) {
-    Assert.equal(SidebarUI.isOpen, false, "Sidebar should not be open");
-  }
-}
-
-add_task(function*() {
-  info("Start with ReadingList disabled");
-  RLUtils.enabled = false;
-  checkRLState();
-  info("Enabling ReadingList");
-  RLUtils.enabled = true;
-  checkRLState();
-
-  info("Opening ReadingList sidebar");
-  yield ReadingListUI.showSidebar();
-  Assert.ok(SidebarUI.isOpen, "Sidebar should be open");
-  Assert.equal(SidebarUI.currentID, "readingListSidebar", "Sidebar should have ReadingList loaded");
-
-  info("Disabling ReadingList");
-  RLUtils.enabled = false;
-  Assert.ok(!SidebarUI.isOpen, "Sidebar should be closed");
-  checkRLState();
-});
deleted file mode 100644
--- a/browser/components/readinglist/test/browser/head.js
+++ /dev/null
@@ -1,13 +0,0 @@
-XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
-                                  "resource:///modules/readinglist/ReadingList.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ReadingListTestUtils",
-                                  "resource://testing-common/ReadingListTestUtils.jsm");
-
-
-XPCOMUtils.defineLazyGetter(this, "RLUtils", () => {
-  return ReadingListTestUtils;
-});
-
-XPCOMUtils.defineLazyGetter(this, "RLSidebarUtils", () => {
-  return new RLUtils.SidebarUtils(window, Assert);
-});
deleted file mode 100644
--- a/browser/components/readinglist/test/xpcshell/head.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-do_get_profile(); // fxa needs a profile directory for storage.
-
-Cu.import("resource://gre/modules/FxAccounts.jsm");
-Cu.import("resource://gre/modules/FxAccountsClient.jsm");
-
-// Create a mocked FxAccounts object with a signed-in, verified user.
-function* createMockFxA() {
-
-  function MockFxAccountsClient() {
-    this._email = "nobody@example.com";
-    this._verified = false;
-
-    this.accountStatus = function(uid) {
-      let deferred = Promise.defer();
-      deferred.resolve(!!uid && (!this._deletedOnServer));
-      return deferred.promise;
-    };
-
-    this.signOut = function() { return Promise.resolve(); };
-
-    FxAccountsClient.apply(this);
-  }
-
-  MockFxAccountsClient.prototype = {
-    __proto__: FxAccountsClient.prototype
-  }
-
-  function MockFxAccounts() {
-    return new FxAccounts({
-      fxAccountsClient: new MockFxAccountsClient(),
-      getAssertion: () => Promise.resolve("assertion"),
-    });
-  }
-
-  let fxa = new MockFxAccounts();
-  let credentials = {
-    email: "foo@example.com",
-    uid: "1234@lcip.org",
-    assertion: "foobar",
-    sessionToken: "dead",
-    kA: "beef",
-    kB: "cafe",
-    verified: true
-  };
-
-  yield fxa.setSignedInUser(credentials);
-  return fxa;
-}
deleted file mode 100644
--- a/browser/components/readinglist/test/xpcshell/test_ReadingList.js
+++ /dev/null
@@ -1,782 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-let gDBFile = do_get_profile();
-
-Cu.import("resource:///modules/readinglist/ReadingList.jsm");
-Cu.import("resource:///modules/readinglist/SQLiteStore.jsm");
-Cu.import("resource://gre/modules/Sqlite.jsm");
-Cu.import("resource://gre/modules/Timer.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-
-Log.repository.getLogger("readinglist.api").level = Log.Level.All;
-Log.repository.getLogger("readinglist.api").addAppender(new Log.DumpAppender());
-
-var gList;
-var gItems;
-
-function run_test() {
-  run_next_test();
-}
-
-add_task(function* prepare() {
-  gList = ReadingList;
-  Assert.ok(gList);
-  gDBFile.append(gList._store.pathRelativeToProfileDir);
-  do_register_cleanup(function* () {
-    // Wait for the list's store to close its connection to the database.
-    yield gList.destroy();
-    if (gDBFile.exists()) {
-      gDBFile.remove(true);
-    }
-  });
-
-  gItems = [];
-  for (let i = 0; i < 3; i++) {
-    gItems.push({
-      guid: `guid${i}`,
-      url: `http://example.com/${i}`,
-      resolvedURL: `http://example.com/resolved/${i}`,
-      title: `title ${i}`,
-      excerpt: `excerpt ${i}`,
-      unread: 0,
-      favorite: 0,
-      isArticle: 1,
-      storedOn: Date.now(),
-    });
-  }
-
-  for (let item of gItems) {
-    let addedItem = yield gList.addItem(item);
-    checkItems(addedItem, item);
-  }
-});
-
-add_task(function* item_properties() {
-  // get an item
-  let iter = gList.iterator({
-    sort: "guid",
-  });
-  let item = (yield iter.items(1))[0];
-  Assert.ok(item);
-
-  Assert.ok(item.uri);
-  Assert.ok(item.uri instanceof Ci.nsIURI);
-  Assert.equal(item.uri.spec, item._record.url);
-
-  Assert.ok(item.resolvedURI);
-  Assert.ok(item.resolvedURI instanceof Ci.nsIURI);
-  Assert.equal(item.resolvedURI.spec, item._record.resolvedURL);
-
-  Assert.ok(item.addedOn);
-  Assert.ok(item.addedOn instanceof Cu.getGlobalForObject(ReadingList).Date);
-
-  Assert.ok(item.storedOn);
-  Assert.ok(item.storedOn instanceof Cu.getGlobalForObject(ReadingList).Date);
-
-  Assert.ok(typeof(item.favorite) == "boolean");
-  Assert.ok(typeof(item.isArticle) == "boolean");
-  Assert.ok(typeof(item.unread) == "boolean");
-
-  Assert.equal(item.id, hash(item._record.url));
-});
-
-add_task(function* constraints() {
-  // add an item again
-  let err = null;
-  try {
-    yield gList.addItem(gItems[0]);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-
-  // add a new item with an existing guid
-  let item = kindOfClone(gItems[0]);
-  item.guid = gItems[0].guid;
-  err = null;
-  try {
-    yield gList.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-
-  // add a new item with an existing url
-  item = kindOfClone(gItems[0]);
-  item.url = gItems[0].url;
-  err = null;
-  try {
-    yield gList.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-
-  // add a new item with an existing resolvedURL
-  item = kindOfClone(gItems[0]);
-  item.resolvedURL = gItems[0].resolvedURL;
-  err = null;
-  try {
-    yield gList.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-
-  // add a new item with no url
-  item = kindOfClone(gItems[0]);
-  delete item.url;
-  err = null;
-  try {
-    yield gList.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Error);
-  Assert.ok(!(err instanceof ReadingList.Error.Exists));
-  Assert.ok(!(err instanceof ReadingList.Error.Deleted));
-
-  // update an item with no url
-  item = (yield gList.item({ guid: gItems[0].guid }));
-  Assert.ok(item);
-  let oldURL = item._record.url;
-  item._record.url = null;
-  err = null;
-  try {
-    yield gList.updateItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  item._record.url = oldURL;
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Error);
-  Assert.ok(!(err instanceof ReadingList.Error.Exists));
-  Assert.ok(!(err instanceof ReadingList.Error.Deleted));
-
-  // add an item with a bogus property
-  item = kindOfClone(gItems[0]);
-  item.bogus = "gnarly";
-  err = null;
-  try {
-    yield gList.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Error);
-  Assert.ok(!(err instanceof ReadingList.Error.Exists));
-  Assert.ok(!(err instanceof ReadingList.Error.Deleted));
-
-  // add a new item with no guid, which is allowed
-  item = kindOfClone(gItems[0]);
-  delete item.guid;
-  err = null;
-  let rlitem1;
-  try {
-    rlitem1 = yield gList.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(!err, err ? err.message : undefined);
-
-  // add a second item with no guid, which is allowed
-  item = kindOfClone(gItems[1]);
-  delete item.guid;
-  err = null;
-  let rlitem2;
-  try {
-    rlitem2 = yield gList.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(!err, err ? err.message : undefined);
-
-  // Delete the two previous items since other tests assume the store contains
-  // only gItems.
-  yield gList.deleteItem(rlitem1);
-  yield gList.deleteItem(rlitem2);
-  let items = [];
-  yield gList.forEachItem(i => items.push(i), { url: [rlitem1.uri.spec, rlitem2.uri.spec] });
-  Assert.equal(items.length, 0);
-});
-
-add_task(function* count() {
-  let count = yield gList.count();
-  Assert.equal(count, gItems.length);
-
-  count = yield gList.count({
-    guid: gItems[0].guid,
-  });
-  Assert.equal(count, 1);
-});
-
-add_task(function* forEachItem() {
-  // all items
-  let items = [];
-  yield gList.forEachItem(item => items.push(item), {
-    sort: "guid",
-  });
-  checkItems(items, gItems);
-
-  // first item
-  items = [];
-  yield gList.forEachItem(item => items.push(item), {
-    limit: 1,
-    sort: "guid",
-  });
-  checkItems(items, gItems.slice(0, 1));
-
-  // last item
-  items = [];
-  yield gList.forEachItem(item => items.push(item), {
-    limit: 1,
-    sort: "guid",
-    descending: true,
-  });
-  checkItems(items, gItems.slice(gItems.length - 1, gItems.length));
-
-  // match on a scalar property
-  items = [];
-  yield gList.forEachItem(item => items.push(item), {
-    guid: gItems[0].guid,
-  });
-  checkItems(items, gItems.slice(0, 1));
-
-  // match on an array
-  items = [];
-  yield gList.forEachItem(item => items.push(item), {
-    guid: gItems.map(i => i.guid),
-    sort: "guid",
-  });
-  checkItems(items, gItems);
-
-  // match on AND'ed properties
-  items = [];
-  yield gList.forEachItem(item => items.push(item), {
-    guid: gItems.map(i => i.guid),
-    title: gItems[0].title,
-    sort: "guid",
-  });
-  checkItems(items, [gItems[0]]);
-
-  // match on OR'ed properties
-  items = [];
-  yield gList.forEachItem(item => items.push(item), {
-    guid: gItems[1].guid,
-    sort: "guid",
-  }, {
-    guid: gItems[0].guid,
-  });
-  checkItems(items, [gItems[0], gItems[1]]);
-
-  // match on AND'ed and OR'ed properties
-  items = [];
-  yield gList.forEachItem(item => items.push(item), {
-    guid: gItems.map(i => i.guid),
-    title: gItems[1].title,
-    sort: "guid",
-  }, {
-    guid: gItems[0].guid,
-  });
-  checkItems(items, [gItems[0], gItems[1]]);
-});
-
-add_task(function* forEachSyncedDeletedItem() {
-  let deletedItem = yield gList.addItem({
-    guid: "forEachSyncedDeletedItem",
-    url: "http://example.com/forEachSyncedDeletedItem",
-  });
-  deletedItem._record.syncStatus = gList.SyncStatus.SYNCED;
-  yield gList.deleteItem(deletedItem);
-  let guids = [];
-  yield gList.forEachSyncedDeletedGUID(guid => guids.push(guid));
-  Assert.equal(guids.length, 1);
-  Assert.equal(guids[0], deletedItem.guid);
-});
-
-add_task(function* forEachItem_promises() {
-  // promises resolved immediately
-  let items = [];
-  yield gList.forEachItem(item => {
-    items.push(item);
-    return Promise.resolve();
-  }, {
-    sort: "guid",
-  });
-  checkItems(items, gItems);
-
-  // promises resolved after a delay
-  items = [];
-  let i = 0;
-  let promises = [];
-  yield gList.forEachItem(item => {
-    items.push(item);
-    // The previous promise should have been resolved by now.
-    if (i > 0) {
-      Assert.equal(promises[i - 1], null);
-    }
-    // Make a new promise that should continue iteration when resolved.
-    let this_i = i++;
-    let promise = new Promise(resolve => {
-      // Resolve the promise one second from now.  The idea is that if
-      // forEachItem works correctly, then the callback should not be called
-      // again before the promise resolves -- before one second elapases.
-      // Maybe there's a better way to do this that doesn't hinge on timeouts.
-      setTimeout(() => {
-        promises[this_i] = null;
-        resolve();
-      }, 0);
-    });
-    promises.push(promise);
-    return promise;
-  }, {
-    sort: "guid",
-  });
-  checkItems(items, gItems);
-});
-
-add_task(function* iterator_forEach() {
-  // no limit
-  let items = [];
-  let iter = gList.iterator({
-    sort: "guid",
-  });
-  yield iter.forEach(item => items.push(item));
-  checkItems(items, gItems);
-
-  // limit one each time
-  items = [];
-  iter = gList.iterator({
-    sort: "guid",
-  });
-  for (let i = 0; i < gItems.length; i++) {
-    yield iter.forEach(item => items.push(item), 1);
-    checkItems(items, gItems.slice(0, i + 1));
-  }
-  yield iter.forEach(item => items.push(item), 100);
-  checkItems(items, gItems);
-  yield iter.forEach(item => items.push(item));
-  checkItems(items, gItems);
-
-  // match on a scalar property
-  items = [];
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems[0].guid,
-  });
-  yield iter.forEach(item => items.push(item));
-  checkItems(items, [gItems[0]]);
-
-  // match on an array
-  items = [];
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems.map(i => i.guid),
-  });
-  yield iter.forEach(item => items.push(item));
-  checkItems(items, gItems);
-
-  // match on AND'ed properties
-  items = [];
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems.map(i => i.guid),
-    title: gItems[0].title,
-  });
-  yield iter.forEach(item => items.push(item));
-  checkItems(items, [gItems[0]]);
-
-  // match on OR'ed properties
-  items = [];
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems[1].guid,
-  }, {
-    guid: gItems[0].guid,
-  });
-  yield iter.forEach(item => items.push(item));
-  checkItems(items, [gItems[0], gItems[1]]);
-
-  // match on AND'ed and OR'ed properties
-  items = [];
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems.map(i => i.guid),
-    title: gItems[1].title,
-  }, {
-    guid: gItems[0].guid,
-  });
-  yield iter.forEach(item => items.push(item));
-  checkItems(items, [gItems[0], gItems[1]]);
-});
-
-add_task(function* iterator_items() {
-  // no limit
-  let iter = gList.iterator({
-    sort: "guid",
-  });
-  let items = yield iter.items(gItems.length);
-  checkItems(items, gItems);
-  items = yield iter.items(100);
-  checkItems(items, []);
-
-  // limit one each time
-  iter = gList.iterator({
-    sort: "guid",
-  });
-  for (let i = 0; i < gItems.length; i++) {
-    items = yield iter.items(1);
-    checkItems(items, gItems.slice(i, i + 1));
-  }
-  items = yield iter.items(100);
-  checkItems(items, []);
-
-  // match on a scalar property
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems[0].guid,
-  });
-  items = yield iter.items(gItems.length);
-  checkItems(items, [gItems[0]]);
-
-  // match on an array
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems.map(i => i.guid),
-  });
-  items = yield iter.items(gItems.length);
-  checkItems(items, gItems);
-
-  // match on AND'ed properties
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems.map(i => i.guid),
-    title: gItems[0].title,
-  });
-  items = yield iter.items(gItems.length);
-  checkItems(items, [gItems[0]]);
-
-  // match on OR'ed properties
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems[1].guid,
-  }, {
-    guid: gItems[0].guid,
-  });
-  items = yield iter.items(gItems.length);
-  checkItems(items, [gItems[0], gItems[1]]);
-
-  // match on AND'ed and OR'ed properties
-  iter = gList.iterator({
-    sort: "guid",
-    guid: gItems.map(i => i.guid),
-    title: gItems[1].title,
-  }, {
-    guid: gItems[0].guid,
-  });
-  items = yield iter.items(gItems.length);
-  checkItems(items, [gItems[0], gItems[1]]);
-});
-
-add_task(function* iterator_forEach_promise() {
-  // promises resolved immediately
-  let items = [];
-  let iter = gList.iterator({
-    sort: "guid",
-  });
-  yield iter.forEach(item => {
-    items.push(item);
-    return Promise.resolve();
-  });
-  checkItems(items, gItems);
-
-  // promises resolved after a delay
-  // See forEachItem_promises above for comments on this part.
-  items = [];
-  let i = 0;
-  let promises = [];
-  iter = gList.iterator({
-    sort: "guid",
-  });
-  yield iter.forEach(item => {
-    items.push(item);
-    if (i > 0) {
-      Assert.equal(promises[i - 1], null);
-    }
-    let this_i = i++;
-    let promise = new Promise(resolve => {
-      setTimeout(() => {
-        promises[this_i] = null;
-        resolve();
-      }, 0);
-    });
-    promises.push(promise);
-    return promise;
-  });
-  checkItems(items, gItems);
-});
-
-add_task(function* item() {
-  let item = yield gList.item({ guid: gItems[0].guid });
-  checkItems([item], [gItems[0]]);
-
-  item = yield gList.item({ guid: gItems[1].guid });
-  checkItems([item], [gItems[1]]);
-});
-
-add_task(function* itemForURL() {
-  let item = yield gList.itemForURL(gItems[0].url);
-  checkItems([item], [gItems[0]]);
-
-  item = yield gList.itemForURL(gItems[1].url);
-  checkItems([item], [gItems[1]]);
-});
-
-add_task(function* updateItem() {
-  // get an item
-  let items = [];
-  yield gList.forEachItem(i => items.push(i), {
-    guid: gItems[0].guid,
-  });
-  Assert.equal(items.length, 1);
-  let item = items[0];
-
-  // update its title
-  let newTitle = "updateItem new title";
-  Assert.notEqual(item.title, newTitle);
-  item.title = newTitle;
-  yield gList.updateItem(item);
-
-  // get the item again
-  items = [];
-  yield gList.forEachItem(i => items.push(i), {
-    guid: gItems[0].guid,
-  });
-  Assert.equal(items.length, 1);
-  item = items[0];
-  Assert.equal(item.title, newTitle);
-});
-
-add_task(function* item_setRecord() {
-  // get an item
-  let iter = gList.iterator({
-    sort: "guid",
-  });
-  let item = (yield iter.items(1))[0];
-  Assert.ok(item);
-
-  // Set item._record followed by an updateItem.  After fetching the item again,
-  // its title should be the new title.
-  let newTitle = "item_setRecord title 1";
-  item._record.title = newTitle;
-  yield gList.updateItem(item);
-  Assert.equal(item.title, newTitle);
-  iter = gList.iterator({
-    sort: "guid",
-  });
-  let sameItem = (yield iter.items(1))[0];
-  Assert.ok(item === sameItem);
-  Assert.equal(sameItem.title, newTitle);
-
-  // Set item.title directly and call updateItem.  After fetching the item
-  // again, its title should be the new title.
-  newTitle = "item_setRecord title 2";
-  item.title = newTitle;
-  yield gList.updateItem(item);
-  Assert.equal(item.title, newTitle);
-  iter = gList.iterator({
-    sort: "guid",
-  });
-  sameItem = (yield iter.items(1))[0];
-  Assert.ok(item === sameItem);
-  Assert.equal(sameItem.title, newTitle);
-
-  // Setting _record to an object with a bogus property should throw.
-  let err = null;
-  try {
-    item._record = { bogus: "gnarly" };
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err.message);
-  Assert.ok(err.message.indexOf("Unrecognized item property:") >= 0);
-});
-
-add_task(function* listeners() {
-  Assert.equal((yield gList.count()), gItems.length);
-  // add an item
-  let resolve;
-  let listenerPromise = new Promise(r => resolve = r);
-  let listener = {
-    onItemAdded: resolve,
-  };
-  gList.addListener(listener);
-  let item = kindOfClone(gItems[0]);
-  let items = yield Promise.all([listenerPromise, gList.addItem(item)]);
-  Assert.ok(items[0]);
-  Assert.ok(items[0] === items[1]);
-  gList.removeListener(listener);
-  Assert.equal((yield gList.count()), gItems.length + 1);
-
-  // update an item
-  listenerPromise = new Promise(r => resolve = r);
-  listener = {
-    onItemUpdated: resolve,
-  };
-  gList.addListener(listener);
-  items[0].title = "listeners new title";
-  yield gList.updateItem(items[0]);
-  let listenerItem = yield listenerPromise;
-  Assert.ok(listenerItem);
-  Assert.ok(listenerItem === items[0]);
-  gList.removeListener(listener);
-  Assert.equal((yield gList.count()), gItems.length + 1);
-
-  // delete an item
-  listenerPromise = new Promise(r => resolve = r);
-  listener = {
-    onItemDeleted: resolve,
-  };
-  gList.addListener(listener);
-  items[0].delete();
-  listenerItem = yield listenerPromise;
-  Assert.ok(listenerItem);
-  Assert.ok(listenerItem === items[0]);
-  gList.removeListener(listener);
-  Assert.equal((yield gList.count()), gItems.length);
-});
-
-// This test deletes items so it should probably run last of the 'gItems' tests...
-add_task(function* deleteItem() {
-  // delete first item with item.delete()
-  let iter = gList.iterator({
-    sort: "guid",
-  });
-  let item = (yield iter.items(1))[0];
-  Assert.ok(item);
-  let {url, guid} = item;
-  Assert.ok((yield gList.itemForURL(url)), "should be able to get the item by URL before deletion");
-  Assert.ok((yield gList.item({guid})), "should be able to get the item by GUID before deletion");
-
-  yield item.delete();
-  try {
-    yield item.delete();
-    Assert.ok(false, "should not successfully delete the item a second time")
-  } catch(ex) {
-    Assert.ok(ex instanceof ReadingList.Error.Deleted);
-  }
-
-  Assert.ok(!(yield gList.itemForURL(url)), "should fail to get a deleted item by URL");
-  Assert.ok(!(yield gList.item({guid})), "should fail to get a deleted item by GUID");
-
-  gItems[0].list = null;
-  Assert.equal((yield gList.count()), gItems.length - 1);
-  let items = [];
-  yield gList.forEachItem(i => items.push(i), {
-    sort: "guid",
-  });
-  checkItems(items, gItems.slice(1));
-
-  // delete second item with list.deleteItem()
-  yield gList.deleteItem(items[0]);
-  try {
-    yield gList.deleteItem(items[0]);
-    Assert.ok(false, "should not successfully delete the item a second time")
-  } catch(ex) {
-    Assert.ok(ex instanceof ReadingList.Error.Deleted);
-  }
-  gItems[1].list = null;
-  Assert.equal((yield gList.count()), gItems.length - 2);
-  items = [];
-  yield gList.forEachItem(i => items.push(i), {
-    sort: "guid",
-  });
-  checkItems(items, gItems.slice(2));
-
-  // delete third item with list.deleteItem()
-  yield gList.deleteItem(items[0]);
-  gItems[2].list = null;
-  Assert.equal((yield gList.count()), gItems.length - 3);
-  items = [];
-  yield gList.forEachItem(i => items.push(i), {
-    sort: "guid",
-  });
-  checkItems(items, gItems.slice(3));
-});
-
-// Check that when we delete an item with a GUID it's no longer available as
-// an item
-add_task(function* deletedItemRemovedFromMap() {
-  yield gList.forEachItem(item => item.delete());
-  Assert.equal((yield gList.count()), 0);
-  let map = gList._itemsByNormalizedURL;
-  Assert.equal(gList._itemsByNormalizedURL.size, 0, [for (i of map.keys()) i]);
-  let record = {
-    guid: "test-item",
-    url: "http://localhost",
-    syncStatus: gList.SyncStatus.SYNCED,
-  }
-  let item = yield gList.addItem(record);
-  Assert.equal(map.size, 1);
-  yield item.delete();
-  Assert.equal(gList._itemsByNormalizedURL.size, 0, [for (i of map.keys()) i]);
-
-  // Now enumerate deleted items - should not come back.
-  yield gList.forEachSyncedDeletedGUID(() => {});
-  Assert.equal(gList._itemsByNormalizedURL.size, 0, [for (i of map.keys()) i]);
-});
-
-function checkItems(actualItems, expectedItems) {
-  Assert.equal(actualItems.length, expectedItems.length);
-  for (let i = 0; i < expectedItems.length; i++) {
-    for (let prop in expectedItems[i]._record) {
-      Assert.ok(prop in actualItems[i]._record, prop);
-      Assert.equal(actualItems[i]._record[prop], expectedItems[i][prop]);
-    }
-  }
-}
-
-function kindOfClone(item) {
-  let newItem = {};
-  for (let prop in item) {
-    newItem[prop] = item[prop];
-    if (typeof(newItem[prop]) == "string") {
-      newItem[prop] += " -- make this string different";
-    }
-  }
-  return newItem;
-}
-
-function hash(str) {
-  let hasher = Cc["@mozilla.org/security/hash;1"].
-               createInstance(Ci.nsICryptoHash);
-  hasher.init(Ci.nsICryptoHash.MD5);
-  let stream = Cc["@mozilla.org/io/string-input-stream;1"].
-               createInstance(Ci.nsIStringInputStream);
-  stream.data = str;
-  hasher.updateFromStream(stream, -1);
-  let binaryStr = hasher.finish(false);
-  let hexStr =
-    [("0" + binaryStr.charCodeAt(i).toString(16)).slice(-2) for (i in binaryStr)].
-    join("");
-  return hexStr;
-}
deleted file mode 100644
--- a/browser/components/readinglist/test/xpcshell/test_SQLiteStore.js
+++ /dev/null
@@ -1,333 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-Cu.import("resource:///modules/readinglist/ReadingList.jsm");
-Cu.import("resource:///modules/readinglist/SQLiteStore.jsm");
-Cu.import("resource://gre/modules/Sqlite.jsm");
-
-var gStore;
-var gItems;
-
-function run_test() {
-  run_next_test();
-}
-
-add_task(function* prepare() {
-  let basename = "reading-list-test.sqlite";
-  let dbFile = do_get_profile();
-  dbFile.append(basename);
-  function removeDB() {
-    if (dbFile.exists()) {
-      dbFile.remove(true);
-    }
-  }
-  removeDB();
-  do_register_cleanup(function* () {
-    // Wait for the store to close its connection to the database.
-    yield gStore.destroy();
-    removeDB();
-  });
-
-  gStore = new SQLiteStore(dbFile.path);
-
-  gItems = [];
-  for (let i = 0; i < 3; i++) {
-    gItems.push({
-      guid: `guid${i}`,
-      url: `http://example.com/${i}`,
-      resolvedURL: `http://example.com/resolved/${i}`,
-      title: `title ${i}`,
-      excerpt: `excerpt ${i}`,
-      unread: true,
-      addedOn: i,
-    });
-  }
-
-  for (let item of gItems) {
-    yield gStore.addItem(item);
-  }
-});
-
-add_task(function* constraints() {
-  // add an item again
-  let err = null;
-  try {
-    yield gStore.addItem(gItems[0]);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-  Assert.ok(err.message);
-  Assert.ok(err.message.indexOf("An item with the following property already exists:") >= 0);
-
-  // add a new item with an existing guid
-  function kindOfClone(item) {
-    let newItem = {};
-    for (let prop in item) {
-      newItem[prop] = item[prop];
-      if (typeof(newItem[prop]) == "string") {
-        newItem[prop] += " -- make this string different";
-      }
-    }
-    return newItem;
-  }
-  let item = kindOfClone(gItems[0]);
-  item.guid = gItems[0].guid;
-  err = null;
-  try {
-    yield gStore.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-  Assert.ok(err.message);
-  Assert.ok(err.message.indexOf("An item with the following property already exists: guid") >= 0);
-
-  // add a new item with an existing url
-  item = kindOfClone(gItems[0]);
-  item.url = gItems[0].url;
-  err = null;
-  try {
-    yield gStore.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-  Assert.ok(err.message);
-  Assert.ok(err.message.indexOf("An item with the following property already exists: url") >= 0);
-
-  // update an item with an existing url
-  item.guid = gItems[1].guid;
-  err = null;
-  try {
-    yield gStore.updateItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  // The failure actually happens on items.guid, not items.url, because the item
-  // is first looked up by url, and then its other properties are updated on the
-  // resulting row.
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-  Assert.ok(err.message);
-  Assert.ok(err.message.indexOf("An item with the following property already exists: guid") >= 0);
-
-  // add a new item with an existing resolvedURL
-  item = kindOfClone(gItems[0]);
-  item.resolvedURL = gItems[0].resolvedURL;
-  err = null;
-  try {
-    yield gStore.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-  Assert.ok(err.message);
-  Assert.ok(err.message.indexOf("An item with the following property already exists: resolvedURL") >= 0);
-
-  // update an item with an existing resolvedURL
-  item.url = gItems[1].url;
-  err = null;
-  try {
-    yield gStore.updateItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(err);
-  Assert.ok(err instanceof ReadingList.Error.Exists);
-  Assert.ok(err.message);
-  Assert.ok(err.message.indexOf("An item with the following property already exists: resolvedURL") >= 0);
-
-  // add a new item with no guid, which is allowed
-  item = kindOfClone(gItems[0]);
-  delete item.guid;
-  err = null;
-  try {
-    yield gStore.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(!err, err ? err.message : undefined);
-  let url1 = item.url;
-
-  // add a second new item with no guid, which is allowed
-  item = kindOfClone(gItems[1]);
-  delete item.guid;
-  err = null;
-  try {
-    yield gStore.addItem(item);
-  }
-  catch (e) {
-    err = e;
-  }
-  Assert.ok(!err, err ? err.message : undefined);
-  let url2 = item.url;
-
-  // Delete both items since other tests assume the store contains only gItems.
-  yield gStore.deleteItemByURL(url1);
-  yield gStore.deleteItemByURL(url2);
-  let items = [];
-  yield gStore.forEachItem(i => items.push(i), [{ url: [url1, url2] }]);
-  Assert.equal(items.length, 0);
-});
-
-add_task(function* count() {
-  let count = yield gStore.count();
-  Assert.equal(count, gItems.length);
-
-  count = yield gStore.count([{
-    guid: gItems[0].guid,
-  }]);
-  Assert.equal(count, 1);
-});
-
-add_task(function* forEachItem() {
-  // all items
-  let items = [];
-  yield gStore.forEachItem(item => items.push(item), [{
-    sort: "guid",
-  }]);
-  checkItems(items, gItems);
-
-  // first item
-  items = [];
-  yield gStore.forEachItem(item => items.push(item), [{
-    limit: 1,
-    sort: "guid",
-  }]);
-  checkItems(items, gItems.slice(0, 1));
-
-  // last item
-  items = [];
-  yield gStore.forEachItem(item => items.push(item), [{
-    limit: 1,
-    sort: "guid",
-    descending: true,
-  }]);
-  checkItems(items, gItems.slice(gItems.length - 1, gItems.length));
-
-  // match on a scalar property
-  items = [];
-  yield gStore.forEachItem(item => items.push(item), [{
-    guid: gItems[0].guid,
-  }]);
-  checkItems(items, gItems.slice(0, 1));
-
-  // match on an array
-  items = [];
-  yield gStore.forEachItem(item => items.push(item), [{
-    guid: gItems.map(i => i.guid),
-    sort: "guid",
-  }]);
-  checkItems(items, gItems);
-
-  // match on AND'ed properties
-  items = [];
-  yield gStore.forEachItem(item => items.push(item), [{
-    guid: gItems.map(i => i.guid),
-    title: gItems[0].title,
-    sort: "guid",
-  }]);
-  checkItems(items, [gItems[0]]);
-
-  // match on OR'ed properties
-  items = [];
-  yield gStore.forEachItem(item => items.push(item), [{
-    guid: gItems[1].guid,
-    sort: "guid",
-  }, {
-    guid: gItems[0].guid,
-  }]);
-  checkItems(items, [gItems[0], gItems[1]]);
-
-  // match on AND'ed and OR'ed properties
-  items = [];
-  yield gStore.forEachItem(item => items.push(item), [{
-    guid: gItems.map(i => i.guid),
-    title: gItems[1].title,
-    sort: "guid",
-  }, {
-    guid: gItems[0].guid,
-  }]);
-  checkItems(items, [gItems[0], gItems[1]]);
-});
-
-add_task(function* updateItem() {
-  let newTitle = "a new title";
-  gItems[0].title = newTitle;
-  yield gStore.updateItem(gItems[0]);
-  let item;
-  yield gStore.forEachItem(i => item = i, [{
-    guid: gItems[0].guid,
-  }]);
-  Assert.ok(item);
-  Assert.equal(item.title, gItems[0].title);
-});
-
-add_task(function* updateItemByGUID() {
-  let newTitle = "updateItemByGUID";
-  gItems[0].title = newTitle;
-  yield gStore.updateItemByGUID(gItems[0]);
-  let item;
-  yield gStore.forEachItem(i => item = i, [{
-    guid: gItems[0].guid,
-  }]);
-  Assert.ok(item);
-  Assert.equal(item.title, gItems[0].title);
-});
-
-// This test deletes items so it should probably run last.
-add_task(function* deleteItemByURL() {
-  // delete first item
-  yield gStore.deleteItemByURL(gItems[0].url);
-  Assert.equal((yield gStore.count()), gItems.length - 1);
-  let items = [];
-  yield gStore.forEachItem(i => items.push(i), [{
-    sort: "guid",
-  }]);
-  checkItems(items, gItems.slice(1));
-
-  // delete second item
-  yield gStore.deleteItemByURL(gItems[1].url);
-  Assert.equal((yield gStore.count()), gItems.length - 2);
-  items = [];
-  yield gStore.forEachItem(i => items.push(i), [{
-    sort: "guid",
-  }]);
-  checkItems(items, gItems.slice(2));
-});
-
-// This test deletes items so it should probably run last.
-add_task(function* deleteItemByGUID() {
-  // delete third item
-  yield gStore.deleteItemByGUID(gItems[2].guid);
-  Assert.equal((yield gStore.count()), gItems.length - 3);
-  let items = [];
-  yield gStore.forEachItem(i => items.push(i), [{
-    sort: "guid",
-  }]);
-  checkItems(items, gItems.slice(3));
-});
-
-function checkItems(actualItems, expectedItems) {
-  Assert.equal(actualItems.length, expectedItems.length);
-  for (let i = 0; i < expectedItems.length; i++) {
-    for (let prop in expectedItems[i]) {
-      Assert.ok(prop in actualItems[i], prop);
-      Assert.equal(actualItems[i][prop], expectedItems[i][prop]);
-    }
-  }
-}
deleted file mode 100644
--- a/browser/components/readinglist/test/xpcshell/test_ServerClient.js
+++ /dev/null
@@ -1,285 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-Cu.import("resource://testing-common/httpd.js");
-Cu.import("resource:///modules/readinglist/ServerClient.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-
-let appender = new Log.DumpAppender();
-for (let logName of ["FirefoxAccounts", "readinglist.serverclient"]) {
-  Log.repository.getLogger(logName).addAppender(appender);
-}
-
-// Some test servers we use.
-let Server = function(handlers) {
-  this._server = null;
-  this._handlers = handlers;
-}
-
-Server.prototype = {
-  start() {
-    this._server = new HttpServer();
-    for (let [path, handler] in Iterator(this._handlers)) {
-      // httpd.js seems to swallow exceptions
-      let thisHandler = handler;
-      let wrapper = (request, response) => {
-        try {
-          thisHandler(request, response);
-        } catch (ex) {
-          print("**** Handler for", path, "failed:", ex, ex.stack);
-          throw ex;
-        }
-      }
-      this._server.registerPathHandler(path, wrapper);
-    }
-    this._server.start(-1);
-  },
-
-  stop() {
-    return new Promise(resolve => {
-      this._server.stop(resolve);
-      this._server = null;
-    });
-  },
-
-  get host() {
-    return "http://localhost:" + this._server.identity.primaryPort;
-  },
-};
-
-// An OAuth server that hands out tokens.
-function OAuthTokenServer() {
-  let server;
-  let handlers = {
-    "/v1/authorization": (request, response) => {
-      response.setStatusLine("1.1", 200, "OK");
-      let token = "token" + server.numTokenFetches;
-      print("Test OAuth server handing out token", token);
-      server.numTokenFetches += 1;
-      server.activeTokens.add(token);
-      response.write(JSON.stringify({access_token: token}));
-    },
-    "/v1/destroy": (request, response) => {
-      // Getting the body seems harder than it should be!
-      let sis = Cc["@mozilla.org/scriptableinputstream;1"]
-                .createInstance(Ci.nsIScriptableInputStream);
-      sis.init(request.bodyInputStream);
-      let body = JSON.parse(sis.read(sis.available()));
-      sis.close();
-      let token = body.token;
-      ok(server.activeTokens.delete(token));
-      print("after destroy have", server.activeTokens.size, "tokens left.")
-      response.setStatusLine("1.1", 200, "OK");
-      response.write('{}');
-    },
-  }
-  server = new Server(handlers);
-  server.numTokenFetches = 0;
-  server.activeTokens = new Set();
-  return server;
-}
-
-function promiseObserver(topic) {
-  return new Promise(resolve => {
-    function observe(subject, topic, data) {
-      Services.obs.removeObserver(observe, topic);
-      resolve(data);
-    }
-    Services.obs.addObserver(observe, topic, false);
-  });
-}
-
-// The tests.
-function run_test() {
-  run_next_test();
-}
-
-// Arrange for the first token we hand out to be rejected - the client should
-// notice the 401 and silently get a new token and retry the request.
-add_task(function testAuthRetry() {
-  let handlers = {
-    "/v1/batch": (request, response) => {
-      // We know the first token we will get is "token0", so we simulate that
-      // "expiring" by only accepting "token1". Then we just echo the response
-      // back.
-      let authHeader;
-      try {
-        authHeader = request.getHeader("Authorization");
-      } catch (ex) {}
-      if (authHeader != "Bearer token1") {
-        response.setStatusLine("1.1", 401, "Unauthorized");
-        response.write("wrong token");
-        return;
-      }
-      response.setStatusLine("1.1", 200, "OK");
-      response.write(JSON.stringify({ok: true}));
-    }
-  };
-  let rlserver = new Server(handlers);
-  rlserver.start();
-  let authServer = OAuthTokenServer();
-  authServer.start();
-  try {
-    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
-    Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", authServer.host + "/v1");
-
-    let fxa = yield createMockFxA();
-    let sc = new ServerClient(fxa);
-
-    let response = yield sc.request({
-      path: "/batch",
-      method: "post",
-      body: {foo: "bar"},
-    });
-    equal(response.status, 200, "got the 200 we expected");
-    equal(authServer.numTokenFetches, 2, "took 2 tokens to get the 200")
-    deepEqual(response.body, {ok: true});
-  } finally {
-    yield authServer.stop();
-    yield rlserver.stop();
-  }
-});
-
-// Check that specified headers are seen by the server, and that server headers
-// in the response are seen by the client.
-add_task(function testHeaders() {
-  let handlers = {
-    "/v1/batch": (request, response) => {
-      ok(request.hasHeader("x-foo"), "got our foo header");
-      equal(request.getHeader("x-foo"), "bar", "foo header has the correct value");
-      response.setHeader("Server-Sent-Header", "hello");
-      response.setStatusLine("1.1", 200, "OK");
-      response.write("{}");
-    }
-  };
-  let rlserver = new Server(handlers);
-  rlserver.start();
-  try {
-    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
-
-    let fxa = yield createMockFxA();
-    let sc = new ServerClient(fxa);
-    sc._getToken = () => Promise.resolve();
-
-    let response = yield sc.request({
-      path: "/batch",
-      method: "post",
-      headers: {"X-Foo": "bar"},
-      body: {foo: "bar"}});
-    equal(response.status, 200, "got the 200 we expected");
-    equal(response.headers["server-sent-header"], "hello", "got the server header");
-  } finally {
-    yield rlserver.stop();
-  }
-});
-
-// Check that a "backoff" header causes the correct notification.
-add_task(function testBackoffHeader() {
-  let handlers = {
-    "/v1/batch": (request, response) => {
-      response.setHeader("Backoff", "123");
-      response.setStatusLine("1.1", 200, "OK");
-      response.write("{}");
-    }
-  };
-  let rlserver = new Server(handlers);
-  rlserver.start();
-
-  let observerPromise = promiseObserver("readinglist:backoff-requested");
-  try {
-    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
-
-    let fxa = yield createMockFxA();
-    let sc = new ServerClient(fxa);
-    sc._getToken = () => Promise.resolve();
-
-    let response = yield sc.request({
-      path: "/batch",
-      method: "post",
-      headers: {"X-Foo": "bar"},
-      body: {foo: "bar"}});
-    equal(response.status, 200, "got the 200 we expected");
-    let data = yield observerPromise;
-    equal(data, "123", "got the expected header value.")
-  } finally {
-    yield rlserver.stop();
-  }
-});
-
-// Check that a "backoff" header causes the correct notification.
-add_task(function testRetryAfterHeader() {
-  let handlers = {
-    "/v1/batch": (request, response) => {
-      response.setHeader("Retry-After", "456");
-      response.setStatusLine("1.1", 500, "Not OK");
-      response.write("{}");
-    }
-  };
-  let rlserver = new Server(handlers);
-  rlserver.start();
-
-  let observerPromise = promiseObserver("readinglist:backoff-requested");
-  try {
-    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
-
-    let fxa = yield createMockFxA();
-    let sc = new ServerClient(fxa);
-    sc._getToken = () => Promise.resolve();
-
-    let response = yield sc.request({
-      path: "/batch",
-      method: "post",
-      headers: {"X-Foo": "bar"},
-      body: {foo: "bar"}});
-    equal(response.status, 500, "got the 500 we expected");
-    let data = yield observerPromise;
-    equal(data, "456", "got the expected header value.")
-  } finally {
-    yield rlserver.stop();
-  }
-});
-
-// Check that unicode ends up as utf-8 in requests, and vice-versa in responses.
-// (Note the ServerClient assumes all strings in and out are UCS, and thus have
-// already been encoded/decoded (ie, it never expects to receive stuff already
-// utf-8 encoded, and never returns utf-8 encoded responses.)
-add_task(function testUTF8() {
-  let handlers = {
-    "/v1/hello": (request, response) => {
-      // Get the body as bytes.
-      let sis = Cc["@mozilla.org/scriptableinputstream;1"]
-                .createInstance(Ci.nsIScriptableInputStream);
-      sis.init(request.bodyInputStream);
-      let body = sis.read(sis.available());
-      sis.close();
-      // The client sent "{"copyright: "\xa9"} where \xa9 is the copyright symbol.
-      // It should have been encoded as utf-8 which is \xc2\xa9
-      equal(body, '{"copyright":"\xc2\xa9"}', "server saw utf-8 encoded data");
-      // and just write it back unchanged.
-      response.setStatusLine("1.1", 200, "OK");
-      response.write(body);
-    }
-  };
-  let rlserver = new Server(handlers);
-  rlserver.start();
-  try {
-    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
-
-    let fxa = yield createMockFxA();
-    let sc = new ServerClient(fxa);
-    sc._getToken = () => Promise.resolve();
-
-    let body = {copyright: "\xa9"}; // see above - \xa9 is the copyright symbol
-    let response = yield sc.request({
-      path: "/hello",
-      method: "post",
-      body: body
-    });
-    equal(response.status, 200, "got the 200 we expected");
-    deepEqual(response.body, body);
-  } finally {
-    yield rlserver.stop();
-  }
-});
deleted file mode 100644
--- a/browser/components/readinglist/test/xpcshell/test_Sync.js
+++ /dev/null