Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 27 Mar 2015 11:17:04 -0400
changeset 266423 44e454b5e93b64cdb77a025c5d6b8d8ca5c2926e
parent 266422 aef75ff8a91166c258a166384275ca8697707fd5 (current diff)
parent 266275 94c247bc2480052cd0d27238f77279a0dcc5f265 (diff)
child 266424 3c34fd480729e3b6684fba747ff61078f672ce16
child 266467 16c68807669eb4c3ddd2ef01268bb4e7313ad418
child 266496 bcc307ea64f495daebc40b65cb3d070ccf65205d
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound. a=merge CLOSED TREE
browser/app/profile/firefox.js
dom/base/ScriptSettings.cpp
mobile/android/base/tests/robocop_login.html
toolkit/components/places/tests/unit/test_398914.js
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,25 +10,25 @@
   <!--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="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="5a63e2b9f3ef85e82a33440cb73c55dff4e9bf78"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f5de61a5d8fdaa2db3d4e17e0c4212ec4d54a365"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="83760d213fb3bec7b4117d266fcfbf6fe2ba14ab"/>
   <project name="device/common" path="device/common" revision="6a2995683de147791e516aae2ccb31fdfbe2ad30"/>
@@ -110,20 +110,20 @@
   <project name="platform/libcore" path="libcore" revision="e195beab082c09217318fc19250caeaf4c1bd800"/>
   <project name="platform/libnativehelper" path="libnativehelper" revision="feeb36c2bd4adfe285f98f5de92e0f3771b2c115"/>
   <project name="platform/ndk" path="ndk" revision="e58ef003be4306bb53a8c11331146f39e4eab31f"/>
   <project name="platform_prebuilts_misc" path="prebuilts/misc" remote="b2g" revision="0e7c060db684b409616fe67ea433ef19f5634c60"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="c792f0bd9fff7aea2887c60bbb3a9bbdb534ffa3"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="f7d9bf71cf6693474f3f2a81a4ba62c0fc5646aa"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="cfcef469537869947abb9aa1d656774cc2678d4c"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="5a48c04c4bb5f079bc757e29864a42427378e051"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="e0fc03e0a3062063c3c85996dcc881c0a49ed98d"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="48d2332e6d8400cdc0de273ceff2abe8aaababf8"/>
   <project name="platform/system/extras" path="system/extras" revision="10e78a05252b3de785f88c2d0b9ea8a428009c50"/>
   <project name="platform/system/media" path="system/media" revision="7ff72c2ea2496fa50b5e8a915e56e901c3ccd240"/>
-  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="8f7d94ac711af4678169805137c6c42def39b3ed"/>
+  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="3c5405863d2002f665ef2b901abb3853c420129b"/>
   <project name="platform/system/netd" path="system/netd" revision="3ae56364946d4a5bf5a5f83f12f9a45a30398e33"/>
   <project name="platform/system/security" path="system/security" revision="ee8068b9e7bfb2770635062fc9c2035be2142bd8"/>
   <project name="platform/system/vold" path="system/vold" revision="bb33b1ce8ad9cd3fc4311801b4d56db1d5c8175b"/>
   <!--original fetch url was http://sprdsource.spreadtrum.com:8085/b2g/android-->
   <remote fetch="https://git.mozilla.org/external/sprd-aosp" name="sprd-aosp"/>
   <default remote="sprd-aosp" revision="sprdb2g_gonk4.4" sync-j="4"/>
   <!-- Stock Android things -->
   <project name="platform/external/icu4c" path="external/icu4c" revision="2bb01561780583cc37bc667f0ea79f48a122d8a2"/>
--- 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="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <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="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="527d1c939ee57deb7192166e56e2a3fffa8cb087"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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,20 +12,20 @@
   <!--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="4efd19d199ae52656604f794c5a77518400220fd">
     <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="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5a63e2b9f3ef85e82a33440cb73c55dff4e9bf78"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f5de61a5d8fdaa2db3d4e17e0c4212ec4d54a365"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
@@ -112,20 +112,20 @@
   <project name="platform/libnativehelper" path="libnativehelper" revision="4792069e90385889b0638e97ae62c67cdf274e22"/>
   <project name="platform/ndk" path="ndk" revision="7666b97bbaf1d645cdd6b4430a367b7a2bb53369"/>
   <project name="platform/prebuilts/misc" path="prebuilts/misc" revision="f6ab40b3257abc07741188fd173ac392575cc8d2"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="e52099755d0bd3a579130eefe8e58066cc6c0cb6"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="02c32feb2fe97037be0ac4dace3a6a5025ac895d"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="842e33e43a55ea44833b9e23e4d180fa17c843af"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="5db24726f0f42124304195a6bdea129039eeeaeb"/>
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="930ae098543881f47eac054677726ee4b998b2f8"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="e0fc03e0a3062063c3c85996dcc881c0a49ed98d"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="48d2332e6d8400cdc0de273ceff2abe8aaababf8"/>
   <project name="platform_system_core" path="system/core" remote="b2g" revision="542d1f59dc331b472307e5bd043101d14d5a3a3e"/>
   <project name="platform/system/extras" path="system/extras" revision="18c1180e848e7ab8691940481f5c1c8d22c37b3e"/>
-  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="8f7d94ac711af4678169805137c6c42def39b3ed"/>
+  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="3c5405863d2002f665ef2b901abb3853c420129b"/>
   <project name="platform/system/media" path="system/media" revision="d90b836f66bf1d9627886c96f3a2d9c3007fbb80"/>
   <project name="platform/system/netd" path="system/netd" revision="56112dd7b811301b718d0643a82fd5cac9522073"/>
   <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/>
   <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/>
   <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/>
   <!-- Emulator specific things -->
   <project name="android-development" path="development" remote="b2g" revision="dab55669da8f48b6e57df95d5af9f16b4a87b0b1"/>
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,25 +10,25 @@
   <!--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="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="5a63e2b9f3ef85e82a33440cb73c55dff4e9bf78"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f5de61a5d8fdaa2db3d4e17e0c4212ec4d54a365"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="1950e4760fa14688b83cdbb5acaa1af9f82ef434"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="ac6eb97a37035c09fb5ede0852f0881e9aadf9ad"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="737f591c5f95477148d26602c7be56cbea0cdeb9"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="51da9b1981be481b92a59a826d4d78dc73d0989a"/>
   <project name="device/common" path="device/common" revision="798a3664597e6041985feab9aef42e98d458bc3d"/>
@@ -110,20 +110,20 @@
   <project name="platform/libcore" path="libcore" revision="9877ade9617bb0db6e59aa2a54719a9bc92600f3"/>
   <project name="platform/libnativehelper" path="libnativehelper" revision="46c96ace65eb1ccab05bf15b9bf8e53e443039af"/>
   <project name="platform/ndk" path="ndk" revision="cb5519af32ae7b4a9c334913a612462ecd04c5d0"/>
   <project name="platform_prebuilts_misc" path="prebuilts/misc" remote="b2g" revision="0e7c060db684b409616fe67ea433ef19f5634c60"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="6aa61f8557a22039a30b42b7f283996381fd625d"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="f7d9bf71cf6693474f3f2a81a4ba62c0fc5646aa"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="b562b01c93de9578d5db537b6a602a38e1aaa0ce"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="387f03e815f57d536dd922706db1622bddba8d81"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="e0fc03e0a3062063c3c85996dcc881c0a49ed98d"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="48d2332e6d8400cdc0de273ceff2abe8aaababf8"/>
   <project name="platform/system/extras" path="system/extras" revision="5356165f67f4a81c2ef28671c13697f1657590df"/>
   <project name="platform/system/media" path="system/media" revision="be0e2fe59a8043fa5200f75697df9220a99abe9d"/>
-  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="8f7d94ac711af4678169805137c6c42def39b3ed"/>
+  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="3c5405863d2002f665ef2b901abb3853c420129b"/>
   <project name="platform/system/netd" path="system/netd" revision="36704b0da24debcab8090156568ac236315036bb"/>
   <project name="platform/system/security" path="system/security" revision="583374f69f531ba68fc3dcbff1f74893d2a96406"/>
   <project name="platform/system/vold" path="system/vold" revision="d4455b8cf361f8353e8aebac15ffd64b4aedd2b9"/>
   <project name="platform/external/icu4c" path="external/icu4c" remote="aosp" revision="b4c6379528887dc25ca9991a535a8d92a61ad6b6"/>
   <project name="platform_frameworks_av" path="frameworks/av" remote="b2g" revision="f3cedd7fd9b1649aa5107d466be9078bb7602af6"/>
   <project name="platform_system_core" path="system/core" remote="b2g" revision="9395eb5aa885cf6d305a202de6e9694a58a89717"/>
   <default remote="caf" revision="refs/tags/android-4.4.2_r1" sync-j="4"/>
   <!-- Emulator specific things -->
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,25 +10,25 @@
   <!--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="52775e03a2d8532429dff579cb2cd56718e488c3">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="5a63e2b9f3ef85e82a33440cb73c55dff4e9bf78"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f5de61a5d8fdaa2db3d4e17e0c4212ec4d54a365"/>
   <!-- Stock Android things -->
   <project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="50d1ca4ab8add54523b7bc692860d57e8ee4c0d1"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" path="prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" revision="fb3845864573857677f9b500040a8f011eaf5078"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" revision="354496e8eddd28c743d8e02c02eeab02958367e6"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" revision="b37c91354272b7413a0dc058b7445e677921d39e"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" revision="a227c92e0170bcf2296a63386956946b0dd78ca7"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" revision="884626610186b6dbea52cec5194b1c4bcfe1cb98"/>
   <project groups="pdk,linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" path="prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" revision="29f9b82faa1af9730f52e933dca848546cbea84c"/>
@@ -122,19 +122,19 @@
   <project name="platform/libcore" path="libcore" revision="1a07f00d8163f497a785a3285ec55fe551ba95c1"/>
   <project name="platform/libnativehelper" path="libnativehelper" revision="4834b58ed7af3ee69523177e00e55603ac90ed50"/>
   <project name="platform/ndk" path="ndk" revision="869c05cab7b4315c2bc607493db3f5b18ead2580"/>
   <project name="platform_prebuilts_misc" path="prebuilts/misc" remote="b2g" revision="da8b660db117e2a69a7624bfdca2f02cad397f2e"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="5fd638a4a0ff3677fc3c970fab038d6db1bb7665"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="2c0d193349c55337e37196a7f2d5cef37753ed3e"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="a982f43b7f2d5916dc3a859667a8ba78e50b6202"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="6e18b61ee446bdd9880c07ae84197a087490c2e5"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="e0fc03e0a3062063c3c85996dcc881c0a49ed98d"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="48d2332e6d8400cdc0de273ceff2abe8aaababf8"/>
   <project name="platform/system/extras" path="system/extras" revision="18f7c51415917eb0e21b30f220db7bd0be4130a7"/>
-  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="8f7d94ac711af4678169805137c6c42def39b3ed"/>
+  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="3c5405863d2002f665ef2b901abb3853c420129b"/>
   <project name="platform/system/media" path="system/media" revision="adf8fbacf7395858884690df5e3ce46bc75fa683"/>
   <project name="platform/system/netd" path="system/netd" revision="655392625db084a7122d65a15acf74db7f1da7f7"/>
   <project name="platform/system/security" path="system/security" revision="e6b3fdd892ad994ec3fd0b8959d630e31881801b"/>
   <project name="platform/system/vold" path="system/vold" revision="eb59d2afd5f6e1cbab2ef985a8dd1c7105b499e8"/>
   <project name="platform_frameworks_av" path="frameworks/av" remote="b2g" revision="ea531874885eed7f68802048218ed86dde927f58"/>
   <project name="platform_frameworks_base" path="frameworks/base" remote="b2g" revision="df7e0cfbbc7e954ed26c73ac17832a5ff035f046"/>
   <project name="platform_frameworks_wilhelm" path="frameworks/wilhelm" remote="b2g" revision="73f7e7f12c8c5459f7a39e2fa343f083c942864d"/>
   <project name="platform_system_core" path="system/core" remote="b2g" revision="4df51d9abf6cc9a6ec49b965e621699e0e6dc4fb"/>
new file mode 100644
--- /dev/null
+++ b/b2g/config/emulator-x86-kk/config.json
@@ -0,0 +1,32 @@
+{
+    "config_version": 2,
+    "tooltool_manifest": "releng-emulator-kk.tt",
+    "mock_target": "mozilla-centos6-x86_64",
+    "mock_packages": ["ccache", "make", "bison", "flex", "gcc", "g++", "mpfr", "zlib-devel", "ncurses-devel", "zip", "autoconf213", "glibc-static", "perl-Digest-SHA", "wget", "alsa-lib", "atk", "cairo", "dbus-glib", "fontconfig", "freetype", "glib2", "gtk2", "libXRender", "libXt", "pango", "mozilla-python27-mercurial", "openssh-clients", "nss-devel", "glibc-devel.i686", "libstdc++.i686", "zlib-devel.i686", "ncurses-devel.i686", "libX11-devel.i686", "mesa-libGL-devel.i686", "mesa-libGL-devel", "libX11-devel", "git", "libxml2"],
+    "mock_files": [["/home/cltbld/.ssh", "/home/mock_mozilla/.ssh"]],
+    "build_targets": ["droid", "package-emulator", "package-tests"],
+    "upload_files": [
+        "{workdir}/out/target/product/generic/*.tar.bz2",
+        "{workdir}/out/target/product/generic/tests/*.zip",
+        "{workdir}/out/emulator.tar.gz",
+        "{objdir}/dist/b2g-*.crashreporter-symbols.zip",
+        "{workdir}/sources.xml"
+    ],
+    "public_upload_files": [
+        "{workdir}/out/target/product/generic/*.tar.bz2",
+        "{workdir}/out/target/product/generic/tests/*.zip",
+        "{objdir}/dist/b2g-*.crashreporter-symbols.zip",
+        "{objdir}/dist/b2g-*.tar.gz",
+        "{workdir}/sources.xml"
+    ],
+    "upload_platform": "emulator-kk",
+    "gecko_l10n_root": "https://hg.mozilla.org/l10n-central",
+    "gaia": {
+        "l10n": {
+            "vcs": "hgtool",
+            "root": "https://hg.mozilla.org/gaia-l10n"
+        }
+    },
+    "b2g_manifest": "emulator-kk.xml",
+    "b2g_manifest_intree": true
+}
new file mode 100644
--- /dev/null
+++ b/b2g/config/emulator-x86-kk/releng-emulator-kk.tt
@@ -0,0 +1,9 @@
+[
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
+}
+]
new file mode 120000
--- /dev/null
+++ b/b2g/config/emulator-x86-kk/sources.xml
@@ -0,0 +1,1 @@
+../emulator-kk/sources.xml
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/b2g/config/emulator-x86-l/config.json
@@ -0,0 +1,32 @@
+{
+    "config_version": 2,
+    "tooltool_manifest": "releng-emulator-l.tt",
+    "mock_target": "mozilla-centos6-x86_64",
+    "mock_packages": ["ccache", "make", "bison", "flex", "gcc", "g++", "mpfr", "zlib-devel", "ncurses-devel", "zip", "autoconf213", "glibc-static", "perl-Digest-SHA", "wget", "alsa-lib", "atk", "cairo", "dbus-glib", "fontconfig", "freetype", "glib2", "gtk2", "libXRender", "libXt", "pango", "mozilla-python27-mercurial", "openssh-clients", "nss-devel", "glibc-devel.i686", "libstdc++.i686", "zlib-devel.i686", "ncurses-devel.i686", "libX11-devel.i686", "mesa-libGL-devel.i686", "mesa-libGL-devel", "libX11-devel", "git", "libxml2"],
+    "mock_files": [["/home/cltbld/.ssh", "/home/mock_mozilla/.ssh"]],
+    "build_targets": ["droid", "package-emulator", "package-tests"],
+    "upload_files": [
+        "{workdir}/out/target/product/generic/*.tar.bz2",
+        "{workdir}/out/target/product/generic/tests/*.zip",
+        "{workdir}/out/emulator.tar.gz",
+        "{objdir}/dist/b2g-*.crashreporter-symbols.zip",
+        "{workdir}/sources.xml"
+    ],
+    "public_upload_files": [
+        "{workdir}/out/target/product/generic/*.tar.bz2",
+        "{workdir}/out/target/product/generic/tests/*.zip",
+        "{objdir}/dist/b2g-*.crashreporter-symbols.zip",
+        "{objdir}/dist/b2g-*.tar.gz",
+        "{workdir}/sources.xml"
+    ],
+    "upload_platform": "emulator-l",
+    "gecko_l10n_root": "https://hg.mozilla.org/l10n-central",
+    "gaia": {
+        "l10n": {
+            "vcs": "hgtool",
+            "root": "https://hg.mozilla.org/gaia-l10n"
+        }
+    },
+    "b2g_manifest": "emulator-l.xml",
+    "b2g_manifest_intree": true
+}
new file mode 100644
--- /dev/null
+++ b/b2g/config/emulator-x86-l/releng-emulator-l.tt
@@ -0,0 +1,9 @@
+[
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
+}
+]
new file mode 120000
--- /dev/null
+++ b/b2g/config/emulator-x86-l/sources.xml
@@ -0,0 +1,1 @@
+../emulator-l/sources.xml
\ No newline at end of file
--- 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="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <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="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="527d1c939ee57deb7192166e56e2a3fffa8cb087"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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,25 +10,25 @@
   <!--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="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="5a63e2b9f3ef85e82a33440cb73c55dff4e9bf78"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f5de61a5d8fdaa2db3d4e17e0c4212ec4d54a365"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="a32003194f707f66a2d8cdb913ed1869f1926c5d"/>
   <project name="device/common" path="device/common" revision="96d4d2006c4fcb2f19a3fa47ab10cb409faa017b"/>
@@ -104,19 +104,19 @@
   <project name="platform/libcore" path="libcore" revision="baf7d8068dd501cfa338d3a8b1b87216d6ce0571"/>
   <project name="platform/libnativehelper" path="libnativehelper" revision="50c4430e32849530ced32680fd6ee98963b3f7ac"/>
   <project name="platform/ndk" path="ndk" revision="e58ef003be4306bb53a8c11331146f39e4eab31f"/>
   <project name="platform_prebuilts_misc" path="prebuilts/misc" remote="b2g" revision="0e7c060db684b409616fe67ea433ef19f5634c60"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="c792f0bd9fff7aea2887c60bbb3a9bbdb534ffa3"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="f7d9bf71cf6693474f3f2a81a4ba62c0fc5646aa"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="69d524e80cdf3981006627c65ac85f3a871238a3"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="5a48c04c4bb5f079bc757e29864a42427378e051"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="e0fc03e0a3062063c3c85996dcc881c0a49ed98d"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="48d2332e6d8400cdc0de273ceff2abe8aaababf8"/>
   <project name="platform/system/extras" path="system/extras" revision="576f57b6510de59c08568b53c0fb60588be8689e"/>
-  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="8f7d94ac711af4678169805137c6c42def39b3ed"/>
+  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="3c5405863d2002f665ef2b901abb3853c420129b"/>
   <project name="platform/system/netd" path="system/netd" revision="a6531f7befb49b1c81bc0de7e51c5482b308e1c5"/>
   <project name="platform/system/security" path="system/security" revision="ee8068b9e7bfb2770635062fc9c2035be2142bd8"/>
   <project name="platform/system/vold" path="system/vold" revision="42fa2a0f14f965970a4b629a176bbd2666edf017"/>
   <project name="platform/external/curl" path="external/curl" revision="e68addd988448959ea8157c5de637346b4180c33"/>
   <project name="platform/external/icu4c" path="external/icu4c" revision="d3ec7428eb276db43b7ed0544e09344a6014806c"/>
   <project name="platform/hardware/libhardware_legacy" path="hardware/libhardware_legacy" revision="76c4bf4bc430a1b8317f2f21ef735867733e50cc"/>
   <project name="platform/system/media" path="system/media" revision="c1332c21c608f4932a6d7e83450411cde53315ef"/>
   <!--original fetch url was git://github.com/t2m-foxfone/-->
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,20 +12,20 @@
   <!--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="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5a63e2b9f3ef85e82a33440cb73c55dff4e9bf78"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f5de61a5d8fdaa2db3d4e17e0c4212ec4d54a365"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "249b8c08c1d57961ef6c905f3498fa62b032bf24", 
+        "git_revision": "9cc496cecc37d7a29f9279827cdf6e4891211f67", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "87ffbce1342f44bdeee96c7d08d41ac630254eb3", 
+    "revision": "9e79307fd6bcade07847b92d42948a6a6a334f79", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,20 +12,20 @@
   <!--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="4efd19d199ae52656604f794c5a77518400220fd">
     <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="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5a63e2b9f3ef85e82a33440cb73c55dff4e9bf78"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f5de61a5d8fdaa2db3d4e17e0c4212ec4d54a365"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
@@ -112,20 +112,20 @@
   <project name="platform/libnativehelper" path="libnativehelper" revision="4792069e90385889b0638e97ae62c67cdf274e22"/>
   <project name="platform/ndk" path="ndk" revision="7666b97bbaf1d645cdd6b4430a367b7a2bb53369"/>
   <project name="platform/prebuilts/misc" path="prebuilts/misc" revision="f6ab40b3257abc07741188fd173ac392575cc8d2"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="e52099755d0bd3a579130eefe8e58066cc6c0cb6"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="02c32feb2fe97037be0ac4dace3a6a5025ac895d"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="842e33e43a55ea44833b9e23e4d180fa17c843af"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="5db24726f0f42124304195a6bdea129039eeeaeb"/>
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="930ae098543881f47eac054677726ee4b998b2f8"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="e0fc03e0a3062063c3c85996dcc881c0a49ed98d"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="48d2332e6d8400cdc0de273ceff2abe8aaababf8"/>
   <project name="platform_system_core" path="system/core" remote="b2g" revision="542d1f59dc331b472307e5bd043101d14d5a3a3e"/>
   <project name="platform/system/extras" path="system/extras" revision="18c1180e848e7ab8691940481f5c1c8d22c37b3e"/>
-  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="8f7d94ac711af4678169805137c6c42def39b3ed"/>
+  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="3c5405863d2002f665ef2b901abb3853c420129b"/>
   <project name="platform/system/media" path="system/media" revision="d90b836f66bf1d9627886c96f3a2d9c3007fbb80"/>
   <project name="platform/system/netd" path="system/netd" revision="56112dd7b811301b718d0643a82fd5cac9522073"/>
   <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/>
   <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/>
   <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/>
   <!-- Nexus 4 specific things -->
   <project name="device-mako" path="device/lge/mako" remote="b2g" revision="78d17f0c117f0c66dd55ee8d5c5dde8ccc93ecba"/>
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,25 +10,25 @@
   <!--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="52775e03a2d8532429dff579cb2cd56718e488c3">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="249b8c08c1d57961ef6c905f3498fa62b032bf24"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="9cc496cecc37d7a29f9279827cdf6e4891211f67"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="5a63e2b9f3ef85e82a33440cb73c55dff4e9bf78"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="f5de61a5d8fdaa2db3d4e17e0c4212ec4d54a365"/>
   <!-- Stock Android things -->
   <project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="50d1ca4ab8add54523b7bc692860d57e8ee4c0d1"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" path="prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" revision="fb3845864573857677f9b500040a8f011eaf5078"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" revision="354496e8eddd28c743d8e02c02eeab02958367e6"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" revision="b37c91354272b7413a0dc058b7445e677921d39e"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" revision="a227c92e0170bcf2296a63386956946b0dd78ca7"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" revision="884626610186b6dbea52cec5194b1c4bcfe1cb98"/>
   <project groups="pdk,linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" path="prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" revision="29f9b82faa1af9730f52e933dca848546cbea84c"/>
@@ -122,19 +122,19 @@
   <project name="platform/libcore" path="libcore" revision="1a07f00d8163f497a785a3285ec55fe551ba95c1"/>
   <project name="platform/libnativehelper" path="libnativehelper" revision="4834b58ed7af3ee69523177e00e55603ac90ed50"/>
   <project name="platform/ndk" path="ndk" revision="869c05cab7b4315c2bc607493db3f5b18ead2580"/>
   <project name="platform_prebuilts_misc" path="prebuilts/misc" remote="b2g" revision="da8b660db117e2a69a7624bfdca2f02cad397f2e"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="5fd638a4a0ff3677fc3c970fab038d6db1bb7665"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="2c0d193349c55337e37196a7f2d5cef37753ed3e"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="a982f43b7f2d5916dc3a859667a8ba78e50b6202"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="6e18b61ee446bdd9880c07ae84197a087490c2e5"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="e0fc03e0a3062063c3c85996dcc881c0a49ed98d"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="48d2332e6d8400cdc0de273ceff2abe8aaababf8"/>
   <project name="platform/system/extras" path="system/extras" revision="18f7c51415917eb0e21b30f220db7bd0be4130a7"/>
-  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="8f7d94ac711af4678169805137c6c42def39b3ed"/>
+  <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="3c5405863d2002f665ef2b901abb3853c420129b"/>
   <project name="platform/system/media" path="system/media" revision="adf8fbacf7395858884690df5e3ce46bc75fa683"/>
   <project name="platform/system/netd" path="system/netd" revision="655392625db084a7122d65a15acf74db7f1da7f7"/>
   <project name="platform/system/security" path="system/security" revision="e6b3fdd892ad994ec3fd0b8959d630e31881801b"/>
   <project name="platform/system/vold" path="system/vold" revision="eb59d2afd5f6e1cbab2ef985a8dd1c7105b499e8"/>
   <project name="platform_frameworks_av" path="frameworks/av" remote="b2g" revision="ea531874885eed7f68802048218ed86dde927f58"/>
   <project name="platform_frameworks_base" path="frameworks/base" remote="b2g" revision="df7e0cfbbc7e954ed26c73ac17832a5ff035f046"/>
   <project name="platform_frameworks_wilhelm" path="frameworks/wilhelm" remote="b2g" revision="73f7e7f12c8c5459f7a39e2fa343f083c942864d"/>
   <project name="platform_system_core" path="system/core" remote="b2g" revision="4df51d9abf6cc9a6ec49b965e621699e0e6dc4fb"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1866,19 +1866,14 @@ 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
 
-#ifndef NIGHTLY_BUILD
-// Disable reader mode by default.
-pref("reader.parse-on-load.enabled", false);
-#endif
-
 // Enable ReadingList browser UI by default.
 pref("browser.readinglist.enabled", true);
 pref("browser.readinglist.sidebarEverOpened", false);
 // Enable the readinglist engine by default.
 pref("readinglist.scheduler.enabled", true);
 pref("readinglist.server", "https://readinglist.services.mozilla.com/v1");
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2095,63 +2095,91 @@ function loadURI(uri, referrer, postData
     openLinkIn(uri, "current",
                { referrerURI: referrer,
                  referrerPolicy: referrerPolicy,
                  postData: postData,
                  allowThirdPartyFixup: allowThirdPartyFixup });
   } catch (e) {}
 }
 
-function getShortcutOrURIAndPostData(aURL, aCallback) {
-  let mayInheritPrincipal = false;
-  let postData = null;
-  let shortcutURL = null;
-  let keyword = aURL;
-  let param = "";
-
-  // XXX Bug 1100294 will remove this little hack by using an async version of
-  // PlacesUtils.getURLAndPostDataForKeyword(). For now we simulate an async
-  // execution with at least a setTimeout(fn, 0).
-  let originalCallback = aCallback;
-  aCallback = data => setTimeout(() => originalCallback(data));
-
-  let offset = aURL.indexOf(" ");
-  if (offset > 0) {
-    keyword = aURL.substr(0, offset);
-    param = aURL.substr(offset + 1);
-  }
-
-  let engine = Services.search.getEngineByAlias(keyword);
-  if (engine) {
-    let submission = engine.getSubmission(param, null, "keyword");
-    postData = submission.postData;
-    aCallback({ postData: submission.postData, url: submission.uri.spec,
-                mayInheritPrincipal: mayInheritPrincipal });
-    return;
-  }
-
-  [shortcutURL, postData] =
-    PlacesUtils.getURLAndPostDataForKeyword(keyword);
-
-  if (!shortcutURL) {
-    aCallback({ postData: postData, url: aURL,
-                mayInheritPrincipal: mayInheritPrincipal });
-    return;
-  }
-
-  let escapedPostData = "";
-  if (postData)
-    escapedPostData = unescape(postData);
-
-  if (/%s/i.test(shortcutURL) || /%s/i.test(escapedPostData)) {
-    let charset = "";
-    const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/;
-    let matches = shortcutURL.match(re);
-
-    let continueOperation = function () {
+/**
+ * Given a urlbar value, discerns between URIs, keywords and aliases.
+ *
+ * @param url
+ *        The urlbar value.
+ * @param callback (optional, deprecated)
+ *        The callback function invoked when done. This parameter is
+ *        deprecated, please use the Promise that is returned.
+ *
+ * @return Promise<{ postData, url, mayInheritPrincipal }>
+ */
+function getShortcutOrURIAndPostData(url, callback = null) {
+  if (callback) {
+    Deprecated.warning("Please use the Promise returned by " +
+                       "getShortcutOrURIAndPostData() instead of passing a " +
+                       "callback",
+                       "https://bugzilla.mozilla.org/show_bug.cgi?id=1100294");
+  }
+
+  return Task.spawn(function* () {
+    let mayInheritPrincipal = false;
+    let postData = null;
+    let shortcutURL = null;
+    let keyword = url;
+    let param = "";
+
+    let offset = url.indexOf(" ");
+    if (offset > 0) {
+      keyword = url.substr(0, offset);
+      param = url.substr(offset + 1);
+    }
+
+    let engine = Services.search.getEngineByAlias(keyword);
+    if (engine) {
+      let submission = engine.getSubmission(param, null, "keyword");
+      postData = submission.postData;
+      return { postData: submission.postData, url: submission.uri.spec,
+               mayInheritPrincipal };
+    }
+
+    let entry = yield PlacesUtils.keywords.fetch(keyword);
+    if (entry) {
+      shortcutURL = entry.url.href;
+      postData = entry.postData;
+    }
+
+    if (!shortcutURL) {
+      return { postData, url, mayInheritPrincipal };
+    }
+
+    let escapedPostData = "";
+    if (postData)
+      escapedPostData = unescape(postData);
+
+    if (/%s/i.test(shortcutURL) || /%s/i.test(escapedPostData)) {
+      let charset = "";
+      const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/;
+      let matches = shortcutURL.match(re);
+
+      if (matches) {
+        [, shortcutURL, charset] = matches;
+      } else {
+        let uri;
+        try {
+          // makeURI() throws if URI is invalid.
+          uri = makeURI(shortcutURL);
+        } catch (ex) {}
+
+        if (uri) {
+          // Try to get the saved character-set.
+          // Will return an empty string if character-set is not found.
+          charset = yield PlacesUtils.getCharsetForURI(uri);
+        }
+      }
+
       // encodeURIComponent produces UTF-8, and cannot be used for other charsets.
       // escape() works in those cases, but it doesn't uri-encode +, @, and /.
       // Therefore we need to manually replace these ASCII characters by their
       // encodeURIComponent result, to match the behavior of nsEscape() with
       // url_XPAlphas
       let encodedParam = "";
       if (charset && charset != "UTF-8")
         encodedParam = escape(convertFromUnicode(charset, param)).
@@ -2164,50 +2192,39 @@ function getShortcutOrURIAndPostData(aUR
       if (/%s/i.test(escapedPostData)) // POST keyword
         postData = getPostDataStream(escapedPostData, param, encodedParam,
                                                "application/x-www-form-urlencoded");
 
       // This URL came from a bookmark, so it's safe to let it inherit the current
       // document's principal.
       mayInheritPrincipal = true;
 
-      aCallback({ postData: postData, url: shortcutURL,
-                  mayInheritPrincipal: mayInheritPrincipal });
-    }
-
-    if (matches) {
-      [, shortcutURL, charset] = matches;
-      continueOperation();
-    } else {
-      // Try to get the saved character-set.
-      // makeURI throws if URI is invalid.
-      // Will return an empty string if character-set is not found.
-      try {
-        PlacesUtils.getCharsetForURI(makeURI(shortcutURL))
-                   .then(c => { charset = c; continueOperation(); });
-      } catch (ex) {
-        continueOperation();
-      }
-    }
-  }
-  else if (param) {
-    // This keyword doesn't take a parameter, but one was provided. Just return
-    // the original URL.
-    postData = null;
-
-    aCallback({ postData: postData, url: aURL,
-                mayInheritPrincipal: mayInheritPrincipal });
-  } else {
+      return { postData, url: shortcutURL, mayInheritPrincipal };
+    }
+
+    if (param) {
+      // This keyword doesn't take a parameter, but one was provided. Just return
+      // the original URL.
+      postData = null;
+
+      return { postData, url, mayInheritPrincipal };
+    }
+
     // This URL came from a bookmark, so it's safe to let it inherit the current
     // document's principal.
     mayInheritPrincipal = true;
 
-    aCallback({ postData: postData, url: shortcutURL,
-                mayInheritPrincipal: mayInheritPrincipal });
-  }
+    return { postData, url: shortcutURL, mayInheritPrincipal };
+  }).then(data => {
+    if (callback) {
+      callback(data);
+    }
+
+    return data;
+  });
 }
 
 function getPostDataStream(aStringData, aKeyword, aEncKeyword, aType) {
   var dataStream = Cc["@mozilla.org/io/string-input-stream;1"].
                    createInstance(Ci.nsIStringInputStream);
   aStringData = aStringData.replace(/%s/g, aEncKeyword).replace(/%S/g, aKeyword);
   dataStream.data = aStringData;
 
@@ -3247,17 +3264,17 @@ var newTabButtonObserver = {
 
   onDragExit: function (aEvent)
   {
   },
 
   onDrop: function (aEvent)
   {
     let url = browserDragAndDrop.drop(aEvent, { });
-    getShortcutOrURIAndPostData(url, data => {
+    getShortcutOrURIAndPostData(url).then(data => {
       if (data.url) {
         // allow third-party services to fixup this URL
         openNewTabWith(data.url, null, data.postData, aEvent, true);
       }
     });
   }
 }
 
@@ -3267,17 +3284,17 @@ var newWindowButtonObserver = {
     browserDragAndDrop.dragOver(aEvent);
   },
   onDragExit: function (aEvent)
   {
   },
   onDrop: function (aEvent)
   {
     let url = browserDragAndDrop.drop(aEvent, { });
-    getShortcutOrURIAndPostData(url, data => {
+    getShortcutOrURIAndPostData(url).then(data => {
       if (data.url) {
         // allow third-party services to fixup this URL
         openNewWindowWith(data.url, null, data.postData, true);
       }
     });
   }
 }
 
@@ -5603,17 +5620,17 @@ function middleMousePaste(event) {
   // if it's not the current tab, we don't need to do anything because the 
   // browser doesn't exist.
   let where = whereToOpenLink(event, true, false);
   let lastLocationChange;
   if (where == "current") {
     lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
   }
 
-  getShortcutOrURIAndPostData(clipboard, data => {
+  getShortcutOrURIAndPostData(clipboard).then(data => {
     try {
       makeURI(data.url);
     } catch (ex) {
       // Not a valid URI.
       return;
     }
 
     try {
@@ -5640,17 +5657,17 @@ function stripUnsafeProtocolOnPaste(past
   // LOAD_FLAGS_DISALLOW_INHERIT_OWNER for those.
   return pasteData.replace(/^(?:\s*javascript:)+/i, "");
 }
 
 function handleDroppedLink(event, url, name)
 {
   let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
 
-  getShortcutOrURIAndPostData(url, data => {
+  getShortcutOrURIAndPostData(url).then(data => {
     if (data.url &&
         lastLocationChange == gBrowser.selectedBrowser.lastLocationChange)
       loadURI(data.url, null, data.postData, false);
   });
 
   // Keep the event from being handled by the dragDrop listeners
   // built-in to gecko if they happen to be above us.
   event.preventDefault();
--- a/browser/base/content/newtab/grid.js
+++ b/browser/base/content/newtab/grid.js
@@ -185,19 +185,19 @@ let gGrid = {
     // Same goes for the grid if that's not ready yet.
     if (!this.isDocumentLoaded || !this._ready) {
       return;
     }
 
     // Save the cell's computed height/width including margin and border
     if (this._cellMargin === undefined) {
       let refCell = document.querySelector(".newtab-cell");
-      this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop) +
+      this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop);
+      this._cellHeight = refCell.offsetHeight + this._cellMargin +
         parseFloat(getComputedStyle(refCell).marginBottom);
-      this._cellHeight = refCell.offsetHeight + this._cellMargin;
       this._cellWidth = refCell.offsetWidth + this._cellMargin;
     }
 
     let availSpace = document.documentElement.clientHeight - this._cellMargin -
                      document.querySelector("#newtab-search-container").offsetHeight;
     let visibleRows = Math.floor(availSpace / this._cellHeight);
     this._node.style.height = this._computeHeight() + "px";
     this._node.style.maxHeight = this._computeHeight(visibleRows) + "px";
--- a/browser/base/content/test/general/browser_action_keyword.js
+++ b/browser/base/content/test/general/browser_action_keyword.js
@@ -8,56 +8,56 @@ function* promise_first_result(inputText
 
   let firstResult = gURLBar.popup.richlistbox.firstChild;
   return firstResult;
 }
 
 
 add_task(function*() {
   // This test is only relevant if UnifiedComplete is enabled.
-  if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete")) {
-    todo(false, "Stop supporting old autocomplete components.");
-    return;
-  }
+  let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  registerCleanupFunction(() => {
+    Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
+  });
 
   let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla");
   let tabs = [tab];
-  registerCleanupFunction(() => {
+  registerCleanupFunction(function* () {
     for (let tab of tabs)
       gBrowser.removeTab(tab);
-    PlacesUtils.bookmarks.removeItem(itemId);
+    yield PlacesUtils.bookmarks.remove(bm);
   });
 
   yield promiseTabLoadEvent(tab);
 
-  let itemId =
-    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
-                                         NetUtil.newURI("http://example.com/?q=%s"),
-                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                         "test");
-  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+  let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+                                                url: "http://example.com/?q=%s",
+                                                title: "test" });
+  yield PlacesUtils.keywords.insert({ keyword: "keyword",
+                                      url: "http://example.com/?q=%s" });
 
   let result = yield promise_first_result("keyword something");
   isnot(result, null, "Expect a keyword result");
 
   is(result.getAttribute("type"), "action keyword", "Expect correct  `type` attribute");
   is(result.getAttribute("actiontype"), "keyword", "Expect correct `actiontype` attribute");
-  is(result.getAttribute("title"), "test", "Expect correct title");
+  is(result.getAttribute("title"), "example.com", "Expect correct title");
 
   // We need to make a real URI out of this to ensure it's normalised for
   // comparison.
   let uri = NetUtil.newURI(result.getAttribute("url"));
   is(uri.spec, makeActionURI("keyword", {url: "http://example.com/?q=something", input: "keyword something"}).spec, "Expect correct url");
 
   is_element_visible(result._title, "Title element should be visible");
   is(result._title.childNodes.length, 1, "Title element should have 1 child");
   is(result._title.childNodes[0].nodeName, "#text", "That child should be a text node");
-  is(result._title.childNodes[0].data, "test", "Node should contain the name of the bookmark");
+  is(result._title.childNodes[0].data, "example.com", "Node should contain the name of the bookmark");
 
-  is_element_visible(result._extra, "Extra element should be visible");
+  is_element_visible(result._extraBox, "Extra element should be visible");
   is(result._extra.childNodes.length, 1, "Title element should have 1 child");
   is(result._extra.childNodes[0].nodeName, "span", "That child should be a span node");
   let span = result._extra.childNodes[0];
   is(span.childNodes.length, 1, "span element should have 1 child");
   is(span.childNodes[0].nodeName, "#text", "That child should be a text node");
   is(span.childNodes[0].data, "something", "Node should contain the query for the keyword");
 
   is_element_hidden(result._url, "URL element should be hidden");
--- a/browser/base/content/test/general/browser_action_keyword_override.js
+++ b/browser/base/content/test/general/browser_action_keyword_override.js
@@ -1,35 +1,35 @@
 add_task(function*() {
   // This test is only relevant if UnifiedComplete is enabled.
-  if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete")) {
-    todo(false, "Stop supporting old autocomplete components.");
-    return;
-  }
+  let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  registerCleanupFunction(() => {
+    Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
+  });
 
-  let itemId =
-    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
-                                         NetUtil.newURI("http://example.com/?q=%s"),
-                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                         "test");
-  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+  let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+                                                url: "http://example.com/?q=%s",
+                                                title: "test" });
+  yield PlacesUtils.keywords.insert({ keyword: "keyword",
+                                      url: "http://example.com/?q=%s" })
 
-  registerCleanupFunction(() => {
-    PlacesUtils.bookmarks.removeItem(itemId);
+  registerCleanupFunction(function* () {
+    yield PlacesUtils.bookmarks.remove(bm);
   });
 
   yield promiseAutocompleteResultPopup("keyword search");
   let result = gURLBar.popup.richlistbox.children[0];
 
   info("Before override");
   is_element_hidden(result._url, "URL element should be hidden");
-  is_element_visible(result._extra, "Extra element should be visible");
+  is_element_visible(result._extraBox, "Extra element should be visible");
 
   info("During override");
   EventUtils.synthesizeKey("VK_SHIFT" , { type: "keydown" });
   is_element_hidden(result._url, "URL element should be hidden");
-  is_element_visible(result._extra, "Extra element should be visible");
+  is_element_visible(result._extraBox, "Extra element should be visible");
 
   EventUtils.synthesizeKey("VK_SHIFT" , { type: "keyup" });
 
   gURLBar.popup.hidePopup();
   yield promisePopupHidden(gURLBar.popup);
 });
--- a/browser/base/content/test/general/browser_action_searchengine.js
+++ b/browser/base/content/test/general/browser_action_searchengine.js
@@ -1,17 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 add_task(function* () {
   // This test is only relevant if UnifiedComplete is enabled.
-  if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete")) {
-    todo(false, "Stop supporting old autocomplete components.");
-    return;
-  }
+  let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  registerCleanupFunction(() => {
+    Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
+  });
 
   Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
                                        "http://example.com/?q={searchTerms}");
   let engine = Services.search.getEngineByName("MozSearch");
   let originalEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
 
   let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
--- a/browser/base/content/test/general/browser_action_searchengine_alias.js
+++ b/browser/base/content/test/general/browser_action_searchengine_alias.js
@@ -1,31 +1,35 @@
 /**
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  **/
 
 add_task(function* () {
+  // This test is only relevant if UnifiedComplete is enabled.
+  let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
   Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  registerCleanupFunction(() => {
+    Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
+  });
 
   let iconURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC";
   Services.search.addEngineWithDetails("MozSearch", iconURI, "moz", "", "GET",
                                        "http://example.com/?q={searchTerms}");
   let engine = Services.search.getEngineByName("MozSearch");
   let originalEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
 
   let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
   yield promiseTabLoaded(gBrowser.selectedTab);
 
   registerCleanupFunction(() => {
     Services.search.currentEngine = originalEngine;
     let engine = Services.search.getEngineByName("MozSearch");
     Services.search.removeEngine(engine);
-    Services.prefs.clearUserPref("browser.urlbar.unifiedcomplete");
 
     try {
       gBrowser.removeTab(tab);
     } catch(ex) { /* tab may have already been closed in case of failure */ }
 
     return PlacesTestUtils.clearHistory();
   });
 
--- a/browser/base/content/test/general/browser_autocomplete_a11y_label.js
+++ b/browser/base/content/test/general/browser_autocomplete_a11y_label.js
@@ -1,17 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 add_task(function*() {
   // This test is only relevant if UnifiedComplete is enabled.
-  if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete")) {
-    todo(false, "Stop supporting old autocomplete components.");
-    return;
-  }
+  let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  registerCleanupFunction(() => {
+    Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
+  });
 
   let tab = gBrowser.addTab("about:about");
   yield promiseTabLoaded(tab);
 
   let actionURL = makeActionURI("switchtab", {url: "about:about"}).spec;
   yield promiseAutocompleteResultPopup("% about");
 
   ok(gURLBar.popup.richlistbox.children.length > 1, "Should get at least 2 results");
--- a/browser/base/content/test/general/browser_autocomplete_autoselect.js
+++ b/browser/base/content/test/general/browser_autocomplete_autoselect.js
@@ -5,22 +5,25 @@ function repeat(limit, func) {
 }
 
 function is_selected(index) {
   is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
 }
 
 add_task(function*() {
   // This test is only relevant if UnifiedComplete is enabled.
-  if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete")) {
-    todo(false, "Stop supporting old autocomplete components.");
-    return;
-  }
+  let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  registerCleanupFunction(() => {
+    Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
+  });
 
-  registerCleanupFunction(() => PlacesTestUtils.clearHistory());
+  registerCleanupFunction(function* () {
+    yield PlacesTestUtils.clearHistory();
+  });
 
   let visits = [];
   repeat(10, i => {
     visits.push({
       uri: makeURI("http://example.com/autocomplete/?" + i),
     });
   });
   yield PlacesTestUtils.addVisits(visits);
--- a/browser/base/content/test/general/browser_autocomplete_enter_race.js
+++ b/browser/base/content/test/general/browser_autocomplete_enter_race.js
@@ -1,23 +1,22 @@
 add_task(function*() {
   // This test is only relevant if UnifiedComplete is enabled.
   Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
 
-  registerCleanupFunction(() => {
-    PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+  registerCleanupFunction(function* () {
     Services.prefs.clearUserPref("browser.urlbar.unifiedcomplete");
+    yield PlacesUtils.bookmarks.remove(bm);
   });
 
-  let itemId =
-    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
-                                         NetUtil.newURI("http://example.com/?q=%s"),
-                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                         "test");
-  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+  let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+                                                url: "http://example.com/?q=%s",
+                                                title: "test" });
+  yield PlacesUtils.keywords.insert({ keyword: "keyword",
+                                      url: "http://example.com/?q=%s" });
 
   yield new Promise(resolve => waitForFocus(resolve, window));
 
   yield promiseAutocompleteResultPopup("keyword bear");
   gURLBar.focus();
   EventUtils.synthesizeKey("d", {});
   EventUtils.synthesizeKey("VK_RETURN", {});
 
--- a/browser/base/content/test/general/browser_autocomplete_no_title.js
+++ b/browser/base/content/test/general/browser_autocomplete_no_title.js
@@ -1,17 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 add_task(function*() {
   // This test is only relevant if UnifiedComplete is enabled.
-  if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete")) {
-    todo(false, "Stop supporting old autocomplete components.");
-    return;
-  }
+  let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  registerCleanupFunction(() => {
+    Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
+  });
 
   let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
   yield promiseTabLoaded(tab);
 
   let uri = NetUtil.newURI("http://bug1060642.example.com/beards/are/pretty/great");
   yield PlacesTestUtils.addVisits([{uri: uri, title: ""}]);
 
   yield promiseAutocompleteResultPopup("bug1060642");
--- a/browser/base/content/test/general/browser_autocomplete_oldschool_wrap.js
+++ b/browser/base/content/test/general/browser_autocomplete_oldschool_wrap.js
@@ -10,33 +10,37 @@ function is_selected(index) {
 
 add_task(function*() {
   // This test is only relevant if UnifiedComplete is *disabled*.
   if (Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete")) {
     ok(true, "Don't run this test with UnifiedComplete enabled.")
     return;
   }
 
-  registerCleanupFunction(() => PlacesTestUtils.clearHistory());
-
+  yield PlacesTestUtils.clearHistory();
   let visits = [];
   repeat(10, i => {
     visits.push({
       uri: makeURI("http://example.com/autocomplete/?" + i),
     });
   });
   yield PlacesTestUtils.addVisits(visits);
 
+  registerCleanupFunction(function* () {
+    yield PlacesTestUtils.clearHistory();
+  });
+
   let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
   yield promiseTabLoaded(tab);
   yield promiseAutocompleteResultPopup("example.com/autocomplete");
 
   let popup = gURLBar.popup;
-  let results = popup.richlistbox.children;
-  is(results.length, 10, "Should get 11 results");
+  let results = popup.richlistbox.children.filter(is_visible);
+
+  is(results.length, 10, "Should get 10 results");
   is_selected(-1);
 
   info("Key Down to select the next item");
   EventUtils.synthesizeKey("VK_DOWN", {});
   is_selected(0);
 
   info("Key Up to select the previous item");
   EventUtils.synthesizeKey("VK_UP", {});
--- a/browser/base/content/test/general/browser_bug1003461-switchtab-override.js
+++ b/browser/base/content/test/general/browser_bug1003461-switchtab-override.js
@@ -1,18 +1,19 @@
 /* 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/. */
 
 add_task(function* test_switchtab_override() {
   // This test is only relevant if UnifiedComplete is enabled.
-  if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete")) {
-    todo(false, "Stop supporting old autocomplete components.");
-    return;
-  }
+  let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  registerCleanupFunction(() => {
+    Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
+  });
 
   let testURL = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
 
   info("Opening first tab");
   let tab = gBrowser.addTab(testURL);
   let deferred = Promise.defer();
   whenTabLoaded(tab, deferred.resolve);
   yield deferred.promise;
--- a/browser/base/content/test/general/browser_bug1070778.js
+++ b/browser/base/content/test/general/browser_bug1070778.js
@@ -2,39 +2,42 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function is_selected(index) {
   is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
 }
 
 add_task(function*() {
   // This test is only relevant if UnifiedComplete is enabled.
-  if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete")) {
-    todo(false, "Stop supporting old autocomplete components.");
-    return;
-  }
-
+  let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
   registerCleanupFunction(() => {
-    PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+    Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
   });
 
-  let itemId =
-    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
-                                         NetUtil.newURI("http://example.com/?q=%s"),
-                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                         "test");
-  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+  let bookmarks = [];
+  bookmarks.push((yield PlacesUtils.bookmarks
+                                   .insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+                                             url: "http://example.com/?q=%s",
+                                             title: "test" })));
+  yield PlacesUtils.keywords.insert({ keyword: "keyword",
+                                      url: "http://example.com/?q=%s" });
 
   // This item only needed so we can select the keyword item, select something
   // else, then select the keyword item again.
-  itemId =
-    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
-                                         NetUtil.newURI("http://example.com/keyword"),
-                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                         "keyword abc");
+  bookmarks.push((yield PlacesUtils.bookmarks
+                                   .insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+                                             url: "http://example.com/keyword",
+                                             title: "keyword abc" })));
+
+  registerCleanupFunction(function* () {
+    for (let bm of bookmarks) {
+      yield PlacesUtils.bookmarks.remove(bm);
+    }
+  });
 
   let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
   yield promiseTabLoaded(tab);
   yield promiseAutocompleteResultPopup("keyword a");
 
   // First item should already be selected
   is_selected(0);
   // Select next one (important!)
--- a/browser/base/content/test/general/browser_getshortcutoruri.js
+++ b/browser/base/content/test/general/browser_getshortcutoruri.js
@@ -95,18 +95,17 @@ add_task(function* test_getshortcutoruri
   yield setupKeywords();
 
   for (let item of testData) {
     let [data, result] = item;
 
     let query = data.keyword;
     if (data.searchWord)
       query += " " + data.searchWord;
-    let returnedData = yield new Promise(
-      resolve => getShortcutOrURIAndPostData(query, resolve));
+    let returnedData = yield getShortcutOrURIAndPostData(query);
     // null result.url means we should expect the same query we sent in
     let expected = result.url || query;
     is(returnedData.url, expected, "got correct URL for " + data.keyword);
     is(getPostDataString(returnedData.postData), result.postData, "got correct postData for " + data.keyword);
     is(returnedData.mayInheritPrincipal, !result.isUnsafe, "got correct mayInheritPrincipal for " + data.keyword);
   }
 
   yield cleanupKeywords();
--- a/browser/base/content/test/general/browser_urlbarEnterAfterMouseOver.js
+++ b/browser/base/content/test/general/browser_urlbarEnterAfterMouseOver.js
@@ -11,17 +11,19 @@ function* promiseAutoComplete(inputText)
   yield promiseSearchComplete();
 }
 
 function is_selected(index) {
   is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
 }
 
 add_task(function*() {
-  registerCleanupFunction(() => PlacesTestUtils.clearHistory());
+  registerCleanupFunction(function* () {
+    yield PlacesTestUtils.clearHistory();
+  });
 
   yield PlacesTestUtils.clearHistory();
   let tabCount = gBrowser.tabs.length;
 
   let visits = [];
   repeat(10, i => {
     visits.push({
       uri: makeURI("http://example.com/autocomplete/?" + i),
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -445,17 +445,17 @@
               } else {
                 url = url + suffix;
               }
 
               url = "http://www." + url;
             }
           }
 
-          getShortcutOrURIAndPostData(url, data => {
+          getShortcutOrURIAndPostData(url).then(data => {
             aCallback([data.url, data.postData, data.mayInheritPrincipal]);
           });
         ]]></body>
       </method>
 
       <field name="_contentIsCropped">false</field>
 
       <method name="_initURLTooltip">
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1656,17 +1656,17 @@ BrowserGlue.prototype = {
     var notifyBox = win.gBrowser.getNotificationBox();
     var notification = notifyBox.appendNotification(text, title, null,
                                                     notifyBox.PRIORITY_CRITICAL_MEDIUM,
                                                     buttons);
     notification.persistence = -1; // Until user closes it
   },
 
   _migrateUI: function BG__migrateUI() {
-    const UI_VERSION = 28;
+    const UI_VERSION = 29;
     const BROWSER_DOCURL = "chrome://browser/content/browser.xul";
     let currentUIVersion = 0;
     try {
       currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
     } catch(ex) {}
     if (currentUIVersion >= UI_VERSION)
       return;
 
@@ -2011,16 +2011,31 @@ BrowserGlue.prototype = {
         Services.prefs.setCharPref("lightweightThemes.selectedThemeID", "");
       }
 
       // Not clearing browser.devedition.theme.enabled, to preserve user's pref
       // if for some reason this function runs again (even though it shouldn't)
       Services.prefs.clearUserPref("browser.devedition.showCustomizeButton");
     }
 
+    if (currentUIVersion < 29) {
+      let group = null;
+      try {
+        group = Services.prefs.getComplexValue("font.language.group",
+                                               Ci.nsIPrefLocalizedString);
+      } catch (ex) {}
+      if (group &&
+          ["tr", "x-baltic", "x-central-euro"].some(g => g == group.data)) {
+        // Latin groups were consolidated.
+        group.data = "x-western";
+        Services.prefs.setComplexValue("font.language.group",
+                                       Ci.nsIPrefLocalizedString, group);
+      }
+    }
+
     // Update the migration version.
     Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
   },
 
   // ------------------------------
   // public nsIBrowserGlue members
   // ------------------------------
 
--- a/browser/components/preferences/permissions.js
+++ b/browser/components/preferences/permissions.js
@@ -1,9 +1,8 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
 const nsICookiePermission = Components.interfaces.nsICookiePermission;
@@ -227,16 +226,30 @@ var gPermissionManager = {
     urlField.value = aParams.prefilledHost;
     urlField.hidden = !urlFieldVisible;
 
     this.onHostInput(urlField);
 
     var urlLabel = document.getElementById("urlLabel");
     urlLabel.hidden = !urlFieldVisible;
 
+    let treecols = document.getElementsByTagName("treecols")[0];
+    treecols.addEventListener("click", event => {
+      if (event.target.nodeName != "treecol" || event.button != 0) {
+        return;
+      }
+
+      let sortField = event.target.getAttribute("data-field-name");
+      if (!sortField) {
+        return;
+      }
+
+      gPermissionManager.onPermissionSort(sortField);
+    });
+
     Services.obs.notifyObservers(null, NOTIFICATION_FLUSH_PERMISSIONS, this._type);
     Services.obs.addObserver(this, "perm-changed", false);
 
     this._loadPermissions();
 
     urlField.focus();
   },
 
--- a/browser/components/preferences/permissions.xul
+++ b/browser/components/preferences/permissions.xul
@@ -48,20 +48,20 @@
     </hbox>
     <separator class="thin"/>
     <tree id="permissionsTree" flex="1" style="height: 18em;"
           hidecolumnpicker="true"
           onkeypress="gPermissionManager.onPermissionKeyPress(event)"
           onselect="gPermissionManager.onPermissionSelected();">
       <treecols>
         <treecol id="siteCol" label="&treehead.sitename.label;" flex="3"
-                onclick="gPermissionManager.onPermissionSort('rawHost');" persist="width"/>
+                 data-field-name="rawHost" persist="width"/>
         <splitter class="tree-splitter"/>
         <treecol id="statusCol" label="&treehead.status.label;" flex="1"
-                onclick="gPermissionManager.onPermissionSort('capability');" persist="width"/>
+                 data-field-name="capability" persist="width"/>
       </treecols>
       <treechildren/>
     </tree>
   </vbox>
   <vbox>
     <hbox class="actionButtons" align="left" flex="1">
       <button id="removePermission" disabled="true"
               accesskey="&removepermission.accesskey;"
--- a/browser/components/readinglist/Scheduler.jsm
+++ b/browser/components/readinglist/Scheduler.jsm
@@ -28,16 +28,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 // 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",
@@ -70,16 +76,17 @@ let intervals = {
 
 // 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;
@@ -170,16 +177,18 @@ InternalScheduler.prototype = {
       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
@@ -280,20 +289,34 @@ InternalScheduler.prototype = {
       // 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(this._logManager.REASON_SUCCESS);
       Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
       return intervals.schedule;
     }).catch(err => {
-      this.log.error("Sync failed", err);
-      // XXX - how to detect an auth error?
-      this.state = err == this._engine.ERROR_AUTHENTICATION ?
+      // 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.log.info("Can't sync due to FxA account state " + err.message);
+        this.state = this.STATE_OK;
+        this._logManager.resetFileLog(this._logManager.REASON_SUCCESS);
+        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(this._logManager.REASON_ERROR);
       Services.obs.notifyObservers(null, "readinglist:sync:error", null);
       return intervals.retry;
     }).then(nextDelay => {
       this._timerRunning = false;
       // ensure a new timer is setup for the appropriate next time.
       this._maybeReschedule(nextDelay);
       this._setupTimer();
--- a/browser/components/readinglist/Sync.jsm
+++ b/browser/components/readinglist/Sync.jsm
@@ -273,18 +273,23 @@ SyncImpl.prototype = {
         // "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, so treat 200 as an unexpected response.
-      if (response.status != 201) {
+      // 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("uploading a new item", response);
         continue;
       }
       let item = yield this.list.itemForURL(response.body.url);
       yield this._updateItemWithServerRecord(item, response.body);
     }
   }),
 
@@ -399,17 +404,23 @@ SyncImpl.prototype = {
         if (serverRecord.deleted) {
           yield this._deleteItemForGUID(serverRecord.id);
           continue;
         }
         yield this._updateItemWithServerRecord(localItem, serverRecord);
         continue;
       }
       // new item
-      yield this.list.addItem(localRecordFromServerRecord(serverRecord));
+      let localRecord = localRecordFromServerRecord(serverRecord);
+      try {
+        yield this.list.addItem(localRecord);
+      } catch (ex) {
+        log.warn("Failed to add a new item from server record ${serverRecord}: ${ex}",
+                 {serverRecord, ex});
+      }
     }
   }),
 
   /**
    * Phase 3 (material changes)
    *
    * Uploads not-new items with material changes.
    */
@@ -437,41 +448,56 @@ SyncImpl.prototype = {
    * @param item A local ReadingListItem.
    * @param serverRecord A server record representing the item.
    */
   _updateItemWithServerRecord: Task.async(function* (localItem, serverRecord) {
     if (!localItem) {
       throw new Error("Item should exist");
     }
     localItem._record = localRecordFromServerRecord(serverRecord);
-    yield this.list.updateItem(localItem);
+    try {
+      yield this.list.updateItem(localItem);
+    } catch (ex) {
+      log.warn("Failed to update 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;
-      yield this.list.deleteItem(item);
+      try {
+        yield this.list.deleteItem(item);
+      } catch (ex) {
+        log.warn("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");
-    this.list._store.deleteItemByGUID(guid);
+    try {
+      this.list._store.deleteItemByGUID(guid);
+    } catch (ex) {
+      log.warn("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 }.
--- a/browser/modules/DirectoryLinksProvider.jsm
+++ b/browser/modules/DirectoryLinksProvider.jsm
@@ -52,18 +52,21 @@ const PREF_NEWTAB_ENHANCED = "browser.ne
 const ALLOWED_LINK_SCHEMES = new Set(["http", "https"]);
 
 // Only allow link image urls that are https or data
 const ALLOWED_IMAGE_SCHEMES = new Set(["https", "data"]);
 
 // The frecency of a directory link
 const DIRECTORY_FRECENCY = 1000;
 
-// The frecency of a related link
-const RELATED_FRECENCY = Infinity;
+// The frecency of a suggested link
+const SUGGESTED_FRECENCY = Infinity;
+
+// Default number of times to show a link
+const DEFAULT_FREQUENCY_CAP = 5;
 
 // Divide frecency by this amount for pings
 const PING_SCORE_DIVISOR = 10000;
 
 // Allowed ping actions remotely stored as columns: case-insensitive [a-z0-9_]
 const PING_ACTIONS = ["block", "click", "pin", "sponsored", "sponsored_link", "unpin", "view"];
 
 /**
@@ -84,24 +87,29 @@ let DirectoryLinksProvider = {
   _downloadIntervalMS: 86400000,
 
   /**
    * A mapping from eTLD+1 to an enhanced link objects
    */
   _enhancedLinks: new Map(),
 
   /**
-   * A mapping from site to a list of related link objects
+   * A mapping from site to remaining number of views
    */
-  _relatedLinks: new Map(),
+  _frequencyCaps: new Map(),
 
   /**
-   * A set of top sites that we can provide related links for
+   * A mapping from site to a list of suggested link objects
    */
-  _topSitesWithRelatedLinks: new Set(),
+  _suggestedLinks: new Map(),
+
+  /**
+   * A set of top sites that we can provide suggested links for
+   */
+  _topSitesWithSuggestedLinks: new Set(),
 
   get _observedPrefs() Object.freeze({
     enhanced: PREF_NEWTAB_ENHANCED,
     linksURL: PREF_DIRECTORY_SOURCE,
     matchOSLocale: PREF_MATCH_OS_LOCALE,
     prefSelectedLocale: PREF_SELECTED_LOCALE,
   }),
 
@@ -196,21 +204,21 @@ let DirectoryLinksProvider = {
 
   _removePrefsObserver: function DirectoryLinksProvider_removeObserver() {
     for (let pref in this._observedPrefs) {
       let prefName = this._observedPrefs[pref];
       Services.prefs.removeObserver(prefName, this);
     }
   },
 
-  _cacheRelatedLinks: function(link) {
-    for (let relatedSite of link.frecent_sites) {
-      let relatedMap = this._relatedLinks.get(relatedSite) || new Map();
-      relatedMap.set(link.url, link);
-      this._relatedLinks.set(relatedSite, relatedMap);
+  _cacheSuggestedLinks: function(link) {
+    for (let suggestedSite of link.frecent_sites) {
+      let suggestedMap = this._suggestedLinks.get(suggestedSite) || new Map();
+      suggestedMap.set(link.url, link);
+      this._suggestedLinks.set(suggestedSite, suggestedMap);
     }
   },
 
   _fetchAndCacheLinks: function DirectoryLinksProvider_fetchAndCacheLinks(uri) {
     // Replace with the same display locale used for selecting links data
     uri = uri.replace("%LOCALE%", this.locale);
 
     let deferred = Promise.defer();
@@ -320,16 +328,33 @@ let DirectoryLinksProvider = {
   /**
    * Report some action on a newtab page (view, click)
    * @param sites Array of sites shown on newtab page
    * @param action String of the behavior to report
    * @param triggeringSiteIndex optional Int index of the site triggering action
    * @return download promise
    */
   reportSitesAction: function DirectoryLinksProvider_reportSitesAction(sites, action, triggeringSiteIndex) {
+    // Check if the suggested tile was shown
+    if (action == "view") {
+      sites.slice(0, triggeringSiteIndex + 1).forEach(site => {
+        let {targetedSite, url} = site.link;
+        if (targetedSite) {
+          this._decreaseFrequencyCap(url, 1);
+        }
+      });
+    }
+    // Use up all views if the user clicked on a frequency capped tile
+    else if (action == "click") {
+      let {targetedSite, url} = sites[triggeringSiteIndex].link;
+      if (targetedSite) {
+        this._decreaseFrequencyCap(url, DEFAULT_FREQUENCY_CAP);
+      }
+    }
+
     let newtabEnhanced = false;
     let pingEndPoint = "";
     try {
       newtabEnhanced = Services.prefs.getBoolPref(PREF_NEWTAB_ENHANCED);
       pingEndPoint = Services.prefs.getCharPref(PREF_DIRECTORY_PING);
     }
     catch (ex) {}
 
@@ -408,19 +433,20 @@ let DirectoryLinksProvider = {
   },
 
   /**
    * Gets the current set of directory links.
    * @param aCallback The function that the array of links is passed to.
    */
   getLinks: function DirectoryLinksProvider_getLinks(aCallback) {
     this._readDirectoryLinksFile().then(rawLinks => {
-      // Reset the cache of related tiles and enhanced images for this new set of links
+      // Reset the cache of suggested tiles and enhanced images for this new set of links
       this._enhancedLinks.clear();
-      this._relatedLinks.clear();
+      this._frequencyCaps.clear();
+      this._suggestedLinks.clear();
 
       let validityFilter = function(link) {
         // Make sure the link url is allowed and images too if they exist
         return this.isURLAllowed(link.url, ALLOWED_LINK_SCHEMES) &&
                this.isURLAllowed(link.imageURI, ALLOWED_IMAGE_SCHEMES) &&
                this.isURLAllowed(link.enhancedImageURI, ALLOWED_IMAGE_SCHEMES);
       }.bind(this);
 
@@ -430,26 +456,32 @@ let DirectoryLinksProvider = {
           this._enhancedLinks.set(NewTabUtils.extractSite(link.url), link);
         }
         link.lastVisitDate = length - position;
       }.bind(this);
 
       rawLinks.suggested.filter(validityFilter).forEach((link, position) => {
         setCommonProperties(link, rawLinks.suggested.length, position);
 
-        // We cache related tiles here but do not push any of them in the links list yet.
-        // The decision for which related tile to include will be made separately.
-        this._cacheRelatedLinks(link);
+        // We cache suggested tiles here but do not push any of them in the links list yet.
+        // The decision for which suggested tile to include will be made separately.
+        this._cacheSuggestedLinks(link);
+        this._frequencyCaps.set(link.url, DEFAULT_FREQUENCY_CAP);
       });
 
-      return rawLinks.directory.filter(validityFilter).map((link, position) => {
+      let links = rawLinks.directory.filter(validityFilter).map((link, position) => {
         setCommonProperties(link, rawLinks.directory.length, position);
         link.frecency = DIRECTORY_FRECENCY;
         return link;
       });
+
+      // Allow for one link suggestion on top of the default directory links
+      this.maxNumLinks = links.length + 1;
+
+      return links;
     }).catch(ex => {
       Cu.reportError(ex);
       return [];
     }).then(links => {
       aCallback(links);
       this._populatePlacesLinks();
     });
   },
@@ -471,145 +503,171 @@ let DirectoryLinksProvider = {
         this._lastDownloadMS = Date.parse(fileInfo.lastModificationDate);
       }
       // fetch directory on startup without force
       yield this._fetchAndCacheLinksIfNecessary();
     }.bind(this));
   },
 
   _handleManyLinksChanged: function() {
-    this._topSitesWithRelatedLinks.clear();
-    this._relatedLinks.forEach((relatedLinks, site) => {
+    this._topSitesWithSuggestedLinks.clear();
+    this._suggestedLinks.forEach((suggestedLinks, site) => {
       if (NewTabUtils.isTopPlacesSite(site)) {
-        this._topSitesWithRelatedLinks.add(site);
+        this._topSitesWithSuggestedLinks.add(site);
       }
     });
-    this._updateRelatedTile();
+    this._updateSuggestedTile();
   },
 
   /**
-   * Updates _topSitesWithRelatedLinks based on the link that was changed.
+   * Updates _topSitesWithSuggestedLinks based on the link that was changed.
    *
-   * @return true if _topSitesWithRelatedLinks was modified, false otherwise.
+   * @return true if _topSitesWithSuggestedLinks was modified, false otherwise.
    */
   _handleLinkChanged: function(aLink) {
     let changedLinkSite = NewTabUtils.extractSite(aLink.url);
-    let linkStored = this._topSitesWithRelatedLinks.has(changedLinkSite);
+    let linkStored = this._topSitesWithSuggestedLinks.has(changedLinkSite);
 
     if (!NewTabUtils.isTopPlacesSite(changedLinkSite) && linkStored) {
-      this._topSitesWithRelatedLinks.delete(changedLinkSite);
+      this._topSitesWithSuggestedLinks.delete(changedLinkSite);
       return true;
     }
 
-    if (this._relatedLinks.has(changedLinkSite) &&
+    if (this._suggestedLinks.has(changedLinkSite) &&
         NewTabUtils.isTopPlacesSite(changedLinkSite) && !linkStored) {
-      this._topSitesWithRelatedLinks.add(changedLinkSite);
+      this._topSitesWithSuggestedLinks.add(changedLinkSite);
       return true;
     }
     return false;
   },
 
   _populatePlacesLinks: function () {
     NewTabUtils.links.populateProviderCache(NewTabUtils.placesProvider, () => {
       this._handleManyLinksChanged();
     });
   },
 
   onLinkChanged: function (aProvider, aLink) {
     // Make sure NewTabUtils.links handles the notification first.
     setTimeout(() => {
       if (this._handleLinkChanged(aLink)) {
-        this._updateRelatedTile();
+        this._updateSuggestedTile();
       }
     }, 0);
   },
 
   onManyLinksChanged: function () {
     // Make sure NewTabUtils.links handles the notification first.
     setTimeout(() => {
       this._handleManyLinksChanged();
     }, 0);
   },
 
   /**
-   * Chooses and returns a related tile based on a user's top sites
-   * that we have an available related tile for.
+   * Record for a url that some number of views have been used
+   * @param url String url of the suggested link
+   * @param amount Number of equivalent views to decrease
+   */
+  _decreaseFrequencyCap(url, amount) {
+    let remainingViews = this._frequencyCaps.get(url) - amount;
+    this._frequencyCaps.set(url, remainingViews);
+
+    // Reached the number of views, so pick a new one.
+    if (remainingViews <= 0) {
+      this._updateSuggestedTile();
+    }
+  },
+
+  /**
+   * Chooses and returns a suggested tile based on a user's top sites
+   * that we have an available suggested tile for.
    *
-   * @return the chosen related tile, or undefined if there isn't one
+   * @return the chosen suggested tile, or undefined if there isn't one
    */
-  _updateRelatedTile: function() {
+  _updateSuggestedTile: function() {
     let sortedLinks = NewTabUtils.getProviderLinks(this);
 
     if (!sortedLinks) {
       // If NewTabUtils.links.resetCache() is called before getting here,
       // sortedLinks may be undefined.
       return;
     }
 
-    // Delete the current related tile, if one exists.
+    // Delete the current suggested tile, if one exists.
     let initialLength = sortedLinks.length;
-    this.maxNumLinks = initialLength;
     if (initialLength) {
       let mostFrecentLink = sortedLinks[0];
       if (mostFrecentLink.targetedSite) {
         this._callObservers("onLinkChanged", {
           url: mostFrecentLink.url,
-          frecency: 0,
+          frecency: SUGGESTED_FRECENCY,
           lastVisitDate: mostFrecentLink.lastVisitDate,
           type: mostFrecentLink.type,
         }, 0, true);
       }
     }
 
-    if (this._topSitesWithRelatedLinks.size == 0) {
-      // There are no potential related links we can show.
+    if (this._topSitesWithSuggestedLinks.size == 0) {
+      // There are no potential suggested links we can show.
       return;
     }
 
-    // Create a flat list of all possible links we can show as related.
-    // Note that many top sites may map to the same related links, but we only
-    // want to count each related link once (based on url), thus possibleLinks is a map
-    // from url to relatedLink. Thus, each link has an equal chance of being chosen at
+    // Create a flat list of all possible links we can show as suggested.
+    // Note that many top sites may map to the same suggested links, but we only
+    // want to count each suggested link once (based on url), thus possibleLinks is a map
+    // from url to suggestedLink. Thus, each link has an equal chance of being chosen at
     // random from flattenedLinks if it appears only once.
     let possibleLinks = new Map();
     let targetedSites = new Map();
-    this._topSitesWithRelatedLinks.forEach(topSiteWithRelatedLink => {
-      let relatedLinksMap = this._relatedLinks.get(topSiteWithRelatedLink);
-      relatedLinksMap.forEach((relatedLink, url) => {
-        possibleLinks.set(url, relatedLink);
+    this._topSitesWithSuggestedLinks.forEach(topSiteWithSuggestedLink => {
+      let suggestedLinksMap = this._suggestedLinks.get(topSiteWithSuggestedLink);
+      suggestedLinksMap.forEach((suggestedLink, url) => {
+        // Skip this link if we've shown it too many times already
+        if (this._frequencyCaps.get(url) <= 0) {
+          return;
+        }
+
+        possibleLinks.set(url, suggestedLink);
 
         // Keep a map of URL to targeted sites. We later use this to show the user
         // what site they visited to trigger this suggestion.
         if (!targetedSites.get(url)) {
           targetedSites.set(url, []);
         }
-        targetedSites.get(url).push(topSiteWithRelatedLink);
+        targetedSites.get(url).push(topSiteWithSuggestedLink);
       })
     });
+
+    // We might have run out of possible links to show
+    let numLinks = possibleLinks.size;
+    if (numLinks == 0) {
+      return;
+    }
+
     let flattenedLinks = [...possibleLinks.values()];
 
-    // Choose our related link at random
-    let relatedIndex = Math.floor(Math.random() * flattenedLinks.length);
-    let chosenRelatedLink = flattenedLinks[relatedIndex];
+    // Choose our suggested link at random
+    let suggestedIndex = Math.floor(Math.random() * numLinks);
+    let chosenSuggestedLink = flattenedLinks[suggestedIndex];
 
     // Show the new directory tile.
     this._callObservers("onLinkChanged", {
-      url: chosenRelatedLink.url,
-      title: chosenRelatedLink.title,
-      frecency: RELATED_FRECENCY,
-      lastVisitDate: chosenRelatedLink.lastVisitDate,
-      type: chosenRelatedLink.type,
+      url: chosenSuggestedLink.url,
+      title: chosenSuggestedLink.title,
+      frecency: SUGGESTED_FRECENCY,
+      lastVisitDate: chosenSuggestedLink.lastVisitDate,
+      type: chosenSuggestedLink.type,
 
       // Choose the first site a user has visited as the target. In the future,
       // this should be the site with the highest frecency. However, we currently
       // store frecency by URL not by site.
-      targetedSite: targetedSites.get(chosenRelatedLink.url).length ?
-        targetedSites.get(chosenRelatedLink.url)[0] : null
+      targetedSite: targetedSites.get(chosenSuggestedLink.url).length ?
+        targetedSites.get(chosenSuggestedLink.url)[0] : null
     });
-    return chosenRelatedLink;
+    return chosenSuggestedLink;
    },
 
   /**
    * Return the object to its pre-init state
    */
   reset: function DirectoryLinksProvider_reset() {
     delete this.__linksURL;
     this._removePrefsObserver();
@@ -619,21 +677,21 @@ let DirectoryLinksProvider = {
   addObserver: function DirectoryLinksProvider_addObserver(aObserver) {
     this._observers.add(aObserver);
   },
 
   removeObserver: function DirectoryLinksProvider_removeObserver(aObserver) {
     this._observers.delete(aObserver);
   },
 
-  _callObservers: function DirectoryLinksProvider__callObservers(aMethodName, aArg) {
+  _callObservers(methodName, ...args) {
     for (let obs of this._observers) {
-      if (typeof(obs[aMethodName]) == "function") {
+      if (typeof(obs[methodName]) == "function") {
         try {
-          obs[aMethodName](this, aArg);
+          obs[methodName](this, ...args);
         } catch (err) {
           Cu.reportError(err);
         }
       }
     }
   },
 
   _removeObservers: function() {
--- a/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js
+++ b/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js
@@ -21,16 +21,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 
 do_get_profile();
 
 const DIRECTORY_LINKS_FILE = "directoryLinks.json";
 const DIRECTORY_FRECENCY = 1000;
+const SUGGESTED_FRECENCY = Infinity;
 const kURLData = {"directory": [{"url":"http://example.com","title":"LocalSource"}]};
 const kTestURL = 'data:application/json,' + JSON.stringify(kURLData);
 
 // DirectoryLinksProvider preferences
 const kLocalePref = DirectoryLinksProvider._observedPrefs.prefSelectedLocale;
 const kSourceUrlPref = DirectoryLinksProvider._observedPrefs.linksURL;
 const kPingUrlPref = "browser.newtabpage.directory.ping";
 const kNewtabEnhancedPref = "browser.newtabpage.enhanced";
@@ -56,50 +57,50 @@ const kHttpHandlerData = {};
 kHttpHandlerData[kExamplePath] = {"directory": [{"url":"http://example.com","title":"RemoteSource"}]};
 
 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
                               "nsIBinaryInputStream",
                               "setInputStream");
 
 let gLastRequestPath;
 
-let relatedTile1 = {
+let suggestedTile1 = {
   url: "http://turbotax.com",
   type: "affiliate",
   lastVisitDate: 3,
   frecent_sites: [
     "taxact.com",
     "hrblock.com",
     "1040.com",
     "taxslayer.com"
   ]
 };
-let relatedTile2 = {
+let suggestedTile2 = {
   url: "http://irs.gov",
   type: "affiliate",
   lastVisitDate: 2,
   frecent_sites: [
     "taxact.com",
     "hrblock.com",
     "freetaxusa.com",
     "taxslayer.com"
   ]
 };
-let relatedTile3 = {
+let suggestedTile3 = {
   url: "http://hrblock.com",
   type: "affiliate",
   lastVisitDate: 1,
   frecent_sites: [
     "taxact.com",
     "freetaxusa.com",
     "1040.com",
     "taxslayer.com"
   ]
 };
-let someOtherSite = {url: "http://someothersite.com", title: "Not_A_Related_Site"};
+let someOtherSite = {url: "http://someothersite.com", title: "Not_A_Suggested_Site"};
 
 function getHttpHandler(path) {
   let code = 200;
   let body = JSON.stringify(kHttpHandlerData[path]);
   if (path == kFailPath) {
     code = 204;
   }
   return function(aRequest, aResponse) {
@@ -209,21 +210,21 @@ function run_test() {
     DirectoryLinksProvider.reset();
     Services.prefs.clearUserPref(kLocalePref);
     Services.prefs.clearUserPref(kSourceUrlPref);
     Services.prefs.clearUserPref(kPingUrlPref);
     Services.prefs.clearUserPref(kNewtabEnhancedPref);
   });
 }
 
-add_task(function test_updateRelatedTile() {
+add_task(function test_updateSuggestedTile() {
   let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"];
 
   // Initial setup
-  let data = {"suggested": [relatedTile1, relatedTile2, relatedTile3], "directory": [someOtherSite]};
+  let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]};
   let dataURI = 'data:application/json,' + JSON.stringify(data);
 
   let testObserver = new TestFirstRun();
   DirectoryLinksProvider.addObserver(testObserver);
 
   yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
   let links = yield fetchData();
 
@@ -232,201 +233,337 @@ add_task(function test_updateRelatedTile
     return topSites.indexOf(site) >= 0;
   }
 
   let origGetProviderLinks = NewTabUtils.getProviderLinks;
   NewTabUtils.getProviderLinks = function(provider) {
     return links;
   }
 
-  do_check_eq(DirectoryLinksProvider._updateRelatedTile(), undefined);
+  do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined);
 
   function TestFirstRun() {
     this.promise = new Promise(resolve => {
       this.onLinkChanged = (directoryLinksProvider, link) => {
         links.unshift(link);
-        let possibleLinks = [relatedTile1.url, relatedTile2.url, relatedTile3.url];
+        let possibleLinks = [suggestedTile1.url, suggestedTile2.url, suggestedTile3.url];
 
-        isIdentical([...DirectoryLinksProvider._topSitesWithRelatedLinks], ["hrblock.com", "1040.com", "freetaxusa.com"]);
+        isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "1040.com", "freetaxusa.com"]);
         do_check_true(possibleLinks.indexOf(link.url) > -1);
-        do_check_eq(link.frecency, Infinity);
+        do_check_eq(link.frecency, SUGGESTED_FRECENCY);
         do_check_eq(link.type, "affiliate");
         resolve();
       };
     });
   }
 
-  function TestChangingRelatedTile() {
+  function TestChangingSuggestedTile() {
     this.count = 0;
     this.promise = new Promise(resolve => {
       this.onLinkChanged = (directoryLinksProvider, link) => {
         this.count++;
-        let possibleLinks = [relatedTile1.url, relatedTile2.url, relatedTile3.url];
+        let possibleLinks = [suggestedTile1.url, suggestedTile2.url, suggestedTile3.url];
 
         do_check_true(possibleLinks.indexOf(link.url) > -1);
         do_check_eq(link.type, "affiliate");
         do_check_true(this.count <= 2);
 
         if (this.count == 1) {
-          // The removed related link is the one we added initially.
+          // The removed suggested link is the one we added initially.
           do_check_eq(link.url, links.shift().url);
-          do_check_eq(link.frecency, 0);
+          do_check_eq(link.frecency, SUGGESTED_FRECENCY);
         } else {
           links.unshift(link);
-          do_check_eq(link.frecency, Infinity);
+          do_check_eq(link.frecency, SUGGESTED_FRECENCY);
         }
-        isIdentical([...DirectoryLinksProvider._topSitesWithRelatedLinks], ["hrblock.com", "freetaxusa.com"]);
+        isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "freetaxusa.com"]);
         resolve();
       }
     });
   }
 
-  function TestRemovingRelatedTile() {
+  function TestRemovingSuggestedTile() {
     this.count = 0;
     this.promise = new Promise(resolve => {
       this.onLinkChanged = (directoryLinksProvider, link) => {
         this.count++;
 
         do_check_eq(link.type, "affiliate");
         do_check_eq(this.count, 1);
-        do_check_eq(link.frecency, 0);
+        do_check_eq(link.frecency, SUGGESTED_FRECENCY);
         do_check_eq(link.url, links.shift().url);
-        isIdentical([...DirectoryLinksProvider._topSitesWithRelatedLinks], []);
+        isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], []);
         resolve();
       }
     });
   }
 
-  // Test first call to '_updateRelatedTile()', called when fetching directory links.
+  // Test first call to '_updateSuggestedTile()', called when fetching directory links.
   yield testObserver.promise;
   DirectoryLinksProvider.removeObserver(testObserver);
 
-  // Removing a top site that doesn't have a related link should
-  // not change the current related tile.
+  // Removing a top site that doesn't have a suggested link should
+  // not change the current suggested tile.
   let removedTopsite = topSites.shift();
   do_check_eq(removedTopsite, "site0.com");
   do_check_false(NewTabUtils.isTopPlacesSite(removedTopsite));
-  let updateRelatedTile = DirectoryLinksProvider._handleLinkChanged({
+  let updateSuggestedTile = DirectoryLinksProvider._handleLinkChanged({
     url: "http://" + removedTopsite,
     type: "history",
   });
-  do_check_false(updateRelatedTile);
+  do_check_false(updateSuggestedTile);
 
-  // Removing a top site that has a related link should
-  // remove any current related tile and add a new one.
-  testObserver = new TestChangingRelatedTile();
+  // Removing a top site that has a suggested link should
+  // remove any current suggested tile and add a new one.
+  testObserver = new TestChangingSuggestedTile();
   DirectoryLinksProvider.addObserver(testObserver);
   removedTopsite = topSites.shift();
   do_check_eq(removedTopsite, "1040.com");
   do_check_false(NewTabUtils.isTopPlacesSite(removedTopsite));
   DirectoryLinksProvider.onLinkChanged(DirectoryLinksProvider, {
     url: "http://" + removedTopsite,
     type: "history",
   });
   yield testObserver.promise;
   do_check_eq(testObserver.count, 2);
   DirectoryLinksProvider.removeObserver(testObserver);
 
-  // Removing all top sites with related links should remove
-  // the current related link and not replace it.
+  // Removing all top sites with suggested links should remove
+  // the current suggested link and not replace it.
   topSites = [];
-  testObserver = new TestRemovingRelatedTile();
+  testObserver = new TestRemovingSuggestedTile();
   DirectoryLinksProvider.addObserver(testObserver);
   DirectoryLinksProvider.onManyLinksChanged();
   yield testObserver.promise;
 
   // Cleanup
   yield promiseCleanDirectoryLinksProvider();
   NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
   NewTabUtils.getProviderLinks = origGetProviderLinks;
 });
 
-add_task(function test_relatedLinksMap() {
-  let data = {"suggested": [relatedTile1, relatedTile2, relatedTile3], "directory": [someOtherSite]};
+add_task(function test_suggestedLinksMap() {
+  let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]};
   let dataURI = 'data:application/json,' + JSON.stringify(data);
 
   yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
   let links = yield fetchData();
 
-  // Ensure the related tiles were not considered directory tiles.
+  // Ensure the suggested tiles were not considered directory tiles.
   do_check_eq(links.length, 1);
-  let expected_data = [{url: "http://someothersite.com", title: "Not_A_Related_Site", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}];
+  let expected_data = [{url: "http://someothersite.com", title: "Not_A_Suggested_Site", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}];
   isIdentical(links, expected_data);
 
-  // Check for correctly saved related tiles data.
+  // Check for correctly saved suggested tiles data.
   expected_data = {
-    "taxact.com": [relatedTile1, relatedTile2, relatedTile3],
-    "hrblock.com": [relatedTile1, relatedTile2],
-    "1040.com": [relatedTile1, relatedTile3],
-    "taxslayer.com": [relatedTile1, relatedTile2, relatedTile3],
-    "freetaxusa.com": [relatedTile2, relatedTile3],
+    "taxact.com": [suggestedTile1, suggestedTile2, suggestedTile3],
+    "hrblock.com": [suggestedTile1, suggestedTile2],
+    "1040.com": [suggestedTile1, suggestedTile3],
+    "taxslayer.com": [suggestedTile1, suggestedTile2, suggestedTile3],
+    "freetaxusa.com": [suggestedTile2, suggestedTile3],
   };
 
-  DirectoryLinksProvider._relatedLinks.forEach((relatedLinks, site) => {
-    let relatedLinksItr = relatedLinks.values();
+  DirectoryLinksProvider._suggestedLinks.forEach((suggestedLinks, site) => {
+    let suggestedLinksItr = suggestedLinks.values();
     for (let link of expected_data[site]) {
-      isIdentical(relatedLinksItr.next().value, link);
+      isIdentical(suggestedLinksItr.next().value, link);
     }
   })
 
   yield promiseCleanDirectoryLinksProvider();
 });
 
-add_task(function test_topSitesWithRelatedLinks() {
+add_task(function test_topSitesWithSuggestedLinks() {
   let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"];
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = function(site) {
     return topSites.indexOf(site) >= 0;
   }
 
   // Mock out getProviderLinks() so we don't have to populate cache in NewTabUtils
   let origGetProviderLinks = NewTabUtils.getProviderLinks;
   NewTabUtils.getProviderLinks = function(provider) {
     return [];
   }
 
-  // We start off with no top sites with related links.
-  do_check_eq(DirectoryLinksProvider._topSitesWithRelatedLinks.size, 0);
+  // We start off with no top sites with suggested links.
+  do_check_eq(DirectoryLinksProvider._topSitesWithSuggestedLinks.size, 0);
 
-  let data = {"suggested": [relatedTile1, relatedTile2, relatedTile3], "directory": [someOtherSite]};
+  let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]};
   let dataURI = 'data:application/json,' + JSON.stringify(data);
 
   yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
   let links = yield fetchData();
 
-  // Check we've populated related links as expected.
-  do_check_eq(DirectoryLinksProvider._relatedLinks.size, 5);
+  // Check we've populated suggested links as expected.
+  do_check_eq(DirectoryLinksProvider._suggestedLinks.size, 5);
 
-  // When many sites change, we update _topSitesWithRelatedLinks as expected.
-  let expectedTopSitesWithRelatedLinks = ["hrblock.com", "1040.com", "freetaxusa.com"];
+  // When many sites change, we update _topSitesWithSuggestedLinks as expected.
+  let expectedTopSitesWithSuggestedLinks = ["hrblock.com", "1040.com", "freetaxusa.com"];
   DirectoryLinksProvider._handleManyLinksChanged();
-  isIdentical([...DirectoryLinksProvider._topSitesWithRelatedLinks], expectedTopSitesWithRelatedLinks);
+  isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks);
 
-  // Removing site6.com as a topsite has no impact on _topSitesWithRelatedLinks.
+  // Removing site6.com as a topsite has no impact on _topSitesWithSuggestedLinks.
   let popped = topSites.pop();
   DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped});
-  isIdentical([...DirectoryLinksProvider._topSitesWithRelatedLinks], expectedTopSitesWithRelatedLinks);
+  isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks);
 
-  // Removing freetaxusa.com as a topsite will remove it from _topSitesWithRelatedLinks.
+  // Removing freetaxusa.com as a topsite will remove it from _topSitesWithSuggestedLinks.
   popped = topSites.pop();
-  expectedTopSitesWithRelatedLinks.pop();
+  expectedTopSitesWithSuggestedLinks.pop();
   DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped});
-  isIdentical([...DirectoryLinksProvider._topSitesWithRelatedLinks], expectedTopSitesWithRelatedLinks);
+  isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks);
 
-  // Re-adding freetaxusa.com as a topsite will add it to _topSitesWithRelatedLinks.
+  // Re-adding freetaxusa.com as a topsite will add it to _topSitesWithSuggestedLinks.
   topSites.push(popped);
-  expectedTopSitesWithRelatedLinks.push(popped);
+  expectedTopSitesWithSuggestedLinks.push(popped);
   DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped});
-  isIdentical([...DirectoryLinksProvider._topSitesWithRelatedLinks], expectedTopSitesWithRelatedLinks);
+  isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks);
 
   // Cleanup.
   NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
   NewTabUtils.getProviderLinks = origGetProviderLinks;
 });
 
+add_task(function test_frequencyCappedSites_views() {
+  Services.prefs.setCharPref(kPingUrlPref, "");
+  let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
+  NewTabUtils.isTopPlacesSite = () => true;
+
+  let testUrl = "http://frequency.capped/link";
+  let targets = ["top.site.com"];
+  let data = {
+    suggested: [{
+      type: "sponsored",
+      frecent_sites: targets,
+      url: testUrl
+    }],
+    directory: [{
+      type: "organic",
+      url: "http://directory.site/"
+    }]
+  };
+  let dataURI = "data:application/json," + JSON.stringify(data);
+
+  yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
+
+  // Wait for links to get loaded
+  let gLinks = NewTabUtils.links;
+  gLinks.addProvider(DirectoryLinksProvider);
+  gLinks.populateCache();
+  yield new Promise(resolve => {
+    NewTabUtils.allPages.register({
+      observe: _ => _,
+      update() {
+        NewTabUtils.allPages.unregister(this);
+        resolve();
+      }
+    });
+  });
+
+  function synthesizeAction(action) {
+    DirectoryLinksProvider.reportSitesAction([{
+      link: {
+        targetedSite: targets[0],
+        url: testUrl
+      }
+    }], action, 0);
+  }
+
+  function checkFirstTypeAndLength(type, length) {
+    let links = gLinks.getLinks();
+    do_check_eq(links[0].type, type);
+    do_check_eq(links.length, length);
+  }
+
+  // Make sure we get 5 views of the link before it is removed
+  checkFirstTypeAndLength("sponsored", 2);
+  synthesizeAction("view");
+  checkFirstTypeAndLength("sponsored", 2);
+  synthesizeAction("view");
+  checkFirstTypeAndLength("sponsored", 2);
+  synthesizeAction("view");
+  checkFirstTypeAndLength("sponsored", 2);
+  synthesizeAction("view");
+  checkFirstTypeAndLength("sponsored", 2);
+  synthesizeAction("view");
+  checkFirstTypeAndLength("organic", 1);
+
+  // Cleanup.
+  NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
+  gLinks.removeProvider(DirectoryLinksProvider);
+  DirectoryLinksProvider.removeObserver(gLinks);
+  Services.prefs.setCharPref(kPingUrlPref, kPingUrl);
+});
+
+add_task(function test_frequencyCappedSites_click() {
+  Services.prefs.setCharPref(kPingUrlPref, "");
+  let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
+  NewTabUtils.isTopPlacesSite = () => true;
+
+  let testUrl = "http://frequency.capped/link";
+  let targets = ["top.site.com"];
+  let data = {
+    suggested: [{
+      type: "sponsored",
+      frecent_sites: targets,
+      url: testUrl
+    }],
+    directory: [{
+      type: "organic",
+      url: "http://directory.site/"
+    }]
+  };
+  let dataURI = "data:application/json," + JSON.stringify(data);
+
+  yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
+
+  // Wait for links to get loaded
+  let gLinks = NewTabUtils.links;
+  gLinks.addProvider(DirectoryLinksProvider);
+  gLinks.populateCache();
+  yield new Promise(resolve => {
+    NewTabUtils.allPages.register({
+      observe: _ => _,
+      update() {
+        NewTabUtils.allPages.unregister(this);
+        resolve();
+      }
+    });
+  });
+
+  function synthesizeAction(action) {
+    DirectoryLinksProvider.reportSitesAction([{
+      link: {
+        targetedSite: targets[0],
+        url: testUrl
+      }
+    }], action, 0);
+  }
+
+  function checkFirstTypeAndLength(type, length) {
+    let links = gLinks.getLinks();
+    do_check_eq(links[0].type, type);
+    do_check_eq(links.length, length);
+  }
+
+  // Make sure the link disappears after the first click
+  checkFirstTypeAndLength("sponsored", 2);
+  synthesizeAction("view");
+  checkFirstTypeAndLength("sponsored", 2);
+  synthesizeAction("click");
+  checkFirstTypeAndLength("organic", 1);
+
+  // Cleanup.
+  NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
+  gLinks.removeProvider(DirectoryLinksProvider);
+  DirectoryLinksProvider.removeObserver(gLinks);
+  Services.prefs.setCharPref(kPingUrlPref, kPingUrl);
+});
+
 add_task(function test_reportSitesAction() {
   yield DirectoryLinksProvider.init();
   let deferred, expectedPath, expectedPost;
   let done = false;
   server.registerPrefixHandler(kPingPath, (aRequest, aResponse) => {
     if (done) {
       return;
     }
--- a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp
+++ b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp
@@ -223,16 +223,17 @@ BluetoothHfpManager::Cleanup()
   mSignal = 0;
 
   mController = nullptr;
 }
 
 void
 BluetoothHfpManager::Reset()
 {
+  mFirstCKPD = false;
   // Phone & Device CIND
   ResetCallArray();
   // Clear Sco state
   mAudioState = HFP_AUDIO_STATE_DISCONNECTED;
   Cleanup();
 }
 
 bool
@@ -1281,16 +1282,18 @@ BluetoothHfpManager::Disconnect(Bluetoot
                                      new DisconnectResultHandler(this));
 }
 
 void
 BluetoothHfpManager::OnConnect(const nsAString& aErrorStr)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  mFirstCKPD = true;
+
   /**
    * On the one hand, notify the controller that we've done for outbound
    * connections. On the other hand, we do nothing for inbound connections.
    */
   NS_ENSURE_TRUE_VOID(mController);
 
   mController->NotifyCompletion(aErrorStr);
   mController = nullptr;
@@ -1608,39 +1611,49 @@ BluetoothHfpManager::KeyPressedNotificat
   MOZ_ASSERT(NS_IsMainThread());
 
   bool hasActiveCall =
     (FindFirstCall(nsITelephonyService::CALL_STATE_CONNECTED) > 0);
 
   // Refer to AOSP HeadsetStateMachine.processKeyPressed
   if (FindFirstCall(nsITelephonyService::CALL_STATE_INCOMING)
       && !hasActiveCall) {
-    /**
+    /*
      * Bluetooth HSP spec 4.2.2
      * There is an incoming call, notify Dialer to pick up the phone call
      * and SCO will be established after we get the CallStateChanged event
      * indicating the call is answered successfully.
      */
     NotifyDialer(NS_LITERAL_STRING("ATA"));
   } else if (hasActiveCall) {
     if (!IsScoConnected()) {
-      /**
+      /*
        * Bluetooth HSP spec 4.3
        * If there's no SCO, set up a SCO link.
        */
       ConnectSco();
-    } else {
-      /**
+    } else if (mFirstCKPD) {
+      /*
+       * Bluetooth HSP spec 4.2 & 4.3
+       * The SCO link connection may be set up prior to receiving the AT+CKPD=200
+       * command from the HS.
+       *
+       * Once FxOS initiates a SCO connection before receiving the
+       * AT+CKPD=200, we should ignore this key press.
+       */
+     } else {
+      /*
        * Bluetooth HSP spec 4.5
        * There are two ways to release SCO: sending CHUP to dialer or closing
        * SCO socket directly. We notify dialer only if there is at least one
        * active call.
        */
       NotifyDialer(NS_LITERAL_STRING("CHUP"));
     }
+    mFirstCKPD = false;
   } else {
     // BLDN
     mDialingRequestProcessed = false;
 
     NotifyDialer(NS_LITERAL_STRING("BLDN"));
 
     MessageLoop::current()->PostDelayedTask(FROM_HERE,
                                             new RespondToBLDNTask(),
--- a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.h
+++ b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.h
@@ -204,16 +204,17 @@ private:
   BluetoothHandsfreeNetworkState mService;
   BluetoothHandsfreeServiceType mRoam;
   int mSignal;
 
   int mCurrentVgs;
   int mCurrentVgm;
   bool mReceiveVgsFlag;
   bool mDialingRequestProcessed;
+  bool mFirstCKPD;
   PhoneType mPhoneType;
   nsString mDeviceAddress;
   nsString mMsisdn;
   nsString mOperatorName;
 
   nsTArray<Call> mCurrentCallArray;
   nsAutoPtr<BluetoothRilListener> mListener;
   nsRefPtr<BluetoothProfileController> mController;
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1,45 +1,32 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.net.URLEncoder;
-import java.util.EnumSet;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Vector;
-
-import org.json.JSONException;
-import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.PinReason;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.TransitionsTracker;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
+import org.mozilla.gecko.firstrun.FirstrunPane;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerMarginsAnimator;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.health.BrowserHealthRecorder;
 import org.mozilla.gecko.health.BrowserHealthReporter;
@@ -52,28 +39,28 @@ import org.mozilla.gecko.home.HomePager.
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.home.SearchEngine;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import org.mozilla.gecko.mozglue.RobocopTarget;
-import org.mozilla.gecko.firstrun.FirstrunPane;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.prompts.Prompt;
 import org.mozilla.gecko.prompts.PromptListItem;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
+import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabs.TabHistoryController;
+import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
 import org.mozilla.gecko.tabs.TabsPanel;
-import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GamepadUtils;
@@ -134,16 +121,30 @@ import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.Window;
 import android.view.animation.Interpolator;
 import android.widget.ListView;
 import android.widget.RelativeLayout;
 import android.widget.Toast;
 import android.widget.ViewFlipper;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URLEncoder;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Vector;
 
 public class BrowserApp extends GeckoApp
                         implements TabsPanel.TabsLayoutChangeListener,
                                    PropertyAnimator.PropertyAnimationListener,
                                    View.OnKeyListener,
                                    LayerView.OnMetricsChangedListener,
                                    BrowserSearch.OnSearchListener,
                                    BrowserSearch.OnEditSuggestionListener,
@@ -900,16 +901,29 @@ public class BrowserApp extends GeckoApp
     }
 
     @Override
     public void onAttachedToWindow() {
         // We can't show the first run experience until Gecko has finished initialization (bug 1077583).
         checkFirstrun(this, new SafeIntent(getIntent()));
     }
 
+    private void processTabQueue() {
+        if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE) {
+            ThreadUtils.postToBackgroundThread(new Runnable() {
+                @Override
+                public void run() {
+                    if (TabQueueHelper.shouldOpenTabQueueUrls(BrowserApp.this)) {
+                        TabQueueHelper.openQueuedUrls(BrowserApp.this, mProfile, TabQueueHelper.FILE_NAME);
+                    }
+                }
+            });
+        }
+    }
+
     @Override
     public void onResume() {
         super.onResume();
 
         final String args = ContextUtils.getStringExtra(getIntent(), "args");
         // If an external intent tries to start Fennec in guest mode, and it's not already
         // in guest mode, this will change modes before opening the url.
         // NOTE: OnResume is called twice sometimes when showing on the lock screen.
@@ -918,16 +932,18 @@ public class BrowserApp extends GeckoApp
         if (enableGuestSession != inGuestSession) {
             doRestart(getIntent());
             GeckoAppShell.gracefulExit();
             return;
         }
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
             "Prompt:ShowTop");
+
+        processTabQueue();
     }
 
     @Override
     public void onPause() {
         super.onPause();
         // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
         EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this,
             "Prompt:ShowTop");
@@ -3375,16 +3391,17 @@ public class BrowserApp extends GeckoApp
      * open a new tab with about:feedback when launching the app from the icon shortcut.
      */
     @Override
     protected void onNewIntent(Intent intent) {
         String action = intent.getAction();
 
         final boolean isViewAction = Intent.ACTION_VIEW.equals(action);
         final boolean isBookmarkAction = GeckoApp.ACTION_HOMESCREEN_SHORTCUT.equals(action);
+        final boolean isTabQueueAction = TabQueueHelper.LOAD_URLS_ACTION.equals(action);
 
         if (mInitialized && (isViewAction || isBookmarkAction)) {
             // Dismiss editing mode if the user is loading a URL from an external app.
             mBrowserToolbar.cancelEdit();
 
             // Hide firstrun-pane if the user is loading a URL from an external app.
             hideFirstrunPager();
 
@@ -3403,16 +3420,27 @@ public class BrowserApp extends GeckoApp
             GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri));
         }
 
         // Only solicit feedback when the app has been launched from the icon shortcut.
         if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
             GuestSession.handleIntent(this, intent);
         }
 
+        // If the user has clicked the tab queue notification then load the tabs.
+        if(AppConstants.NIGHTLY_BUILD  && AppConstants.MOZ_ANDROID_TAB_QUEUE && mInitialized && isTabQueueAction) {
+            int queuedTabCount = TabQueueHelper.getTabQueueLength(this);
+            TabQueueHelper.openQueuedUrls(this, mProfile, TabQueueHelper.FILE_NAME);
+
+            // If there's more than one tab then also show the tabs panel.
+            if (queuedTabCount > 1) {
+                showNormalTabs();
+            }
+        }
+
         if (!mInitialized || !Intent.ACTION_MAIN.equals(action)) {
             return;
         }
 
         // Check to see how many times the app has been launched.
         final String keyName = getPackageName() + ".feedback_launch_count";
         final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
 
--- a/mobile/android/base/DoorHangerPopup.java
+++ b/mobile/android/base/DoorHangerPopup.java
@@ -17,16 +17,17 @@ import org.mozilla.gecko.util.GeckoEvent
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.AnchoredPopup;
 import org.mozilla.gecko.widget.DoorHanger;
 
 import android.content.Context;
 import android.util.Log;
 import android.view.View;
 import android.widget.CheckBox;
+import org.mozilla.gecko.widget.DoorhangerConfig;
 
 public class DoorHangerPopup extends AnchoredPopup
                              implements GeckoEventListener,
                                         Tabs.OnTabsChangedListener,
                                         DoorHanger.OnButtonClickListener {
     private static final String LOGTAG = "GeckoDoorHangerPopup";
 
     // Stores a set of all active DoorHanger notifications. A DoorHanger is
@@ -71,26 +72,22 @@ public class DoorHangerPopup extends Anc
         mDisabled = false;
         updatePopup();
     }
 
     @Override
     public void handleMessage(String event, JSONObject geckoObject) {
         try {
             if (event.equals("Doorhanger:Add")) {
-                final int tabId = geckoObject.getInt("tabID");
-                final String value = geckoObject.getString("value");
-                final String message = geckoObject.getString("message");
-                final JSONArray buttons = geckoObject.getJSONArray("buttons");
-                final JSONObject options = geckoObject.getJSONObject("options");
+                final DoorhangerConfig config = makeConfigFromJSON(geckoObject);
 
                 ThreadUtils.postToUiThread(new Runnable() {
                     @Override
                     public void run() {
-                        addDoorHanger(tabId, value, message, buttons, options);
+                        addDoorHanger(config);
                     }
                 });
             } else if (event.equals("Doorhanger:Remove")) {
                 final int tabId = geckoObject.getInt("tabID");
                 final String value = geckoObject.getString("value");
 
                 ThreadUtils.postToUiThread(new Runnable() {
                     @Override
@@ -104,16 +101,32 @@ public class DoorHangerPopup extends Anc
                     }
                 });
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
+    private DoorhangerConfig makeConfigFromJSON(JSONObject json) throws JSONException {
+        final int tabId = json.getInt("tabID");
+        final String id = json.getString("value");
+        final DoorhangerConfig config = new DoorhangerConfig(tabId, id);
+
+        config.setMessage(json.getString("message"));
+        config.setButtons(json.getJSONArray("buttons"));
+        config.setOptions(json.getJSONObject("options"));
+        final String typeString = json.optString("category");
+        if (DoorHanger.Type.LOGIN.toString().equals(typeString)) {
+            config.setType(DoorHanger.Type.LOGIN);
+        }
+
+        return config;
+    }
+
     // This callback is automatically executed on the UI thread.
     @Override
     public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) {
         switch(msg) {
             case CLOSED:
                 // Remove any doorhangers for a tab when it's closed (make
                 // a temporary set to avoid a ConcurrentModificationException)
                 HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>();
@@ -144,37 +157,36 @@ public class DoorHangerPopup extends Anc
         }
     }
 
     /**
      * Adds a doorhanger.
      *
      * This method must be called on the UI thread.
      */
-    void addDoorHanger(final int tabId, final String value, final String message,
-                       final JSONArray buttons, final JSONObject options) {
+    void addDoorHanger(DoorhangerConfig config) {
+        final int tabId = config.getTabId();
         // Don't add a doorhanger for a tab that doesn't exist
         if (Tabs.getInstance().getTab(tabId) == null) {
             return;
         }
 
         // Replace the doorhanger if it already exists
-        DoorHanger oldDoorHanger = getDoorHanger(tabId, value);
+        DoorHanger oldDoorHanger = getDoorHanger(tabId, config.getId());
         if (oldDoorHanger != null) {
             removeDoorHanger(oldDoorHanger);
         }
 
         if (!mInflated) {
             init();
         }
 
-        final DoorHanger newDoorHanger = new DoorHanger(mContext, tabId, value);
-        newDoorHanger.setMessage(message);
-        newDoorHanger.setOptions(options);
+        final DoorHanger newDoorHanger = DoorHanger.Get(mContext, config);
 
+        final JSONArray buttons = config.getButtons();
         for (int i = 0; i < buttons.length(); i++) {
             try {
                 JSONObject buttonObject = buttons.getJSONObject(i);
                 String label = buttonObject.getString("label");
                 String tag = String.valueOf(buttonObject.getInt("callback"));
                 newDoorHanger.addButton(label, tag, this);
             } catch (JSONException e) {
                 Log.e(LOGTAG, "Error creating doorhanger button", e);
@@ -225,17 +237,17 @@ public class DoorHangerPopup extends Anc
 
     /**
      * Gets a doorhanger.
      *
      * This method must be called on the UI thread.
      */
     DoorHanger getDoorHanger(int tabId, String value) {
         for (DoorHanger dh : mDoorHangers) {
-            if (dh.getTabId() == tabId && dh.getValue().equals(value))
+            if (dh.getTabId() == tabId && dh.getIdentifier().equals(value))
                 return dh;
         }
 
         // If there's no doorhanger for the given tabId and value, return null
         return null;
     }
 
     /**
--- a/mobile/android/base/GeckoProfile.java
+++ b/mobile/android/base/GeckoProfile.java
@@ -22,16 +22,17 @@ import java.util.regex.Pattern;
 import org.json.JSONException;
 import org.json.JSONArray;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.LocalBrowserDB;
 import org.mozilla.gecko.db.StubBrowserDB;
 import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.firstrun.FirstrunPane;
 import org.mozilla.gecko.util.INIParser;
 import org.mozilla.gecko.util.INISection;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -148,17 +149,17 @@ public final class GeckoProfile {
             final GeckoApp geckoApp = (GeckoApp) context;
             if (geckoApp.mProfile != null) {
                 return geckoApp.mProfile;
             }
         }
 
         final String args;
         if (context instanceof Activity) {
-            args = ((Activity) context).getIntent().getStringExtra("args");
+            args = ContextUtils.getStringExtra(((Activity) context).getIntent(), "args");
         } else {
             args = null;
         }
 
         if (GuestSession.shouldUse(context, args)) {
             final GeckoProfile p = GeckoProfile.getOrCreateGuestProfile(context);
             if (isGeckoApp) {
                 ((GeckoApp) context).mProfile = p;
@@ -674,16 +675,24 @@ public final class GeckoProfile {
                 read = fr.read(buf);
             }
             return sb.toString();
         } finally {
             fr.close();
         }
     }
 
+    public boolean deleteFileFromProfileDir(String fileName) throws IllegalArgumentException {
+        if (TextUtils.isEmpty(fileName)) {
+            throw new IllegalArgumentException("Filename cannot be empty.");
+        }
+        File file = new File(getDir(), fileName);
+        return file.delete();
+    }
+
     private boolean remove() {
         try {
             synchronized (this) {
                 final File dir = getDir();
                 if (dir.exists()) {
                     delete(dir);
                 }
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -484,30 +484,33 @@ gbjar.sources += [
     'widget/AllCapsTextView.java',
     'widget/AnchoredPopup.java',
     'widget/AnimatedHeightLayout.java',
     'widget/BasicColorPicker.java',
     'widget/ButtonToast.java',
     'widget/CheckableLinearLayout.java',
     'widget/ClickableWhenDisabledEditText.java',
     'widget/DateTimePicker.java',
+    'widget/DefaultDoorHanger.java',
     'widget/Divider.java',
     'widget/DoorHanger.java',
+    'widget/DoorhangerConfig.java',
     'widget/EllipsisTextView.java',
     'widget/FadedMultiColorTextView.java',
     'widget/FadedSingleColorTextView.java',
     'widget/FadedTextView.java',
     'widget/FaviconView.java',
     'widget/FloatingHintEditText.java',
     'widget/FlowLayout.java',
     'widget/GeckoActionProvider.java',
     'widget/GeckoPopupMenu.java',
     'widget/GeckoSwipeRefreshLayout.java',
     'widget/GeckoViewFlipper.java',
     'widget/IconTabWidget.java',
+    'widget/LoginDoorHanger.java',
     'widget/ResizablePathDrawable.java',
     'widget/SquaredImageView.java',
     'widget/SwipeDismissListViewTouchListener.java',
     'widget/TabThumbnailWrapper.java',
     'widget/ThumbnailView.java',
     'widget/TwoWayView.java',
     'ZoomConstraints.java',
     'ZoomedView.java',
--- a/mobile/android/base/reading/LocalReadingListStorage.java
+++ b/mobile/android/base/reading/LocalReadingListStorage.java
@@ -261,25 +261,27 @@ public class LocalReadingListStorage imp
 
   public Cursor getModifiedWithSelection(final String selection) {
     final String[] projection = new String[] {
       ReadingListItems.GUID,
       ReadingListItems.IS_FAVORITE,
       ReadingListItems.RESOLVED_TITLE,
       ReadingListItems.RESOLVED_URL,
       ReadingListItems.EXCERPT,
+      // TODO: ReadingListItems.IS_ARTICLE,
+      // TODO: ReadingListItems.WORD_COUNT,
     };
 
-
     try {
       return client.query(URI_WITHOUT_DELETED, projection, selection, null, null);
     } catch (RemoteException e) {
       throw new IllegalStateException(e);
     }
   }
+
   @Override
   public Cursor getModified() {
     final String selection = ReadingListItems.SYNC_STATUS + " = " + ReadingListItems.SYNC_STATUS_MODIFIED;
     return getModifiedWithSelection(selection);
   }
 
   // Return changed items that aren't just status changes.
   // This isn't necessary because we insist on processing status changes before modified items.
--- a/mobile/android/base/reading/ReadingListClient.java
+++ b/mobile/android/base/reading/ReadingListClient.java
@@ -477,17 +477,17 @@ public class ReadingListClient {
     final BaseResource r = getRelativeArticleResource(guid);
     r.delegate = new DelegatingUploadResourceDelegate(r, auth, ReadingListRecordResponse.FACTORY, up,
                                                       uploadDelegate);
 
     final ExtendedJSONObject body = up.toJSON();
     if (ReadingListConstants.DEBUG) {
       Logger.info(LOG_TAG, "Patching record " + guid + ": " + body.toJSONString());
     }
-    r.post(body);
+    r.patch(body);
   }
 
   /**
    * Mutates the provided queue.
    */
   public void add(final Queue<ClientReadingListRecord> queue, final Executor executor, final ReadingListRecordUploadDelegate batchUploadDelegate) {
     if (queue.isEmpty()) {
       batchUploadDelegate.onBatchDone();
--- a/mobile/android/base/reading/ReadingListConstants.java
+++ b/mobile/android/base/reading/ReadingListConstants.java
@@ -5,14 +5,14 @@
 package org.mozilla.gecko.reading;
 
 import org.mozilla.gecko.AppConstants;
 
 public class ReadingListConstants {
   public static final String GLOBAL_LOG_TAG = "FxReadingList";
   public static final String USER_AGENT = "Firefox-Android-FxReader/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_DISPLAYNAME + ")";
   public static final String DEFAULT_DEV_ENDPOINT = "https://readinglist.dev.mozaws.net/v1/";
-  public static final String DEFAULT_PROD_ENDPOINT = null;     // TODO
+  public static final String DEFAULT_PROD_ENDPOINT = "https://readinglist.services.mozilla.com/v1/";
 
   public static final String OAUTH_ENDPOINT_PROD = "https://oauth.accounts.firefox.com/v1";
 
   public static boolean DEBUG = false;
 }
--- a/mobile/android/base/reading/ReadingListSynchronizer.java
+++ b/mobile/android/base/reading/ReadingListSynchronizer.java
@@ -243,21 +243,20 @@ public class ReadingListSynchronizer {
       try {
         acc.finish();
       } catch (Exception e) {
         next.fail(e);
         return;
       }
 
       if (failures == 0) {
-        try {
-          next.next();
-        } catch (Exception e) {
-        }
+        next.next();
+        return;
       }
+
       next.fail();
     }
   }
 
   private Queue<ClientReadingListRecord> collectStatusChangesFromCursor(final Cursor cursor) {
     try {
       final Queue<ClientReadingListRecord> toUpload = new LinkedList<>();
 
@@ -300,16 +299,159 @@ public class ReadingListSynchronizer {
       }
 
       return toUpload;
     } finally {
       cursor.close();
     }
   }
 
+  private static class ModifiedUploadDelegate implements ReadingListRecordUploadDelegate {
+    private final ReadingListChangeAccumulator acc;
+
+    public volatile int failures = 0;
+    private final StageDelegate next;
+
+    ModifiedUploadDelegate(ReadingListChangeAccumulator acc, StageDelegate next) {
+      this.acc = acc;
+      this.next = next;
+    }
+
+    @Override
+    public void onInvalidUpload(ClientReadingListRecord up,
+                                ReadingListResponse response) {
+      recordFailed(up);
+    }
+
+    @Override
+    public void onConflict(ClientReadingListRecord up,
+                           ReadingListResponse response) {
+      // This can happen for a material change.
+      failures++;
+    }
+
+    @Override
+    public void onSuccess(ClientReadingListRecord up,
+                          ReadingListRecordResponse response,
+                          ServerReadingListRecord down) {
+      if (!TextUtils.equals(up.getGUID(), down.getGUID())) {
+        // Uh oh!
+        // This should never occur. We should get an onConflict instead,
+        // so this would imply a server bug, or something like a truncated
+        // over-long GUID string.
+        //
+        // Should we wish to recover from this case, probably the right approach
+        // is to ensure that the GUID is overwritten locally (given that we know
+        // the numeric ID).
+      }
+
+      // We could upload our material changes but get back additional status
+      // changes from the server.  Apply them.
+      acc.addChangedRecord(up.givenServerRecord(down));
+    }
+
+    @Override
+    public void onBadRequest(ClientReadingListRecord up, MozResponse response) {
+      recordFailed(up);
+    }
+
+    @Override
+    public void onFailure(ClientReadingListRecord up, Exception ex) {
+      recordFailed(up);
+    }
+
+    @Override
+    public void onFailure(ClientReadingListRecord up, MozResponse response) {
+      // Since we download and apply remote changes before uploading local changes, the conflict
+      // window is very small.  We should essentially never see true conflicts here.
+      if (response.getStatusCode() == 404) {
+        // We shouldn't see a 404; we should see a record with deleted=true when
+        // we fetch remote changes.
+        Logger.warn(LOG_TAG, "Ignoring 404 response patching record with guid: " + up.getGUID());
+      } else if (response.getStatusCode() == 409) {
+        // A 409 indicates that resolved_url has collided with an existing
+        // record. Not much to be done here.
+        Logger.info(LOG_TAG, "409 response seen; deleting record with guid: " + up.getGUID());
+        acc.addDeletion(up);
+      } else {
+        // We should never see a 412 since we race to upload our changes (and
+        // accept whatever the server gives us back).
+        recordFailed(up);
+      }
+    }
+
+    private void recordFailed(ClientReadingListRecord up) {
+      ++failures;
+    }
+
+    @Override
+    public void onBatchDone() {
+      try {
+        acc.finish();
+      } catch (Exception e) {
+        next.fail(e);
+        return;
+      }
+
+      if (failures == 0) {
+        next.next();
+        return;
+      }
+
+      next.fail();
+    }
+  }
+
+  private Queue<ClientReadingListRecord> collectModifiedFromCursor(final Cursor cursor) {
+    try {
+      final Queue<ClientReadingListRecord> toUpload = new LinkedList<>();
+
+      final int columnGUID = cursor.getColumnIndexOrThrow(ReadingListItems.GUID);
+      final int columnExcerpt = cursor.getColumnIndexOrThrow(ReadingListItems.EXCERPT);
+      final int columnResolvedURL = cursor.getColumnIndexOrThrow(ReadingListItems.RESOLVED_URL);
+      final int columnResolvedTitle = cursor.getColumnIndexOrThrow(ReadingListItems.RESOLVED_TITLE);
+      // TODO: final int columnIsArticle = cursor.getColumnIndexOrThrow(ReadingListItems.IS_ARTICLE);
+      // TODO: final int columnWordCount = cursor.getColumnIndexOrThrow(ReadingListItems.WORD_COUNT);
+
+      while (cursor.moveToNext()) {
+        final String guid = cursor.getString(columnGUID);
+        if (guid == null) {
+          // Nothing we can do here, but this should never happen: we should
+          // have uploaded this record as new before trying to upload a
+          // material modification!
+          continue;
+        }
+
+        final ExtendedJSONObject o = new ExtendedJSONObject();
+        o.put("id", guid);
+        final String excerpt = cursor.getString(columnExcerpt); // Can be NULL.
+        final String resolvedURL = cursor.getString(columnResolvedURL); // Can be NULL.
+        final String resolvedTitle = cursor.getString(columnResolvedTitle); // Can be NULL.
+        if (excerpt == null && resolvedURL == null && resolvedTitle == null) {
+          // Nothing material to upload, so skip this record.
+          continue;
+        }
+        o.put("excerpt", excerpt);
+        o.put("resolved_url", resolvedURL);
+        o.put("resolved_title", resolvedTitle);
+        // TODO: o.put("is_article", cursor.getInt(columnIsArticle) == 1);
+        // TODO: o.put("word_count", cursor.getInt(columnWordCount));
+
+        final ClientMetadata cm = null;
+        final ServerMetadata sm = new ServerMetadata(guid, -1L);
+        final ClientReadingListRecord record = new ClientReadingListRecord(sm, cm, o);
+        toUpload.add(record);
+      }
+
+      return toUpload;
+    } finally {
+      cursor.close();
+    }
+  }
+
   private Queue<ClientReadingListRecord> accumulateNewItems(Cursor cursor) {
     try {
       final Queue<ClientReadingListRecord> toUpload = new LinkedList<>();
       final ReadingListClientRecordFactory factory = new ReadingListClientRecordFactory(cursor);
 
       ClientReadingListRecord record;
       while ((record = factory.getNext()) != null) {
         toUpload.add(record);
@@ -330,18 +472,21 @@ public class ReadingListSynchronizer {
         delegate.fail(new RuntimeException("Unable to get unread item cursor."));
         return;
       }
 
       final Queue<ClientReadingListRecord> toUpload = collectStatusChangesFromCursor(cursor);
 
       // Nothing to do.
       if (toUpload.isEmpty()) {
+        Logger.debug(LOG_TAG, "No new unread changes to upload. Skipping.");
         delegate.next();
         return;
+      } else {
+        Logger.debug(LOG_TAG, "Uploading " + toUpload.size() + " new unread changes.");
       }
 
       // Upload each record. This looks like batching, but it's really chained serial requests.
       final ReadingListChangeAccumulator acc = this.local.getChangeAccumulator();
       final StatusUploadDelegate uploadDelegate = new StatusUploadDelegate(acc, delegate);
 
       // Don't send I-U-S; in the case of favorites we're
       // happy to overwrite the server value, and in the case of unread status
@@ -363,16 +508,18 @@ public class ReadingListSynchronizer {
 
       Queue<ClientReadingListRecord> toUpload = accumulateNewItems(cursor);
 
       // Nothing to do.
       if (toUpload.isEmpty()) {
         Logger.debug(LOG_TAG, "No new items to upload. Skipping.");
         delegate.next();
         return;
+      } else {
+        Logger.debug(LOG_TAG, "Uploading " + toUpload.size() + " new items.");
       }
 
       final ReadingListChangeAccumulator acc = this.local.getChangeAccumulator();
       final NewItemUploadDelegate uploadDelegate = new NewItemUploadDelegate(acc, new StageDelegate() {
         private boolean tryFlushChanges() {
           Logger.debug(LOG_TAG, "Flushing post-upload changes.");
           try {
             acc.finish();
@@ -415,19 +562,89 @@ public class ReadingListSynchronizer {
       // ... we need to apply it locally.
       this.remote.add(toUpload, executor, uploadDelegate);
     } catch (Exception e) {
       delegate.fail(e);
       return;
     }
   }
 
-  private void uploadModified(final StageDelegate delegate) {
-    // TODO
-    delegate.next();
+  protected void uploadModified(final StageDelegate delegate) {
+    try {
+      // This looks strange because modified includes material changes and
+      // status changes, but this is called after status changes have been
+      // uploaded and removed from local storage. So what's left should be
+      // material changes.  Even so, it should be safe to upload status changes
+      // here.
+      final Cursor cursor = this.local.getModified();
+
+      if (cursor == null) {
+        delegate.fail(new RuntimeException("Unable to get modified item cursor."));
+        return;
+      }
+
+      final Queue<ClientReadingListRecord> toUpload = collectModifiedFromCursor(cursor);
+
+      // Nothing to do.
+      if (toUpload.isEmpty()) {
+        Logger.debug(LOG_TAG, "No modified items to upload. Skipping.");
+        delegate.next();
+        return;
+      } else {
+        Logger.debug(LOG_TAG, "Uploading " + toUpload.size() + " modified items.");
+      }
+
+      final ReadingListChangeAccumulator acc = this.local.getChangeAccumulator();
+      final ModifiedUploadDelegate uploadDelegate = new ModifiedUploadDelegate(acc, new StageDelegate() {
+        private boolean tryFlushChanges() {
+          Logger.debug(LOG_TAG, "Flushing post-upload changes.");
+          try {
+            acc.finish();
+            return true;
+          } catch (Exception e) {
+            Logger.warn(LOG_TAG, "Flushing changes failed! This sync went wrong.", e);
+            delegate.fail(e);
+            return false;
+          }
+        }
+
+        @Override
+        public void next() {
+          Logger.debug(LOG_TAG, "Modified items uploaded successfully.");
+
+          if (tryFlushChanges()) {
+            delegate.next();
+          }
+        }
+
+        @Override
+        public void fail() {
+          Logger.warn(LOG_TAG, "Couldn't upload modified items.");
+          if (tryFlushChanges()) {
+            delegate.fail();
+          }
+        }
+
+        @Override
+        public void fail(Exception e) {
+          Logger.warn(LOG_TAG, "Couldn't upload modified items.", e);
+          if (tryFlushChanges()) {
+            delegate.fail(e);
+          }
+        }
+      });
+
+      // Handle 201 for success, 400 for invalid, 303 for redirect.
+      // TODO: 200 == "was already on the server, we didn't touch it, here it is."
+      // ... we need to apply it locally.
+      this.remote.patch(toUpload, executor, uploadDelegate);
+    } catch (Exception e) {
+      delegate.fail(e);
+      return;
+    }
   }
 
   private void downloadIncoming(final long since, final StageDelegate delegate) {
     final ReadingListChangeAccumulator postDownload = this.local.getChangeAccumulator();
 
     final FetchSpec spec = new FetchSpec.Builder().setSince(since).build();
 
     // TODO: should we flush the accumulator if we get a failure?
--- a/mobile/android/base/reading/ServerReadingListRecord.java
+++ b/mobile/android/base/reading/ServerReadingListRecord.java
@@ -23,9 +23,13 @@ public class ServerReadingListRecord ext
   public String getTitle() {
     return this.fields.getString("title");  // TODO: resolved_title
   }
 
   @Override
   public String getAddedBy() {
     return this.fields.getString("added_by");
   }
-}
\ No newline at end of file
+
+  public String getExcerpt() {
+    return this.fields.getString("excerpt");
+  }
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e6a7ab9f11dd496b13af8ae1294df877ad031349
GIT binary patch
literal 1732
zc$@*m20QtQP)<h;3K|Lk000e1NJLTq002Ay002A)1^@s6I{evk00001b5ch_0Itp)
z=>Px*en~_@RA>d=TWf3-RTRE=W_FiC6CN5s0TV^TL!lrcAVkGT_+ip+cU!7Z#egXU
ze-KTSAB02_V-yn;e)tFxl~h4(_q~k<Omz8Se3AwlV<IMk1h5j66)nqdXO7=poOZi*
zcV~7UyWuA7+`0GMbIy0~Ip@A+IjghO)6+A{<MXvM;`b3}v+<nGIh#pDX~y{l#^?ek
zb_(Tvx!l!MM@L823<Pe5*eTjQd-f<zP4{jgiEZMHuYw$-di}+SK2<sGZTI{8jp%2T
zu@PQX(S&2MwVX@)0QByv7+h~e0RCYv%XW8kwER+zPt5>#cXv-}oILeCD6qbqf@bPq
z_F2Yhd(iKHw^X~D0FFc=)0wRFVN{owQp{R8?aN)uJO}?)i}}dK1x~+5qtUrc@}xND
z_c$kuuasqF@q+pDdQ+(s70QtcU!2bG-=CbwxST-Lo$ItLMD*6CrORF}mg6em=`-jJ
zD6+6v2?yVIMx*h^3;Da|!{KP+2}$DJg~Hv)Bar=_8KOHkZuAcdd9I8w7872Hvsc^z
zSS$mvqR}@=dO<ID1+e06=!7ZXstc|5iRg1qBi5N=+AQ%W@}jH~KY;8Cg+g};I;9e^
z4fVopp0~;;4@}K9wFQHh3V0pH6nBJV&j|>bRZw1>Rk+9ZEDE1?e1s#B<Z49a1;rYe
zeJ5f&gJZ$wS%@-GSBalQ<OdCR958WBtc@+FEZ0({n3Ss<X#<xEhZ8Ft2aI*{$`PR!
zGTz}~ShcmpW!CICV5n#dU?zJXDh1a|(wb6rh2`+`c_N@VmM|SPdVCF6>=2*_tzth$
z(7K6L4qAB?;OpNy9i<!X04sQiC@i9JIF{V0`)qz>IeS&q8I&`}QNXItCwUBNVuVZp
zL&QHcL0>PKv`@61=BN+n(&@AjAg<96{q88>#>OdYT?1o45YEWXIR+S;g{NS7VFP-&
zMuNjXX8?y|iCx$k9l*lf^#g{oy+@QX>GoyWiAo-cCH4X6j*65uAjpt<o2^kg8clRz
zS5%i^8L{IVT3gTB>cbk>F4kb+42i^={!Kw>E8w=))~Ke@W~_=bhO}^r2#3}`ob2yE
zD8$+A!+8k3vDhk^NngO1z4?L~|09)}gTa<Qp@yv^tmDcJ$v>edhxGSaJ~6r<=wO8&
zwgxT1P~0dqOk{lsUQ@(-O%t(i(tlp+D%8PNlNN_{ig!X1ro66DNKNuYXEimqqOGkB
zH;$vpHj~B;pycsR=&lE_FyrqsoK_Zt6|ijsylCmN*I~wwj`n43a@g}8O8;@`fen8D
z`SCQc#fQVu_>1Vd?c)h>s|-`&DI#?jrp8b?nU+U55=-uoBx!d!A<ov(uK>IKWdHGn
zmB0!;ECsDdJl@Hf^uAR-COZT!%1+>4f|HsE1Oms+w67X)C>DD}=5iduY;lWHN(0!}
zn#T9aZ1}vUY3Bz927W_;94<v)sk|z{;c$Glr0}CKkcpK9SgI$wjHzV_qU{+=d<q(y
z_TipbY>^~$EYqxmMMV85ca84A4WGD*fh&$p8q>P$1S`0k8sVOv#O;cge~H7eTgIwp
z{taM5!`Z8k>7OMa9&AB8IH2o<`nQ9v&0EcLF5klN2p>Kip8>F+*#nl20&J%aR;VaG
zM8Qt$NeuK5{Px(zLH0~})vK#DG{RUiPF8#k$3WavotV*9FyF5(4)V3zwl$Btxe(uM
zSqVy`lE%+f1PrhZ9&f`D82|m_voKzYkvo~z)CV^Q0+)@qvX`=~NsHdb`uJlzz`|62
z&iq5`Y6(_!UAZ}wFJ7|j9e`~%+s6pECsb{Cj1T6nN<Zq~S(xQ#Cw)o4IDLE#0culq
z!szn)Kp<6JTW4F1YtrIe8QVPZiNFn){x`xnYTb%$-b*(e(*FRB8DV_c+K&5|w{BFh
z0@uh0_ePTsb4G_?%40ggD0r-l&Jbw_uha9>M%%efm!F66vBz@U0(}m!RbOC>)ltyj
zvc_!yX5}fNP;$Pc&?i_{R}EGO<-~Jlh1Y^!krfi#yql{LE>=$0G*x{6#I&KI`nvos
z0Z2hao>QW(axw8~8v=pTl?m&IqN)I;tB-}rOyn!B$94L8cv=Ds09UZNm|71&jOs8l
zEg5Z~GHrQ|Xi&~(2hILw$fyh_=}ZI0?}NT~QX!{#TQzB?^|PJ7gsDXwwHiyr28<Eb
aFV(+kITQH|w*}w;0000<MNUMnLSTa0dPCv>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..14d3c53d54350e52736d7da315e90266e19084ee
GIT binary patch
literal 1148
zc$@)z1cUpDP)<h;3K|Lk000e1NJLTq001Ze001Zm1^@s6jQ+T700001b5ch_0Itp)
z=>Px(Hc3Q5R9FeUm|bWbRTRh1nVEDO8`7s5+CVFUU?~WSpn?zSlYMYMN0%VM8uJho
z4Ej*0SU+D{KOWkMNPN($(bjcmW@ne8;!FF`7qMa#RK%!l!B$(WjhbeY-8r5cyJdDV
zw>vv?lYwFGJ@=gd{oQ-^-np{~t9Tu8oSRab@gM;00>W*8FbG8d0^m6=e@`COnoA4E
zipAoeG7OY8B(axDwXH^qUPZuSXC0a+f`jw_%)LA^GBOwRR}N^~?rzZVO+@Ssc6Qx5
z(QiP_JYQe<{WO0iY1yt+pFv$a+%=r&7;y_}#)-01%lSE!&7kE}AJH^?$1fhIOhi|x
ziT9dj`YREy1kkqa?Wh}HvfK@EpjO7vnLp0s_VMxbyqXm2#{Q^qJVT4TD4t#bZ&2K#
z%=c0|ByQa_Ku-x<;u$QLYq#n>@Lg0b0aVDP5BP!XN^LJ9@F3f+xxX)8+LCw%HG_7u
zY~+DbbMf6a46fACdoO6?dVgQv#`_WjGTJ>M;!@M1VMHDbXj(QYAZG^el+uJWq)2->
zXb!UpA0Ek$=%Wd$a=lb4={>35Mo5#?Bo+yG{d_B}SK(gbk@f_G2;f2@Ko&&Dt$9IV
zRdohjNR(p7ac))x6Q+Yi=VJqwUH5j-^pj!2s%!-KODsUkb$9BZ9p?-WsDcUALHHyN
zAR=iF3-*VKsxwlf?m2M)c_GrXd^gpxtW+YPqs3zOr#OIM<}<GY(qk)S)mV7&SE#YH
zPpBDjB&VjPc+bK@oY*KaNHA6~&7XyDoEgL$2R0w(H^{CioptNOv$rpw&m3vRbv=Vl
zrFw`@zYnzpR6WI}%qN{OGne_$ui4cM@{#^IhI_&<prXuRWpAl|o0&_u!3piiuR5zz
zeOyC)Lj};X2En(yQmf!KJ}wjrGcw&sfVS%n^LOnB{4vzkjQcy~S-RXLFdNXAy>RyI
zxrvF1y1!jk?g&_}RQS-S*TPO4645&U&=2w=8shxi)$;KOhB<{Fwdzry*PRi|s@|=m
zZbgL?n|ITL;X*F6_peJAhk)QS0YV<U9S2UtN&(B|>JHTL82fAxju9>bkH#Lzrz^sD
zbaZrK=Ic}UH@wEqZ)Q%7M~Dw~2~DwOS=-Q9caj?iL!&~C-jn%k_E2b30^^E(kWa~*
z81C~<aJ&Lf@Si`&|L+@T&3yL#1RT=PHo)oW>3$=%{!@m#MH+FnLBKK83!VIuwqFCB
zoSaN;=-cSB;2lXtRmLv^E`R*2TYr?dg&~#VDzugXjh^1O7_RMKC7HPAc`vR-YFqsq
zcEx>#_qj(~2HBI}1gH57_$#3Hqs(VD>;}KR_CygTgqoSnC_gB4>RY-|2Lm;t%|Y8*
z)UT~#S!ewE+QaGn9X$9!=Chjij)44LeQh-XI%#v}U{>x#7v*hh)AAocJ!Dd`N_wUM
O0000<MNUMnLSTZe{3^x(
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0c7a6856a73135fe0ca54ffb614e3d24bd7d87ee
GIT binary patch
literal 2272
zc$@*?2p{)}P)<h;3K|Lk000e1NJLTq002+`002-31^@s6juG;$00001b5ch_0Itp)
z=>Px-nn^@KRCodHTzhO3MHrvi+iMH0MG23hrQ$ORiUmz5e`qj@L`i$syQYQ`&<aX0
zu_h!M2nivPh+rhzM5V@t7+TPy_pFVSkVvBP2kIZhAVP={l$W4T8tmilcKl7rHP`Fy
z-EMbwZ?CY)Wq0TCJ%01e%s1c69wp=8>gecrfXW4nDIxPHp|cqw6_k=v##k}b>m!8r
z63WgKMo%)rexXXN%j5AJ6`?wK=I7<}byheFNh82o2_^2;uLV1!5E>yY9*z$Vep_E(
zAJ<O<O*58CxvUci1g24k{4Qgpky0v&kOj1k6NPQ?cxu9W>E#W8EiEn1DbvcDBuUx?
zB}?@xs;N99hn2YUw%6<WK~s4Y$_oJf{zw&d&@V8qJ*ZVNBjprAKD~V5+?#9Gtchl$
zV|xGtfryJpWG}kwglx(P&1d8gQ=)a4Z2rkaSI8KWiGh*4U@*KE-~}uPj|)x7BI0l!
z>F9{e$)tmAEie#@Yyof^GYM}=9%EOT5?cUEJeiK4EdlfgBQHx5-IY$@oTM>!;wp<*
zHP+T%P33Pp0DYm*Y$ug|L?4=%Dr`>Q8R?D=_AO2I7i~2LwzPCkl&R#i9eBLuC|M#G
z6mCszL|Xwkt&BBeDVA~2PF29#ce)Oa{KLsSZ3AFOIQ#^fc~!D-f}gQ8W}hoDw(wt~
zl=2^c=4s%A6=C)ZlbVA_&i9f_+GY$4gd+!FeT$QY6ZH&qE-)5f;dQ$YrlaW(g`NdA
zy8%X7y8P5M7AH@7Jk{UxbX!;;y2WE=fd?I7Wa{8?5IC^B3@^*yvtAP0o+x7r0I4Lu
zmJmqv9q6^HSza=)8z8dQ<TSNsbmfI;^hbDp&H>!Kd9%z2U9Kjq(R6e-O`vc-ddgCV
z)6MJT9Kbo1l?xFhn8bxK<GL!dV_NCzDT}~tEvK7v05PpC(W>BB%H1|Fplw{!NgQML
zd>+)zIe-uZ?u&Vs!%;LeR%zjIdrY4X>U1hga}FT(Dn>Y_S`^h&2A-Q{-%?LmE(4>|
z+?)c~+}vCU=Xs_cK=hWyjJb;YdD-gS@CE7WY~@Dgv)MTXu)MsSPabAF^LzoZI}?Zr
zM|vbU8r~=^H(U6OQUrs+`-mi;%2-HTCR{?+%3YPjY|F<y-fe-fPRkn>>WV!4n1cr>
z<gl;>ffHR#WwLs5!srcZkKMeIoOa><en(FZ+D#ZsO4y~G0(klI=}{40VcJ<k=v*!V
zq~#T}wpgR5Ib0YeXAO=5V-t+8f9F;{-ijsM%CS+fM=h-3&sh#258F?hPWE9Of59Fw
z7?46?KU+8n65{juCd-9IU0A+V84%kRRpCJ>M2XF?%mVj@LNjIJ=thf~x^%M)T}lpP
zulR4v04ySo2)ga~01vIi6c*x*WyvhePgWs*Hrq1mFqsh?=x97D?XwKPp;JzjZU945
zVsj?rHHthO=$-ZT)n_aNkTdeQs&`=2<zet^{5@|k#(JZJ{qH2?x1<Xvn0Z~UNIXgw
zpbPw!NXxgo!r1#b-f<}*x1|7brge4Izrh30ywQ^o)E;+s>UwX_j$~OZ^#eKM8;8T$
z5rhG!Ckv7X&(~NyS_RkTc(SaPTHu~Nom1pO)}3$Qai<yMcS9<897xOjK&(PaoCWL~
zfcH>x3+BZ$rR%+}nyz%RT4I5G@55P86e85z9k_G~8cE}phY4>3Jc#$z=$LMSh`dNM
zD`vF=yl2#euw^>j6?}-Zyv|+SnMpp=0YunpCzf#DOhVW#58nRoN{rONW5{}{!PG8{
zgW_8e^Ly290IA7?F58FSw%9=5{JOfDtl)7vrnxbJaOXM@|3FPxJEa4(ION$C8|>c<
zC;6-%*-Qa25D33S9nvlcVhVjd3T7<JKVcwrAG+%f?%-!EtCA^PL4P=0BN53Dex{GF
zDmtkq^H~+aTy#x#NTKZD+0&=Ho0^&iR4Zu65DFl`dx{c?2ThAK@HVvw!e@KY0e&Jx
z=_PT3&$Z$wdwP0KH8wW(i)1VeKwmIeDO357nP>epE6&Jw_@?9O)NeX8*Vj={$Smyh
zh3|Jtj{RnWhwe8B_u?qOXVLFUKZ!550z58<VF3Mp{|sj#J%H|FVsL<+i4kQURs@y&
zj@7|wH4qgRU$xBb{!=Yqbh;q`+uPgkkQ@a(IB<_>!4mXfb(dH?wz%Hy{*|Zk`v8kA
zN(Um+cH*#jFj3cv`<Mab`@0j1OTUJr@~ByY0+1d=L6)R`DY>k+_K#E$F=-r&4fd@w
zOBOY{W12+xCehTg8DY2t&#MtOnvSuqk0|uH#0M?u=pn*W<}57Q!t?sBvyBZ6u8Zl)
znVB{g0BJB3X+v<J!7RaGc~Q!ktG2cV;V*k%y1OvI`^a4I5JDnkMLvMXn^qTq2o7ul
zcyE|#6O@s)UU&6ZX7Uh8TXProha#^_lzuFdc`7Z8d+R;!nr*4+c7E3cpg+|4A~uuv
z;E1d)i&ay+)>W;EuUdUOrDp}CBNSQ!ZFh?T&rZsVkR@Zpg#Y0Ux-E~aI7KiUk2F**
zkpEu_{w=5rN*9ku#YO3q)&jA<y_?FEhovTqVl-aqjIuy@_fx5(0H5EJyT+^1BJ~<!
zfo*MVr8t7SAK+Q?Cto5J$rY_(0Qu&3amgh9|9SI83p1YFmQ-wp07SQS;N-xUh^#D1
zEpY2SGXg^Z%FYR&0lcCA0LWbYR&FW+av&7mj!^t7sToGz6-C*C`h2eyOJ`~LFrpEf
zcC9Nz!MB79r^G(-dcEg`)3qVNQErR~8Sy`-cZ#5s4>U{x5T60h$Oq*`$Y=_H7*lT=
z3mo<X4YbB}M365c5PE^%LFXdA203AXUS3r-3?Q83lDyJbu&}0dVZk!mn%pe_4K@z|
uWd9kYaZ^{9um}6$m_cMWKKZ<v5&R!hTeiIU{}~<t0000<MNUMnLSTZmP$$O#
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c4a189f29747dc575aaca982517ef5c9a35af46b
GIT binary patch
literal 3536
zc$@*y4KMPEP)<h;3K|Lk000e1NJLTq004LZ004Lh1^@s6Ib=4{00001b5ch_0Itp)
z=>Px?ib+I4RCodHU3ric)fs=UduC@51PlQ|Kw~aJ3L&6YL_!pAlHIwMjUZ?d4<e;j
zG=+b8V3INw#YDlRTqvU=Alcfvc9DP<u!N#aj6v8+2$IsML2$uXBEqse)BW;$lMKx4
z_U!ab_iVpDrfRCY-|_YL{eJzu?srq-{d6M`kH<$)>bqPe>|#pkBxIaG2pvHv8A=Ho
zit`%A$O(iW&;BVpOc*^t2s=O-+5Z_)-dGe4f2R9}CM2XL$Z)iXc)aZjM%6h~qBjsm
zrh{0iQiB;|DjtSCl+f)=VVgQScDFBEwoI*5o}PRj!5S#RQ%4cWcPqfTgHU>rMiNtk
z;Oz$~CF|2nd8siJ+GmQ;UP3&A^8k4znH*nBrH2@!ivYYoFRJRNjIfPFp^rx*b#It?
zGml`P+1%Vb(qB{iFue6HTn5YpxLjEvdYhVN_ct^IJIc+gmEQFPClaXzjMAs^ZXKZo
zkQHHgS1b59d-mjsV~;FYu;6neI=Gf#c<oqUpmsGrcy2NRU{&)nc1WdcK{OcLUg>@0
zN;BFyUo4fHCCmP;@Y<<D+d=8s_;O!(^St?V;)IKKwzs#la`knc2ct;7he*<|0hF&?
zuq;i_$d+_ZS3_fC;}_-V;VOa?skY?+=O^U=Bve|)-b(kJngf!L>eb6N1jm!D&!9-Y
zPcJZ{rb8M1&u*q%GcOeSw;ml`L2xY9wi3-x_vit{4r$Op@{deU*Gzcwr=|3*xY@0g
zU{T`ZskR54L@vh_jW82^HMJX?n%d4Pg|njxZb>HR5XPP?1%e&p@w&NU)VXX;Dcv1Q
za7#<eH)JZm0w5e~BV0L}trhgcvLlU04~-=I0x!aDbm2MtCrQg=v9@W&taK#7mrR}Z
z0J`O76f@dk;Q{JnYw%EO*6STdFfW=>bp6a>B=&0#np-E~q5iQ}BF7Od`2vrl?pLd2
ztW{yWhlr?bv63beKPBo={^#dBEE{KzVvojRv2ndcIjSC<NF=7Cqy8^2QV6?HUws!O
z-W7?|Z|e<w{&hUwI-5vz6{<g9%MUOAoIM{7*58qz&ru%CsQfsL6N6mBcA+ui@>1k-
zGI$!E7T&xopPX{fF;L-FK4>97pJNDaX-QpzD$(`%##wdFJvY;Rs$Ne^Q%*;omgmWO
zeJ-%fBOirQL*)navpI%f$w&BT3?Y6Pq<3dI<mQyl=0TC3yRPI&8N2<3L}GX@uOkTl
zUz44*AZL`7NB+@dq=TLp*G4|PP>Ky2PUOZM296+j)F?HA;%QjUC@YUtCBHX%f5S2;
zy)CDMBM4?x`d-c;tB+}k?J%l?qR>AYl7U7Z)F`rF<hCc6H%nsXz;%WIF*9>__eVy>
zscP3jqcRxv-N<Fwp5V!or}5ba!i<PK9taq&rkNoFQ)#Ys1i@4$-!=NQo0<P?Le4WP
zZ&+FyYg7jEUxksQvh6(>hFoj{7^aEvtJLrVC(CS>VTC9iE=hq&wkH@3?LraEB=Q5J
z<d`9Y>mbY2{sg08a-vZ{&CHJnZZ^hUUtOt8-Y(5$RGL3OAD`xGYY$$xZrwRBNVraY
z%B0nJ|9vSRY(3}MJm_@LlU7d}iPDL-CfM&Eif<b6e!N!*(_&i7A(^M;d5rfc<>Nue
z*_vQUVnPzkAx1HKrLShtn|g|Co`$F8dGhHodX6#F=V%V|Fe-SU^5bb2mSBzn_eM&j
zZShp=Hs1W0ZRN@}gaiU(`4>0;pvt~;4cDGEP^UI0Sd~@3L}U%i#0tt1D3RHI-c=nK
zbVfOppVi>?^bHspZtEhNX6Uig#&{=v5TKN$?M*Pvcm(&MwIJZQtqE2XWk8oCDuB)&
z7xpGtlB7}gz}X@#89icK4{k}Nz9su>c3K3TEpCgEA@A6dU|w&OiPVm7&~t5pv_(o{
zF_-<eBRG~!Hb_MJ127Y=iP9n-i{6$py05C$u%dUaFak5_?#7Zw&pLX>V1?N9&c>Hb
zJjRp}w((%Bukk1rPx-A!Zf`2Jui?47`NF5dH&ya6SC>qk{u`L_BW+W>P)!6V%}$pQ
zn&1^HRs_bKf8koR={9H@;RO~&K+Uig9w~GYJ?{Jqp7O{o%3;w6RFC#YqxF0ep11^a
z@5Sm1ONv_IMHEGVs%O^j5Y~f7j~<Qg%<RhPMGf&H`XT_(PNmb`FXUo`B^VR2dQ_D?
zlvC6@>O_F4&jY)UbE$<Tm@60!*RNC=xdnB)uAIW&u{Z*ldYaDkNKfRa5Sn1FXf#x}
zKEsq5Snulne8s(UEd<n6jg565=En$6FjuxQ9Nxp!%%xabw>@8Z@B9n`DCLeEV``sQ
znk@+CW<(;9{~YNgm_^9`b;d03><j|xQmrj}Y(p?NWbxv<lUVBhgEK~XXITUo*%S$d
z)@Y$2bW2umg<BG>QH)Z%s&}}XuTd@f=+sGaTQLl5)0-5FrLLqheVs2+P|O&&h2y=e
zC`7$_ZYWfA;DaqZn6IEC$#f$I8hhmCKgE>YJvUgKT%J^Tg0XtrSE*0h0$oq99FuRO
zi_ag91cR$f*)Q}*7Ut`WrgF_|c#n)OWr(}t+0)@*-Ou&dEG)qwcbG3QXfvPwp~n!n
zq``alQ7l(@zg~NVETTc~Ak4Je2+y6O*AS<t1F>keB}<~A`lfPh6^>xO?lH)1#(bWc
z<rw1Zw5(HM^p?5dx~=8fDOwRtr%avpJCOVRat(28dd9YPonTWcA(vw!6v2s9>#tB1
zTIg7^$}kK)L&u1!-n)0t?(65z4|bN3wIHc5MKnfD@`iHFRm%%BcEJV6U32QB`k)bF
zb1(_fk`+s~F2;Hi&*hYI<B@+vfsBNTO`@aGy4_}BU^&6@RBA3d5H`4VyfcXX0`@nn
z3VE`jp>Drf#OCB!N-(COT#F?RUIn=|IfYzu%wXi>>sX<E?I)cine8R3lHryS9E-JH
zhGhu1f!rZgvP~qh84Om~3sSc;%G$e*AKz(oXK%g6Swe6;9-oNKGj@VpVHOe93z$|q
z6thRrjrmu~$meK`IF8{({CEhHzuqAvbD*Q6<2}^xGg^789X8+u^GzdaiL?vVp@PjL
zs%>Z$<!3O9;5!&-{--M5qhiqmT1{HNzU|8a%3i~~UZ;>tP*;5<8Vw3bE`KTpj9|1c
z5AoM9%<H8SDt;UV%ZPXCwvN4T|57lu3e*{pmMlIE1?|fz6w8+u+}-L6#`Y;H31d{m
zWl-e_UKiD`68of^u!VTHmXSxlbhA=c-Glk`&++GfWgR?swpmJ>A_r}}M>=VJUKQmn
z>^HnaCUP9|1Euc46tZYuBzUM)I;-QW?!lPb`y4zsYPF#S_h;nK>7G-eJaV(q4WZB`
zrl=uYbQh%WbpcObf$eWL_6ipAvZ@5fldZo%hy9%*S&nwzzomP+=3xHL7g~BnBEil5
zlFZ1SgCD+Mu)CO+98tomR!xeh+8%)Lr$jN7>w&uIdo0s4Ga8K^E~QsIp1P6BQWEL%
zifeXKNz>`oXi_YtPo?oy@nC>+8^~Q=sZ!O*2hTkWa<9`vF26tbWTIwf@yXQunjh5+
zxynxDa=lIW;Fe@tokUmytyID+0}FCLMPtOQhKBmTbEC`s@x1`GriGL%$BOi(5{w1P
zW})+73xw8MVT{&2d2bD6$~EC|_)V>Nr-zwVL^mfAQ?XY;t4QR6yl#wT3b~bB{_vVo
z4{mNwecdn7&5$R=29ew_c&?%{dUK^?nYaum{0tL}nFnJeALZ@KBCQ+Au(kT_4Uu~O
z73}JtA%gj~Hj-S!r<GhN7K-Zq_zHeWES;>;recEmrp#vzCYTULCJ&VAsH*J1@bC8i
z??>N_hJ!EnpT<406%pLj)Z`yIny;irXAC4%oLrE%Y#{Gx_j^@BFdcR7m={6rO?Efl
z4f>Z|O-dwNpF!urd^dpO1RKjD7@Y@?fZThXV7Oas&`mG~r!PVC<6~|C#wm8{A~=?8
z!}e=rrBjS{lN}`q#wdpyK<>-%-jZqE1e{%L?e{AhgVVo_IYTiR<F|`Jo}PsW6!prM
zmexsFw&+zPA6%HlyRs7knjVby<qN2f@@~Jfcd@pU@ovzy55X9}Jo5i5+l;je0992V
z^IELTLBD|DwQJW7#rWmT@Z4{jBi@r@H3Fvz=7ZA*4;hw%Les2P%DcaL2xJLHv5b8l
zYd3fceVPZXD3Y=S`}~7efm}Y|Ta@2PQ53Pe9=>=o^#lf||Ii9UFxqj0!ZJ<y_r{*U
zs*>)`?)j4+?wwbLfDa8+ODoH2LM}?W=Z3=VCP?x`6hWY9Lr0PH<_0r1#$17(wB86<
zOE975^;XEcHWvYl3HChLTsr$ovy@=I2n(-&_f^XaF$)1p3FbzO96#QB51YlNhMdza
zSsHOw3*t*9plWs$(>Xf>jCuPoR1Im9rmpm-AIMya`fev>>|<3WojpCI^T2_(kBYvw
zMSnKgDX!`s90}D6HBZ=1<d~tW<?moKjPPU>N5CVvI5v)m@d$PVjm3@N1mk1Y`#BW>
z317G$R?)cbD$4dm>p(zaRJ{)*e}J@RdvdqR*iUun;3cUd0{;cYHx)JO&DDAU0000<
KMNUMnLSTZxNVq@%
--- a/mobile/android/base/resources/layout/doorhanger.xml
+++ b/mobile/android/base/resources/layout/doorhanger.xml
@@ -11,48 +11,50 @@
                   android:padding="@dimen/doorhanger_padding">
 
         <ImageView android:id="@+id/doorhanger_icon"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:paddingRight="@dimen/doorhanger_padding"
                    android:visibility="gone"/>
 
-        <TextView android:id="@+id/doorhanger_title"
+        <TextView android:id="@+id/doorhanger_message"
                   android:focusable="true"
                   android:layout_width="match_parent"
                   android:layout_height="wrap_content"
-                  android:textAppearance="@style/TextAppearance.Widget.DoorHanger.Medium"/>
+                  android:textAppearance="@style/TextAppearance.DoorHanger.Medium"/>
 
     </LinearLayout>
 
     <LinearLayout android:id="@+id/doorhanger_inputs"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:orientation="vertical"
               android:gravity="right"
+              android:paddingLeft="@dimen/doorhanger_padding"
               android:visibility="gone"/>
 
     <CheckBox android:id="@+id/doorhanger_checkbox"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:checked="true"
               android:textColor="@color/placeholder_active_grey"
               android:visibility="gone"/>
 
-    <View android:id="@+id/divider_choices"
+    <View android:id="@+id/divider_buttons"
           android:layout_width="match_parent"
           android:layout_height="1dp"
+          android:background="@color/divider_light"
           android:visibility="gone"/>
 
-    <LinearLayout android:id="@+id/doorhanger_choices"
+    <LinearLayout android:id="@+id/doorhanger_buttons"
                   android:layout_width="match_parent"
                   android:layout_height="wrap_content"
                   android:orientation="horizontal"
                   android:visibility="gone"/>
 
-    <View android:id="@+id/divider_doorhanger"
+   <View android:id="@+id/divider_doorhanger"
           android:layout_width="match_parent"
           android:layout_height="1dp"
-          android:background="@color/fennec_ui_orange"
+          android:background="@color/divider_light"
           android:visibility="gone"/>
 
 </merge>
--- a/mobile/android/base/resources/layout/doorhanger_button.xml
+++ b/mobile/android/base/resources/layout/doorhanger_button.xml
@@ -1,12 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <Button xmlns:android="http://schemas.android.com/apk/res/android"
-        android:layout_width="wrap_content"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:minHeight="48dip"
         android:textColor="@color/placeholder_active_grey"
         android:textSize="14sp"
         android:background="@drawable/action_bar_button"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/login_doorhanger.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <LinearLayout android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"
+                  android:orientation="horizontal"
+                  android:paddingTop="@dimen/doorhanger_section_padding_small"
+                  android:paddingLeft="@dimen/doorhanger_section_padding_small">
+
+        <ImageView android:id="@+id/doorhanger_icon"
+                   android:layout_width="@dimen/doorhanger_icon_size"
+                   android:layout_height="@dimen/doorhanger_icon_size"
+                   android:layout_gravity="center_horizontal"
+                   android:paddingRight="@dimen/doorhanger_section_padding_small"
+                   android:src="@drawable/icon_key"/>
+
+        <LinearLayout android:layout_width="match_parent"
+                      android:layout_height="wrap_content"
+                      android:orientation="vertical">
+
+            <TextView android:id="@+id/doorhanger_title"
+                      android:layout_width="match_parent"
+                      android:layout_height="wrap_content"
+                      android:paddingBottom="@dimen/doorhanger_section_padding_small"
+                      android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light"/>
+
+            <TextView android:id="@+id/doorhanger_message"
+                      android:focusable="true"
+                      android:layout_width="match_parent"
+                      android:layout_height="wrap_content"
+                      android:paddingBottom="@dimen/doorhanger_section_padding_large"
+                      android:textAppearance="@style/TextAppearance.DoorHanger.Medium"/>
+
+            <TextView android:id="@+id/doorhanger_login"
+                      android:layout_width="match_parent"
+                      android:layout_height="wrap_content"
+                      android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
+                      android:paddingBottom="@dimen/doorhanger_section_padding_large"
+                      android:visibility="gone"/>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <View android:id="@+id/divider_buttons"
+          android:layout_width="match_parent"
+          android:layout_height="1dp"
+          android:background="@color/divider_light"
+          android:visibility="gone"/>
+
+    <LinearLayout android:id="@+id/doorhanger_buttons"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:orientation="horizontal"
+                  android:visibility="gone"/>
+
+   <View android:id="@+id/divider_doorhanger"
+          android:layout_width="match_parent"
+          android:layout_height="1dp"
+          android:background="@color/divider_light"
+          android:visibility="gone"/>
+
+</merge>
--- a/mobile/android/base/resources/layout/overlay_share_dialog.xml
+++ b/mobile/android/base/resources/layout/overlay_share_dialog.xml
@@ -4,25 +4,25 @@
    - 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/. -->
 
 <!-- Serves to position the content on the screen (bottom, centered) and provide the drop-shadow -->
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/sharedialog"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layout_marginLeft="15dp"
-    android:layout_marginRight="15dp"
     android:clipChildren="false"
     android:clipToPadding="false">
 
     <LinearLayout
         android:layout_width="300dp"
         android:layout_height="wrap_content"
         android:layout_gravity="bottom|center"
+        android:layout_marginLeft="15dp"
+        android:layout_marginRight="15dp"
         android:paddingTop="8dp"
         android:orientation="vertical">
 
         <!-- Title -->
         <TextView
             android:id="@+id/title"
             style="@style/ShareOverlayTitle"
             android:textAppearance="@style/TextAppearance.ShareOverlay.Header"
--- a/mobile/android/base/resources/layout/site_identity.xml
+++ b/mobile/android/base/resources/layout/site_identity.xml
@@ -1,68 +1,79 @@
 <?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/. -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
-              android:orientation="horizontal"
-              android:padding="@dimen/doorhanger_padding">
+              android:orientation="vertical">
 
-    <ImageView android:id="@+id/larry"
-               android:layout_width="wrap_content"
-               android:layout_height="wrap_content"
-               android:src="@drawable/larry"
-               android:paddingRight="@dimen/doorhanger_padding"/>
+    <LinearLayout android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:orientation="horizontal"
+                  android:padding="@dimen/doorhanger_padding">
 
-    <FrameLayout android:layout_width="0dip"
-                 android:layout_height="wrap_content"
-                 android:layout_weight="1.0">
+        <ImageView android:id="@+id/larry"
+                   android:layout_width="wrap_content"
+                   android:layout_height="wrap_content"
+                   android:src="@drawable/larry"
+                   android:paddingRight="@dimen/doorhanger_padding"/>
 
-        <include layout="@layout/site_identity_unknown" />
+        <FrameLayout android:layout_width="0dip"
+                     android:layout_height="wrap_content"
+                     android:layout_weight="1.0">
+
+            <include layout="@layout/site_identity_unknown" />
 
-        <LinearLayout android:id="@+id/site_identity_known_container"
-                      android:layout_width="match_parent"
-                      android:layout_height="wrap_content"
-                      android:orientation="vertical">
+            <LinearLayout android:id="@+id/site_identity_known_container"
+                          android:layout_width="match_parent"
+                          android:layout_height="wrap_content"
+                          android:orientation="vertical">
 
-            <TextView android:layout_width="wrap_content"
-                      android:layout_height="wrap_content"
-                      android:textSize="14sp"
-                      android:textColor="@color/placeholder_active_grey"
-                      android:text="@string/identity_connected_to"/>
+                <TextView android:layout_width="wrap_content"
+                          android:layout_height="wrap_content"
+                          android:textSize="14sp"
+                          android:textColor="@color/placeholder_active_grey"
+                          android:text="@string/identity_connected_to"/>
 
-            <TextView android:id="@+id/host"
-                      android:layout_width="wrap_content"
-                      android:layout_height="wrap_content"
-                      android:textSize="20sp"
-                      android:textColor="@color/placeholder_active_grey"
-                      android:textStyle="bold"/>
+                <TextView android:id="@+id/host"
+                          android:layout_width="wrap_content"
+                          android:layout_height="wrap_content"
+                          android:textSize="20sp"
+                          android:textColor="@color/placeholder_active_grey"
+                          android:textStyle="bold"/>
 
-            <TextView android:id="@+id/owner_label"
-                      android:layout_width="wrap_content"
-                      android:layout_height="wrap_content"
-                      android:textSize="14sp"
-                      android:textColor="@color/placeholder_active_grey"
-                      android:text="@string/identity_run_by"
-                      android:paddingTop="12dip"/>
+                <TextView android:id="@+id/owner_label"
+                          android:layout_width="wrap_content"
+                          android:layout_height="wrap_content"
+                          android:textSize="14sp"
+                          android:textColor="@color/placeholder_active_grey"
+                          android:text="@string/identity_run_by"
+                          android:paddingTop="12dip"/>
 
-            <TextView android:id="@+id/owner"
-                      android:layout_width="wrap_content"
-                      android:layout_height="wrap_content"
-                      android:textColor="@color/placeholder_active_grey"
-                      android:textSize="16sp"
-                      android:textStyle="bold"/>
+                <TextView android:id="@+id/owner"
+                          android:layout_width="wrap_content"
+                          android:layout_height="wrap_content"
+                          android:textColor="@color/placeholder_active_grey"
+                          android:textSize="16sp"
+                          android:textStyle="bold"/>
 
-            <TextView android:id="@+id/verifier"
-                      android:layout_width="wrap_content"
-                      android:layout_height="wrap_content"
-                      android:textSize="14sp"
-                      android:textColor="@color/placeholder_active_grey"
-                      android:paddingTop="12dip"/>
+                <TextView android:id="@+id/verifier"
+                          android:layout_width="wrap_content"
+                          android:layout_height="wrap_content"
+                          android:textSize="14sp"
+                          android:textColor="@color/placeholder_active_grey"
+                          android:paddingTop="12dip"/>
+
+            </LinearLayout>
 
-        </LinearLayout>
+         </FrameLayout>
+    </LinearLayout>
 
-     </FrameLayout>
+    <View android:id="@+id/divider_doorhanger"
+          android:layout_width="match_parent"
+          android:layout_height="1dp"
+          android:background="@color/divider_light"
+          android:visibility="gone"/>
 
 </LinearLayout>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -95,16 +95,20 @@
     <dimen name="identity_padding_top">5dp</dimen>
 
     <dimen name="doorhanger_width">300dp</dimen>
     <dimen name="doorhanger_input_width">250dp</dimen>
     <dimen name="doorhanger_spinner_textsize">9sp</dimen>
     <dimen name="doorhanger_padding">15dp</dimen>
     <dimen name="doorhanger_offsetX">10dp</dimen>
     <dimen name="doorhanger_offsetY">7dp</dimen>
+    <dimen name="doorhanger_drawable_padding">5dp</dimen>
+    <dimen name="doorhanger_section_padding_small">20dp</dimen>
+    <dimen name="doorhanger_section_padding_large">30dp</dimen>
+    <dimen name="doorhanger_icon_size">60dp</dimen>
 
     <dimen name="flow_layout_spacing">6dp</dimen>
     <dimen name="menu_item_icon">21dp</dimen>
     <dimen name="menu_item_textsize">16sp</dimen>
     <dimen name="menu_item_state_icon">18dp</dimen>
     <!-- This is chosen to match Android's listPreferredItemHeight.
          TODO: We should inherit these from the system.
          http://androidxref.com/4.2.2_r1/xref/frameworks/base/core/res/res/values/themes.xml#123 -->
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -405,24 +405,31 @@
     <style name="TextAppearance.Widget.Home.ItemDescription" parent="TextAppearance.Micro">
         <item name="android:textColor">#AFB1B3</item>
     </style>
 
     <style name="TextAppearance.Widget.HomeBanner" parent="TextAppearance.Small">
         <item name="android:textColor">?android:attr/textColorHint</item>
     </style>
 
-    <style name="TextAppearance.Widget.DoorHanger.Medium" parent="TextAppearance.Medium">
+    <style name="TextAppearance.DoorHanger">
         <item name="android:textColor">@color/placeholder_active_grey</item>
         <item name="android:textColorLink">@color/doorhanger_link</item>
     </style>
 
-    <style name="TextAppearance.Widget.DoorHanger.Small" parent="TextAppearance.Small">
-        <item name="android:textColor">@color/placeholder_active_grey</item>
-        <item name="android:textColorLink">@color/doorhanger_link</item>
+    <style name="TextAppearance.DoorHanger.Medium">
+        <item name="android:textSize">16dp</item>
+    </style>
+
+    <style name="TextAppearance.DoorHanger.Medium.Light">
+        <item name="android:fontFamily">sans-serif-light</item>
+    </style>
+
+    <style name="TextAppearance.DoorHanger.Small">
+        <item name="android:textSize">14sp</item>
     </style>
 
     <style name="TextAppearance.UrlBar.Title" parent="TextAppearance.Small">
         <item name="android:textSize">15sp</item>
     </style>
 
     <!-- BrowserToolbar -->
     <style name="BrowserToolbar">
@@ -812,17 +819,17 @@
         <item name="android:minHeight">@dimen/menu_item_row_height</item>
     </style>
 
     <style name="FloatingHintEditText" parent="android:style/Widget.EditText">
         <item name="android:paddingTop">0dp</item>
     </style>
 
     <!-- Make the share overlay activity appear like an overlay. -->
-    <style name="ShareOverlayActivity">
+    <style name="ShareOverlayActivity" parent="Gecko">
         <item name="android:windowBackground">@android:color/transparent</item>
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowIsTranslucent">true</item>
         <item name="android:backgroundDimEnabled">true</item>
 
         <!-- We display the overlay on top of other Activities so show their status bar. -->
         <item name="android:statusBarColor">@android:color/transparent</item>
     </style>
--- a/mobile/android/base/sync/net/BaseResource.java
+++ b/mobile/android/base/sync/net/BaseResource.java
@@ -25,16 +25,17 @@ import org.mozilla.gecko.sync.ExtendedJS
 import ch.boye.httpclientandroidlib.Header;
 import ch.boye.httpclientandroidlib.HttpEntity;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.HttpVersion;
 import ch.boye.httpclientandroidlib.client.AuthCache;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import ch.boye.httpclientandroidlib.client.methods.HttpDelete;
 import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpPatch;
 import ch.boye.httpclientandroidlib.client.methods.HttpPost;
 import ch.boye.httpclientandroidlib.client.methods.HttpPut;
 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
 import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
 import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
 import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
 import ch.boye.httpclientandroidlib.conn.scheme.PlainSocketFactory;
 import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
@@ -337,16 +338,24 @@ public class BaseResource implements Res
   public void post(HttpEntity body) {
     Logger.debug(LOG_TAG, "HTTP POST " + this.uri.toASCIIString());
     HttpPost request = new HttpPost(this.uri);
     request.setEntity(body);
     this.go(request);
   }
 
   @Override
+  public void patch(HttpEntity body) {
+    Logger.debug(LOG_TAG, "HTTP PATCH " + this.uri.toASCIIString());
+    HttpPatch request = new HttpPatch(this.uri);
+    request.setEntity(body);
+    this.go(request);
+  }
+
+  @Override
   public void put(HttpEntity body) {
     Logger.debug(LOG_TAG, "HTTP PUT " + this.uri.toASCIIString());
     HttpPut request = new HttpPut(this.uri);
     request.setEntity(body);
     this.go(request);
   }
 
   protected static StringEntity stringEntityWithContentTypeApplicationJSON(String s) {
@@ -458,9 +467,21 @@ public class BaseResource implements Res
 
   public void post(ExtendedJSONObject o) {
     post(jsonEntity(o));
   }
 
   public void post(JSONObject jsonObject) throws UnsupportedEncodingException {
     post(jsonEntity(jsonObject));
   }
+
+  public void patch(JSONArray jsonArray) throws UnsupportedEncodingException {
+    patch(jsonEntity(jsonArray));
+  }
+
+  public void patch(ExtendedJSONObject o) {
+    patch(jsonEntity(o));
+  }
+
+  public void patch(JSONObject jsonObject) throws UnsupportedEncodingException {
+    patch(jsonEntity(jsonObject));
+  }
 }
--- a/mobile/android/base/sync/net/Resource.java
+++ b/mobile/android/base/sync/net/Resource.java
@@ -10,10 +10,11 @@ import ch.boye.httpclientandroidlib.Http
 
 public interface Resource {
   public abstract URI getURI();
   public abstract String getURIString();
   public abstract String getHostname();
   public abstract void get();
   public abstract void delete();
   public abstract void post(HttpEntity body);
+  public abstract void patch(HttpEntity body);
   public abstract void put(HttpEntity body);
 }
--- a/mobile/android/base/sync/net/SyncStorageRequest.java
+++ b/mobile/android/base/sync/net/SyncStorageRequest.java
@@ -186,12 +186,17 @@ public class SyncStorageRequest implemen
   }
 
   @Override
   public void post(HttpEntity body) {
     this.resource.post(body);
   }
 
   @Override
+  public void patch(HttpEntity body) {
+    this.resource.patch(body);
+  }
+
+  @Override
   public void put(HttpEntity body) {
     this.resource.put(body);
   }
 }
--- a/mobile/android/base/tabqueue/TabQueueHelper.java
+++ b/mobile/android/base/tabqueue/TabQueueHelper.java
@@ -1,34 +1,45 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabqueue;
 
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.util.ThreadUtils;
+
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.support.v4.app.NotificationCompat;
+import android.util.Log;
 import org.json.JSONArray;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.ThreadUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
 
 public class TabQueueHelper {
     private static final String LOGTAG = "Gecko" + TabQueueHelper.class.getSimpleName();
 
     public static final String FILE_NAME = "tab_queue_url_list.json";
     public static final String LOAD_URLS_ACTION = "TAB_QUEUE_LOAD_URLS_ACTION";
     public static final int TAB_QUEUE_NOTIFICATION_ID = R.id.tabQueueNotification;
 
+    public static final String PREF_TAB_QUEUE_COUNT = "tab_queue_count";
+
     /**
      * Reads file and converts any content to JSON, adds passed in URL to the data and writes back to the file,
      * creating the file if it doesn't already exist.  This should not be run on the UI thread.
      *
      * @param profile
      * @param url      URL to add
      * @param filename filename to add URL to
      * @return the number of tabs currently queued
@@ -47,34 +58,92 @@ public class TabQueueHelper {
 
     /**
      * Displays a notification showing the total number of tabs queue.  If there is already a notification displayed, it
      * will be replaced.
      *
      * @param context
      * @param tabsQueued
      */
-    static public void showNotification(Context context, int tabsQueued) {
+    public static void showNotification(final Context context, final int tabsQueued) {
+        ThreadUtils.assertNotOnUiThread();
+
         Intent resultIntent = new Intent(context, BrowserApp.class);
         resultIntent.setAction(TabQueueHelper.LOAD_URLS_ACTION);
 
         PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, resultIntent, PendingIntent.FLAG_CANCEL_CURRENT);
 
         String title, text;
         final Resources resources = context.getResources();
-        if(tabsQueued == 1) {
+        if (tabsQueued == 1) {
             title = resources.getString(R.string.tab_queue_notification_title_singular);
             text = resources.getString(R.string.tab_queue_notification_text_singular);
         } else {
             title = resources.getString(R.string.tab_queue_notification_title_plural);
             text = resources.getString(R.string.tab_queue_notification_text_plural, tabsQueued);
         }
 
         NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
-                                                                   .setSmallIcon(R.drawable.ic_status_logo)
-                                                                   .setContentTitle(title)
-                                                                   .setContentText(text)
-                                                                   .setContentIntent(pendingIntent);
+                                                     .setSmallIcon(R.drawable.ic_status_logo)
+                                                     .setContentTitle(title)
+                                                     .setContentText(text)
+                                                     .setContentIntent(pendingIntent);
 
         NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
         notificationManager.notify(TabQueueHelper.TAB_QUEUE_NOTIFICATION_ID, builder.build());
     }
+
+    public static boolean shouldOpenTabQueueUrls(final Context context) {
+        ThreadUtils.assertNotOnUiThread();
+
+        // TODO: Use profile shared prefs when bug 1147925 gets fixed.
+        final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
+
+        boolean tabQueueEnabled = prefs.getBoolean(GeckoPreferences.PREFS_TAB_QUEUE, false);
+        int tabsQueued = prefs.getInt(PREF_TAB_QUEUE_COUNT, 0);
+
+        return tabQueueEnabled && tabsQueued > 0;
+    }
+
+    public static int getTabQueueLength(final Context context) {
+        ThreadUtils.assertNotOnUiThread();
+
+        // TODO: Use profile shared prefs when bug 1147925 gets fixed.
+        final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
+        return prefs.getInt(PREF_TAB_QUEUE_COUNT, 0);
+    }
+
+    public static void openQueuedUrls(final Context context, final GeckoProfile profile, final String filename) {
+        ThreadUtils.assertNotOnUiThread();
+
+        // Remove the notification.
+        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+        notificationManager.cancel(TAB_QUEUE_NOTIFICATION_ID);
+
+        // exit early if we don't have any tabs queued
+        if (getTabQueueLength(context) < 1) {
+            return;
+        }
+
+        JSONArray jsonArray = profile.readJSONArrayFromFile(filename);
+
+        if (jsonArray.length() > 0) {
+            JSONObject data = new JSONObject();
+            try {
+                data.put("urls", jsonArray);
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tabs:OpenMultiple", data.toString()));
+            } catch (JSONException e) {
+                // Don't exit early as we perform cleanup at the end of this function.
+                Log.e(LOGTAG, "Error sending tab queue data", e);
+            }
+        }
+
+        try {
+            profile.deleteFileFromProfileDir(filename);
+        } catch (IllegalArgumentException e) {
+            Log.e(LOGTAG, "Error deleting Tab Queue data file.", e);
+        }
+
+        // TODO: Use profile shared prefs when bug 1147925 gets fixed.
+        final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
+        prefs.edit().remove(PREF_TAB_QUEUE_COUNT).apply();
+    }
 }
\ No newline at end of file
--- a/mobile/android/base/tabqueue/TabQueueService.java
+++ b/mobile/android/base/tabqueue/TabQueueService.java
@@ -3,30 +3,32 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabqueue;
 
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.graphics.PixelFormat;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.WindowManager;
 import android.widget.Button;
 import android.widget.TextView;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.mozglue.ContextUtils;
 
 
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
 
@@ -160,16 +162,23 @@ public class TabQueueService extends Ser
         // As we're doing disk IO, let's run this stuff in a separate thread.
         executorService.submit(new Runnable() {
             @Override
             public void run() {
                 Context applicationContext = getApplicationContext();
                 final GeckoProfile profile = GeckoProfile.get(applicationContext);
                 int tabsQueued = TabQueueHelper.queueURL(profile, intentData, filename);
                 TabQueueHelper.showNotification(applicationContext, tabsQueued);
+
+                // Store the number of URLs queued so that we don't have to read and process the file to see if we have
+                // any urls to open.
+                // TODO: Use profile shared prefs when bug 1147925 gets fixed.
+                final SharedPreferences prefs = GeckoSharedPrefs.forApp(applicationContext);
+
+                prefs.edit().putInt(TabQueueHelper.PREF_TAB_QUEUE_COUNT, tabsQueued).apply();
             }
         });
     }
 
     @Override
     public void onDestroy() {
         super.onDestroy();
         tabQueueHandler = null;
--- a/mobile/android/base/tests/StringHelper.java
+++ b/mobile/android/base/tests/StringHelper.java
@@ -1,16 +1,15 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tests;
 
-
 public class StringHelper {
     private StringHelper() {}
 
     public static final String OK = "OK";
 
     // Note: DEFAULT_BOOKMARKS_TITLES.length == DEFAULT_BOOKMARKS_URLS.length
     public static final String[] DEFAULT_BOOKMARKS_TITLES = new String[] {
         "Firefox: About your browser",
@@ -50,16 +49,17 @@ public class StringHelper {
     public static final String CONTEXT_MENU_OPEN_IN_PRIVATE_TAB = "Open in Private Tab";
     public static final String CONTEXT_MENU_COPY_LINK = "Copy Link";
     public static final String CONTEXT_MENU_SHARE_LINK = "Share Link";
     public static final String CONTEXT_MENU_EDIT = "Edit";
     public static final String CONTEXT_MENU_SHARE = "Share";
     public static final String CONTEXT_MENU_REMOVE = "Remove";
     public static final String CONTEXT_MENU_COPY_ADDRESS = "Copy Address";
     public static final String CONTEXT_MENU_EDIT_SITE_SETTINGS = "Edit Site Settings";
+    public static final String CONTEXT_MENU_SITE_SETTINGS_SAVE_PASSWORD = "Save Password";
     public static final String CONTEXT_MENU_ADD_TO_HOME_SCREEN = "Add to Home Screen";
     public static final String CONTEXT_MENU_PIN_SITE = "Pin Site";
     public static final String CONTEXT_MENU_UNPIN_SITE = "Unpin Site";
 
     // Context Menu menu items
     public static final String[] CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB = new String[] {
         CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB,
         CONTEXT_MENU_COPY_LINK,
@@ -100,17 +100,18 @@ public class StringHelper {
     public static final String ROBOCOP_BIG_MAILTO_URL = "/robocop/robocop_big_mailto.html";
     public static final String ROBOCOP_BLANK_PAGE_01_URL = "/robocop/robocop_blank_01.html";
     public static final String ROBOCOP_BLANK_PAGE_02_URL = "/robocop/robocop_blank_02.html";
     public static final String ROBOCOP_BLANK_PAGE_03_URL = "/robocop/robocop_blank_03.html";
     public static final String ROBOCOP_BLANK_PAGE_04_URL = "/robocop/robocop_blank_04.html";
     public static final String ROBOCOP_BLANK_PAGE_05_URL = "/robocop/robocop_blank_05.html";
     public static final String ROBOCOP_BOXES_URL = "/robocop/robocop_boxes.html";
     public static final String ROBOCOP_GEOLOCATION_URL = "/robocop/robocop_geolocation.html";
-    public static final String ROBOCOP_LOGIN_URL = "/robocop/robocop_login.html";
+    public static final String ROBOCOP_LOGIN_01_URL= "/robocop/robocop_login_01.html";
+    public static final String ROBOCOP_LOGIN_02_URL= "/robocop/robocop_login_02.html";
     public static final String ROBOCOP_POPUP_URL = "/robocop/robocop_popup.html";
     public static final String ROBOCOP_OFFLINE_STORAGE_URL = "/robocop/robocop_offline_storage.html";
     public static final String ROBOCOP_PICTURE_LINK_URL = "/robocop/robocop_picture_link.html";
     public static final String ROBOCOP_SEARCH_URL = "/robocop/robocop_search.html";
     public static final String ROBOCOP_TEXT_PAGE_URL = "/robocop/robocop_text_page.html";
     public static final String ROBOCOP_ADOBE_FLASH_URL = "/robocop/robocop_adobe_flash.html";
     public static final String ROBOCOP_INPUT_URL = "/robocop/robocop_input.html";
     public static final String ROBOCOP_READER_MODE_BASIC_ARTICLE = "/robocop/reader_mode_pages/basic_article.html";
@@ -258,19 +259,19 @@ public class StringHelper {
     public static final String GEO_MESSAGE = "Share your location with";
     public static final String GEO_ALLOW = "Share";
     public static final String GEO_DENY = "Don't share";
 
     public static final String OFFLINE_MESSAGE = "to store data on your device for offline use";
     public static final String OFFLINE_ALLOW = "Allow";
     public static final String OFFLINE_DENY = "Don't allow";
 
-    public static final String LOGIN_MESSAGE = "Save password";
-    public static final String LOGIN_ALLOW = "Save";
-    public static final String LOGIN_DENY = "Don't save";
+    public static final String LOGIN_MESSAGE = "Would you like " + BRAND_NAME + " to remember this login?";
+    public static final String LOGIN_ALLOW = "Remember";
+    public static final String LOGIN_DENY = "Never";
 
     public static final String POPUP_MESSAGE = "prevented this site from opening";
     public static final String POPUP_ALLOW = "Show";
     public static final String POPUP_DENY = "Don't show";
 
     // Strings used as content description, e.g. for ImageButtons
     public static final String CONTENT_DESCRIPTION_READER_MODE_BUTTON = "Enter Reader View";
 }
rename from mobile/android/base/tests/robocop_login.html
rename to mobile/android/base/tests/robocop_login_01.html
--- a/mobile/android/base/tests/robocop_login.html
+++ b/mobile/android/base/tests/robocop_login_01.html
@@ -1,13 +1,13 @@
 <html>
 <script>
 function login(){
-document.login.username.value="Test";
-document.login.password.value="Test";
+document.login.username.value="Test1";
+document.login.password.value="Test2";
 document.getElementById('submit').click();
 }
 </script>
 <head>
   <title>Robocop Login</title>
   <meta charset="utf-8">
 </head>
 <body onload="login()">
copy from mobile/android/base/tests/robocop_login.html
copy to mobile/android/base/tests/robocop_login_02.html
--- a/mobile/android/base/tests/robocop_login.html
+++ b/mobile/android/base/tests/robocop_login_02.html
@@ -1,21 +1,21 @@
 <html>
 <script>
 function login(){
-document.login.username.value="Test";
-document.login.password.value="Test";
+document.login.username.value="Test2";
+document.login.password.value="Test2";
 document.getElementById('submit').click();
 }
 </script>
 <head>
   <title>Robocop Login</title>
   <meta charset="utf-8">
 </head>
 <body onload="login()">
   <h2>User Login </h2>
-  <form name="login" method="post" action="robocop_blank_01.html">
+  <form name="login" method="post" action="robocop_blank_02.html">
    Username: <input type="text" name="username" id="username"><br>
    Password: <input type="password" name="password" id="password"><br>
    <input type="submit" id="submit" name="submit" value="Login!">
   </form>
 </body>
 </html>
--- a/mobile/android/base/tests/testClearPrivateData.java
+++ b/mobile/android/base/tests/testClearPrivateData.java
@@ -73,22 +73,22 @@ public class testClearPrivateData extend
         checkOption(shareStrings[1], "Clear");
         checkOption(shareStrings[3], "Cancel");
         loadCheckDismiss(shareStrings[2], url, shareStrings[0]);
         checkOption(shareStrings[2], "Cancel");
         checkDevice(titleGeolocation, url);
     }
 
     public void clearPassword(){
-        String passwordStrings[] = {"Save password", "Save", "Don't save"};
+        String passwordStrings[] = { StringHelper.LOGIN_MESSAGE, StringHelper.LOGIN_ALLOW, StringHelper.LOGIN_DENY };
         String title = StringHelper.ROBOCOP_BLANK_PAGE_01_TITLE;
-        String loginUrl = getAbsoluteUrl(StringHelper.ROBOCOP_LOGIN_URL);
+        String loginUrl = getAbsoluteUrl(StringHelper.ROBOCOP_LOGIN_01_URL);
 
         loadCheckDismiss(passwordStrings[1], loginUrl, passwordStrings[0]);
-        checkOption(passwordStrings[1], "Clear");
+        checkOption(StringHelper.CONTEXT_MENU_SITE_SETTINGS_SAVE_PASSWORD, "Clear");
         loadCheckDismiss(passwordStrings[2], loginUrl, passwordStrings[0]);
         checkDevice(title, getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL));
     }
 
     // clear private data and verify the device type because for phone there is an extra back action to exit the settings menu
     public void checkDevice(final String title, final String url) {
         clearPrivateData();
         if (mDevice.type.equals("phone")) {
--- a/mobile/android/base/tests/testDoorHanger.java
+++ b/mobile/android/base/tests/testDoorHanger.java
@@ -19,17 +19,16 @@ import org.mozilla.gecko.Actions;
    * offline storage permission doorhangers - allowing and not allowing offline storage dismisses the doorhanger
    * Password Manager doorhangers - Remember and Not Now options dismiss the doorhanger
 */
 public class testDoorHanger extends BaseTest {
     public void testDoorHanger() {
         String GEO_URL = getAbsoluteUrl(StringHelper.ROBOCOP_GEOLOCATION_URL);
         String BLANK_URL = getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL);
         String OFFLINE_STORAGE_URL = getAbsoluteUrl(StringHelper.ROBOCOP_OFFLINE_STORAGE_URL);
-        String LOGIN_URL = getAbsoluteUrl(StringHelper.ROBOCOP_LOGIN_URL);
 
         blockForGeckoReady();
 
         // Test geolocation notification
         loadUrlAndWait(GEO_URL);
         waitForText(StringHelper.GEO_MESSAGE);
         mAsserter.is(mSolo.searchText(StringHelper.GEO_MESSAGE), true, "Geolocation doorhanger has been displayed");
 
@@ -58,17 +57,16 @@ public class testDoorHanger extends Base
 
         // Add a new tab
         addTab(BLANK_URL);
 
         // Make sure doorhanger is hidden
         mAsserter.is(mSolo.searchText(GEO_MESSAGE), false, "Geolocation doorhanger notification is hidden when opening a new tab");
         */
 
-
         boolean offlineAllowedByDefault = true;
         // Save offline-allow-by-default preferences first
         final String[] prefNames = { "offline-apps.allow_by_default" };
         final int ourRequestId = 0x7357;
         final Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
         mActions.sendPreferencesGetEvent(ourRequestId, prefNames);
         try {
             JSONObject data = null;
@@ -125,35 +123,34 @@ public class testDoorHanger extends Base
             jsonPref.put("name", "offline-apps.allow_by_default");
             jsonPref.put("type", "bool");
             jsonPref.put("value", offlineAllowedByDefault);
             setPreferenceAndWaitForChange(jsonPref);
         } catch (JSONException e) {
             mAsserter.ok(false, "exception setting preference", e.toString());
         }
 
+        // Load new login page
+        loadUrlAndWait(getAbsoluteUrl(StringHelper.ROBOCOP_LOGIN_01_URL));
+        waitForText(StringHelper.LOGIN_MESSAGE);
+
+        // Test doorhanger is dismissed when tapping "Remember".
+        mSolo.clickOnButton(StringHelper.LOGIN_ALLOW);
+        waitForTextDismissed(StringHelper.LOGIN_MESSAGE);
+        mAsserter.is(mSolo.searchText(StringHelper.LOGIN_MESSAGE), false, "Login doorhanger notification is hidden when allowing saving password");
 
         // Load login page
-        loadUrlAndWait(LOGIN_URL);
+        loadUrlAndWait(getAbsoluteUrl(StringHelper.ROBOCOP_LOGIN_02_URL));
         waitForText(StringHelper.LOGIN_MESSAGE);
 
-        // Test doorhanger is dismissed when tapping "Don't save"
+        // Test doorhanger is dismissed when tapping "Never".
         mSolo.clickOnButton(StringHelper.LOGIN_DENY);
         waitForTextDismissed(StringHelper.LOGIN_MESSAGE);
         mAsserter.is(mSolo.searchText(StringHelper.LOGIN_MESSAGE), false, "Login doorhanger notification is hidden when denying saving password");
 
-        // Load login page
-        loadUrlAndWait(LOGIN_URL);
-        waitForText(StringHelper.LOGIN_MESSAGE);
-
-        // Test doorhanger is dismissed when tapping "Save" and is no longer triggered
-        mSolo.clickOnButton(StringHelper.LOGIN_ALLOW);
-        waitForTextDismissed(StringHelper.LOGIN_MESSAGE);
-        mAsserter.is(mSolo.searchText(StringHelper.LOGIN_MESSAGE), false, "Login doorhanger notification is hidden when allowing saving password");
-
         testPopupBlocking();
     }
 
     private void testPopupBlocking() {
         String POPUP_URL = getAbsoluteUrl(StringHelper.ROBOCOP_POPUP_URL);
 
         try {
             JSONObject jsonPref = new JSONObject();
--- a/mobile/android/base/tests/testMasterPassword.java
+++ b/mobile/android/base/tests/testMasterPassword.java
@@ -175,17 +175,17 @@ public class testMasterPassword extends 
         }
         waitForText(StringHelper.SETTINGS_LABEL);
         mActions.sendSpecialKey(Actions.SpecialKey.BACK);// Close the Settings Menu
         // Make sure the settings menu has been closed.
         mAsserter.ok(mSolo.waitForText("Browser Blank Page 01"), "Waiting for blank browser page after exiting settings", "Blank browser page present");
     }
 
     public void verifyLoginPage(String password, String badPassword) {
-        String LOGIN_URL = getAbsoluteUrl("/robocop/robocop_login.html");
+        String LOGIN_URL = getAbsoluteUrl(StringHelper.ROBOCOP_LOGIN_01_URL);
         String option [] = {"Save", "Don't save"};
 
         doorhangerDisplayed(LOGIN_URL);// Check that the doorhanger is displayed
 
         // TODO: Remove this hack -- see bug 915449
         mSolo.sleep(2000);
 
         for (String item:option) {
--- a/mobile/android/base/toolbar/SiteIdentityPopup.java
+++ b/mobile/android/base/toolbar/SiteIdentityPopup.java
@@ -22,16 +22,17 @@ import org.json.JSONObject;
 
 import android.content.Context;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.TextView;
+import org.mozilla.gecko.widget.DoorhangerConfig;
 
 /**
  * SiteIdentityPopup is a singleton class that displays site identity data in
  * an arrow panel popup hanging from the lock icon in the browser toolbar.
  */
 public class SiteIdentityPopup extends AnchoredPopup {
     private static final String LOGTAG = "GeckoSiteIdentityPopup";
 
@@ -48,16 +49,18 @@ public class SiteIdentityPopup extends A
     private LinearLayout mIdentityKnownContainer;
     private LinearLayout mIdentityUnknownContainer;
 
     private TextView mHost;
     private TextView mOwnerLabel;
     private TextView mOwner;
     private TextView mVerifier;
 
+    private View mDivider;
+
     private DoorHanger mMixedContentNotification;
     private DoorHanger mTrackingContentNotification;
 
     private final OnButtonClickListener mButtonClickListener;
 
     public SiteIdentityPopup(Context context) {
         super(context);
 
@@ -80,16 +83,17 @@ public class SiteIdentityPopup extends A
                 (LinearLayout) mIdentity.findViewById(R.id.site_identity_known_container);
         mIdentityUnknownContainer =
                 (LinearLayout) mIdentity.findViewById(R.id.site_identity_unknown_container);
 
         mHost = (TextView) mIdentityKnownContainer.findViewById(R.id.host);
         mOwnerLabel = (TextView) mIdentityKnownContainer.findViewById(R.id.owner_label);
         mOwner = (TextView) mIdentityKnownContainer.findViewById(R.id.owner);
         mVerifier = (TextView) mIdentityKnownContainer.findViewById(R.id.verifier);
+        mDivider = mIdentity.findViewById(R.id.divider_doorhanger);
     }
 
     private void updateIdentity(final SiteIdentity siteIdentity) {
         if (!mInflated) {
             init();
         }
 
         final boolean isIdentityKnown = (siteIdentity.getSecurityMode() != SecurityMode.UNKNOWN);
@@ -132,79 +136,84 @@ public class SiteIdentityPopup extends A
         final String verifier = siteIdentity.getVerifier();
         final String encrypted = siteIdentity.getEncrypted();
         mVerifier.setText(verifier + "\n" + encrypted);
     }
 
     private void addMixedContentNotification(boolean blocked) {
         // Remove any existing mixed content notification.
         removeMixedContentNotification();
-        mMixedContentNotification = new DoorHanger(mContext, DoorHanger.Theme.DARK);
 
+        final DoorhangerConfig config = new DoorhangerConfig();
         int icon;
-        String message;
         if (blocked) {
             icon = R.drawable.shield_enabled_doorhanger;
-            message = mContext.getString(R.string.blocked_mixed_content_message_top) + "\n\n" +
-                      mContext.getString(R.string.blocked_mixed_content_message_bottom);
+            config.setMessage(mContext.getString(R.string.blocked_mixed_content_message_top) + "\n\n" +
+                      mContext.getString(R.string.blocked_mixed_content_message_bottom));
         } else {
             icon = R.drawable.shield_disabled_doorhanger;
-            message = mContext.getString(R.string.loaded_mixed_content_message);
+            config.setMessage(mContext.getString(R.string.loaded_mixed_content_message));
         }
 
+        config.setLink(mContext.getString(R.string.learn_more), MIXED_CONTENT_SUPPORT_URL, "\n\n");
+        config.setType(DoorHanger.Type.SITE);
+        mMixedContentNotification = DoorHanger.Get(mContext, config);
         mMixedContentNotification.setIcon(icon);
-        mMixedContentNotification.setMessage(message);
-        mMixedContentNotification.addLink(mContext.getString(R.string.learn_more), MIXED_CONTENT_SUPPORT_URL, "\n\n");
 
         addNotificationButtons(mMixedContentNotification, blocked);
 
         mContent.addView(mMixedContentNotification);
+        mDivider.setVisibility(View.VISIBLE);
     }
 
     private void removeMixedContentNotification() {
         if (mMixedContentNotification != null) {
             mContent.removeView(mMixedContentNotification);
             mMixedContentNotification = null;
         }
     }
 
     private void addTrackingContentNotification(boolean blocked) {
         // Remove any existing tracking content notification.
         removeTrackingContentNotification();
-        mTrackingContentNotification = new DoorHanger(mContext, DoorHanger.Theme.DARK);
+
+        final DoorhangerConfig config = new DoorhangerConfig();
 
         int icon;
-        String message;
         if (blocked) {
             icon = R.drawable.shield_enabled_doorhanger;
-            message = mContext.getString(R.string.blocked_tracking_content_message_top) + "\n\n" +
-                      mContext.getString(R.string.blocked_tracking_content_message_bottom);
+            config.setMessage(mContext.getString(R.string.blocked_tracking_content_message_top) + "\n\n" +
+                      mContext.getString(R.string.blocked_tracking_content_message_bottom));
         } else {
             icon = R.drawable.shield_disabled_doorhanger;
-            message = mContext.getString(R.string.loaded_tracking_content_message_top) + "\n\n" +
-                      mContext.getString(R.string.loaded_tracking_content_message_bottom);
+            config.setMessage(mContext.getString(R.string.loaded_tracking_content_message_top) + "\n\n" +
+                      mContext.getString(R.string.loaded_tracking_content_message_bottom));
         }
 
+        config.setLink(mContext.getString(R.string.learn_more), TRACKING_CONTENT_SUPPORT_URL, "\n\n");
+        config.setType(DoorHanger.Type.SITE);
+        mTrackingContentNotification = DoorHanger.Get(mContext, config);
+
         mTrackingContentNotification.setIcon(icon);
-        mTrackingContentNotification.setMessage(message);
-        mTrackingContentNotification.addLink(mContext.getString(R.string.learn_more), TRACKING_CONTENT_SUPPORT_URL, "\n\n");
 
         addNotificationButtons(mTrackingContentNotification, blocked);
 
         mContent.addView(mTrackingContentNotification);
+        mDivider.setVisibility(View.VISIBLE);
     }
 
     private void removeTrackingContentNotification() {
         if (mTrackingContentNotification != null) {
             mContent.removeView(mTrackingContentNotification);
             mTrackingContentNotification = null;
         }
     }
 
     private void addNotificationButtons(DoorHanger dh, boolean blocked) {
+        // TODO: Add support for buttons in DoorHangerConfig.
         if (blocked) {
             dh.addButton(mContext.getString(R.string.disable_protection), "disable", mButtonClickListener);
             dh.addButton(mContext.getString(R.string.keep_blocking), "keepBlocking", mButtonClickListener);
         } else {
             dh.addButton(mContext.getString(R.string.enable_protection), "enable", mButtonClickListener);
         }
     }
 
@@ -271,16 +280,17 @@ public class SiteIdentityPopup extends A
         }
     }
 
     @Override
     public void dismiss() {
         super.dismiss();
         removeMixedContentNotification();
         removeTrackingContentNotification();
+        mDivider.setVisibility(View.GONE);
     }
 
     private class PopupButtonListener implements OnButtonClickListener {
         @Override
         public void onButtonClick(DoorHanger dh, String tag) {
             try {
                 JSONObject data = new JSONObject();
                 data.put("allowContent", tag.equals("disable"));
copy from mobile/android/base/widget/DoorHanger.java
copy to mobile/android/base/widget/DefaultDoorHanger.java
--- a/mobile/android/base/widget/DoorHanger.java
+++ b/mobile/android/base/widget/DefaultDoorHanger.java
@@ -1,244 +1,87 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.widget;
 
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.prompts.PromptInput;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.Rect;
-import android.text.SpannableString;
-import android.text.Spanned;
-import android.text.Html;
 import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.URLSpan;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.Button;
 import android.widget.CheckBox;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.Spinner;
-import android.widget.SpinnerAdapter;
-import android.widget.TextView;
 
 import java.util.ArrayList;
 import java.util.List;
 
-public class DoorHanger extends LinearLayout {
-    private static final String LOGTAG = "GeckoDoorHanger";
-
-    private static int sInputPadding = -1;
-    private static int sSpinnerTextColor = -1;
-    private static int sSpinnerTextSize = -1;
-
-    private static final LayoutParams sButtonParams;
-    static {
-        sButtonParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f);
-    }
-
-    private final TextView mTextView;
-    private final ImageView mIcon;
-    private final LinearLayout mChoicesLayout;
-
-    // Divider between doorhangers.
-    private final View mDivider;
-
-    // The tab associated with this notification.
-    private final int mTabId;
-
-    // Value used to identify the notification.
-    private final String mValue;
+public class DefaultDoorHanger extends DoorHanger {
+    private static final String LOGTAG = "GeckoDefaultDoorHanger";
 
     private final Resources mResources;
+    private static int sSpinnerTextColor = -1;
 
     private List<PromptInput> mInputs;
     private CheckBox mCheckBox;
 
-    private int mPersistence;
-    private boolean mPersistWhileVisible;
-    private long mTimeout;
-
-    // Color used for dividers above and between buttons.
-    private int mDividerColor;
-
-    public static enum Theme {
-        LIGHT,
-        DARK
+    public DefaultDoorHanger(Context context, DoorhangerConfig config) {
+        this(context, config, Type.DEFAULT);
     }
 
-    public interface OnButtonClickListener {
-        public void onButtonClick(DoorHanger dh, String tag);
-    }
-
-    public DoorHanger(Context context, Theme theme) {
-        this(context, 0, null, theme);
-    }
+    public DefaultDoorHanger(Context context, DoorhangerConfig config, Type type) {
+        super(context, config, type);
 
-    public DoorHanger(Context context, int tabId, String value) {
-        this(context, tabId, value, Theme.LIGHT);
-    }
-
-    private DoorHanger(Context context, int tabId, String value, Theme theme) {
-        super(context);
-
-        mTabId = tabId;
-        mValue = value;
         mResources = getResources();
 
-        if (sInputPadding == -1) {
-            sInputPadding = mResources.getDimensionPixelSize(R.dimen.doorhanger_padding);
+        if (sSpinnerTextColor == -1) {
+            sSpinnerTextColor = getResources().getColor(R.color.text_color_primary_disable_only);
         }
-        if (sSpinnerTextColor == -1) {
-            sSpinnerTextColor = mResources.getColor(R.color.text_color_primary_disable_only);
-        }
-        if (sSpinnerTextSize == -1) {
-            sSpinnerTextSize = mResources.getDimensionPixelSize(R.dimen.doorhanger_spinner_textsize);
+        loadConfig(config);
+    }
+
+    @Override
+    protected void loadConfig(DoorhangerConfig config) {
+        final String message = config.getMessage();
+        if (message != null) {
+            setMessage(message);
         }
 
-        setOrientation(VERTICAL);
-
-        LayoutInflater.from(context).inflate(R.layout.doorhanger, this);
-        mTextView = (TextView) findViewById(R.id.doorhanger_title);
-        mIcon = (ImageView) findViewById(R.id.doorhanger_icon);
-        mChoicesLayout = (LinearLayout) findViewById(R.id.doorhanger_choices);
-        mDivider = findViewById(R.id.divider_doorhanger);
-
-        setTheme(theme);
-    }
+        final JSONObject options = config.getOptions();
+        if (options != null) {
+            setOptions(options);
+        }
 
-    private void setTheme(Theme theme) {
-        if (theme == Theme.LIGHT) {
-            // The default styles declared in doorhanger.xml are light-themed, so we just
-            // need to set the divider color that we'll use in addButton.
-            mDividerColor = mResources.getColor(R.color.divider_light);
-
-        } else if (theme == Theme.DARK) {
-            mDividerColor = mResources.getColor(R.color.divider_dark);
-
-            // Set a dark background, and use a smaller text size for dark-themed DoorHangers.
-            setBackgroundColor(mResources.getColor(R.color.doorhanger_background_dark));
-            mTextView.setTextAppearance(getContext(), R.style.TextAppearance_Widget_DoorHanger_Small);
-
-            // Set the inter-doorhanger divider color
-            mDivider.setBackgroundColor(mDividerColor);
+        final DoorhangerConfig.Link link = config.getLink();
+        if (link != null) {
+            addLink(link.label, link.url, link.delimiter);
         }
     }
 
-    public int getTabId() {
-        return mTabId;
-    }
-
-    public String getValue() {
-        return mValue;
-    }
-
+    @Override
     public List<PromptInput> getInputs() {
         return mInputs;
     }
 
+    @Override
     public CheckBox getCheckBox() {
         return mCheckBox;
     }
 
-    public void showDivider() {
-        mDivider.setVisibility(View.VISIBLE);
-    }
-
-    public void hideDivider() {
-        mDivider.setVisibility(View.GONE);
-    }
-
-    public void setMessage(String message) {
-        Spanned markupMessage = Html.fromHtml(message);
-        mTextView.setMovementMethod(LinkMovementMethod.getInstance()); // Necessary for clickable links
-        mTextView.setText(markupMessage);
-    }
-
-    public void setIcon(int resId) {
-        mIcon.setImageResource(resId);
-        mIcon.setVisibility(View.VISIBLE);
-    }
-
-    public void addLink(String label, String url, String delimiter) {
-        String title = mTextView.getText().toString();
-        SpannableString titleWithLink = new SpannableString(title + delimiter + label);
-        URLSpan linkSpan = new URLSpan(url) {
-            @Override
-            public void onClick(View view) {
-                Tabs.getInstance().loadUrlInTab(getURL());
-            }
-        };
-
-        // Prevent text outside the link from flashing when clicked.
-        ForegroundColorSpan colorSpan = new ForegroundColorSpan(mTextView.getCurrentTextColor());
-        titleWithLink.setSpan(colorSpan, 0, title.length(), 0);
-
-        titleWithLink.setSpan(linkSpan, title.length() + 1, titleWithLink.length(), 0);
-        mTextView.setText(titleWithLink);
-        mTextView.setMovementMethod(LinkMovementMethod.getInstance());
-    }
-
-    public void addButton(final String text, final String tag, final OnButtonClickListener listener) {
-        final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null);
-        button.setText(text);
-        button.setTag(tag);
-
-        button.setOnClickListener(new Button.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                listener.onButtonClick(DoorHanger.this, tag);
-            }
-        });
-
-        if (mChoicesLayout.getChildCount() == 0) {
-            // If this is the first button we're adding, make the choices layout visible.
-            mChoicesLayout.setVisibility(View.VISIBLE);
-            // Make the divider above the buttons visible.
-            View divider = findViewById(R.id.divider_choices);
-            divider.setVisibility(View.VISIBLE);
-            divider.setBackgroundColor(mDividerColor);
-        } else {
-            // Add a vertical divider between additional buttons.
-            Divider divider = new Divider(getContext(), null);
-            divider.setOrientation(Divider.Orientation.VERTICAL);
-            divider.setBackgroundColor(mDividerColor);
-            mChoicesLayout.addView(divider);
-        }
-
-        mChoicesLayout.addView(button, sButtonParams);
-    }
-
+    @Override
     public void setOptions(final JSONObject options) {
-        final int persistence = options.optInt("persistence");
-        if (persistence > 0) {
-            mPersistence = persistence;
-        }
-
-        mPersistWhileVisible = options.optBoolean("persistWhileVisible");
-
-        final long timeout = options.optLong("timeout");
-        if (timeout > 0) {
-            mTimeout = timeout;
-        }
-
+        super.setOptions(options);
         final JSONObject link = options.optJSONObject("link");
         if (link != null) {
             try {
                 final String linkLabel = link.getString("label");
                 final String linkUrl = link.getString("url");
                 addLink(linkLabel, linkUrl, " ");
             } catch (JSONException e) { }
         }
@@ -250,104 +93,40 @@ public class DoorHanger extends LinearLa
             final ViewGroup group = (ViewGroup) findViewById(R.id.doorhanger_inputs);
             group.setVisibility(VISIBLE);
 
             for (int i = 0; i < inputs.length(); i++) {
                 try {
                     PromptInput input = PromptInput.getInput(inputs.getJSONObject(i));
                     mInputs.add(input);
 
+                    final int padding = mResources.getDimensionPixelSize(R.dimen.doorhanger_padding);
                     View v = input.getView(getContext());
                     styleInput(input, v);
+                    v.setPadding(0, 0, 0, padding);
                     group.addView(v);
                 } catch(JSONException ex) { }
             }
         }
 
         final String checkBoxText = options.optString("checkbox");
         if (!TextUtils.isEmpty(checkBoxText)) {
             mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox);
             mCheckBox.setText(checkBoxText);
             mCheckBox.setVisibility(VISIBLE);
         }
     }
 
     private void styleInput(PromptInput input, View view) {
         if (input instanceof PromptInput.MenulistInput) {
-            styleSpinner(input, view);
-        } else {
-            // add some top and bottom padding to separate inputs
-            view.setPadding(0, sInputPadding,
-                            0, sInputPadding);
+            styleDropdownInputs(input, view);
         }
+        view.setPadding(0, 0, 0, mResources.getDimensionPixelSize(R.dimen.doorhanger_padding));
     }
 
-    private void styleSpinner(PromptInput input, View view) {
+    private void styleDropdownInputs(PromptInput input, View view) {
         PromptInput.MenulistInput spinInput = (PromptInput.MenulistInput) input;
 
-        /* Spinners have some intrinsic padding. To force the spinner's text to line up with
-         * the doorhanger text, we have to take that padding into account.
-         * 
-         * |-----A-------| <-- Normal doorhanger message
-         * |-B-|---C+D---| <-- (optional) Spinner Label
-         * |-B-|-C-|--D--| <-- Spinner
-         *
-         * A - Desired padding (sInputPadding)
-         * B - Final padding applied to input element (sInputPadding - rect.left - textPadding).
-         * C - Spinner background drawable padding (rect.left).
-         * D - Spinner inner TextView padding (textPadding).
-         */
-
-        // First get the padding of the selected view inside the spinner. Since the spinner
-        // hasn't been shown yet, we get this view directly from the adapter.
-        Spinner spinner = spinInput.spinner;
-        SpinnerAdapter adapter = spinner.getAdapter();
-        View dropView = adapter.getView(0, null, spinner);
-        int textPadding = 0;
-        if (dropView != null) {
-            textPadding = dropView.getPaddingLeft();
-        }
-
-        // Then get the intrinsic padding built into the background image of the spinner.
-        Rect rect = new Rect();
-        spinner.getBackground().getPadding(rect);
-
-        // Set the difference in padding to the spinner view to align it with doorhanger text.
-        view.setPadding(sInputPadding - rect.left - textPadding, 0, rect.right, sInputPadding);
-
         if (spinInput.textView != null) {
             spinInput.textView.setTextColor(sSpinnerTextColor);
-            spinInput.textView.setTextSize(sSpinnerTextSize);
-
-            // If this spinner has a label, offset it to also be aligned with the doorhanger text.
-            spinInput.textView.setPadding(rect.left + textPadding, 0, 0, 0);
         }
     }
-
-
-    /*
-     * Checks with persistence and timeout options to see if it's okay to remove a doorhanger.
-     *
-     * @param isShowing Whether or not this doorhanger is currently visible to the user.
-     *                 (e.g. the DoorHanger view might be VISIBLE, but its parent could be hidden)
-     */
-    public boolean shouldRemove(boolean isShowing) {
-        if (mPersistWhileVisible && isShowing) {
-            // We still want to decrement mPersistence, even if the popup is showing
-            if (mPersistence != 0)
-                mPersistence--;
-            return false;
-        }
-
-        // If persistence is set to -1, the doorhanger will never be
-        // automatically removed.
-        if (mPersistence != 0) {
-            mPersistence--;
-            return false;
-        }
-
-        if (System.currentTimeMillis() <= mTimeout) {
-            return false;
-        }
-
-        return true;
-    }
 }
--- a/mobile/android/base/widget/DoorHanger.java
+++ b/mobile/android/base/widget/DoorHanger.java
@@ -1,353 +1,235 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.widget;
 
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.prompts.PromptInput;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
 import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
+import android.text.Html;
 import android.text.SpannableString;
 import android.text.Spanned;
-import android.text.Html;
-import android.text.TextUtils;
 import android.text.method.LinkMovementMethod;
 import android.text.style.ForegroundColorSpan;
 import android.text.style.URLSpan;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
-import android.widget.Spinner;
-import android.widget.SpinnerAdapter;
 import android.widget.TextView;
+import org.json.JSONObject;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.prompts.PromptInput;
 
-import java.util.ArrayList;
 import java.util.List;
 
-public class DoorHanger extends LinearLayout {
-    private static final String LOGTAG = "GeckoDoorHanger";
+public abstract class DoorHanger extends LinearLayout {
 
-    private static int sInputPadding = -1;
-    private static int sSpinnerTextColor = -1;
-    private static int sSpinnerTextSize = -1;
+    public static DoorHanger Get(Context context, DoorhangerConfig config) {
+        final Type type = config.getType();
+        if (type != null) {
+            switch (type) {
+                case LOGIN:
+                    return new LoginDoorHanger(context, config);
+                case SITE:
+                    return new DefaultDoorHanger(context, config, type);
+            }
+        }
+
+        return new DefaultDoorHanger(context, config);
+    }
+
+    public static enum Type { DEFAULT, LOGIN, SITE }
+
+    public interface OnButtonClickListener {
+        public void onButtonClick(DoorHanger dh, String tag);
+    }
 
     private static final LayoutParams sButtonParams;
     static {
         sButtonParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f);
     }
 
-    private final TextView mTextView;
-    private final ImageView mIcon;
-    private final LinearLayout mChoicesLayout;
+    private static final String LOGTAG = "GeckoDoorHanger";
 
     // Divider between doorhangers.
     private final View mDivider;
 
-    // The tab associated with this notification.
+    private final LinearLayout mButtonsContainer;
+
+    // The tab this doorhanger is associated with.
     private final int mTabId;
 
-    // Value used to identify the notification.
-    private final String mValue;
+    // DoorHanger identifier.
+    private final String mIdentifier;
 
-    private final Resources mResources;
+    private final ImageView mIcon;
+    private final TextView mMessage;
+
+    protected Context mContext;
+
+    protected int mDividerColor;
 
-    private List<PromptInput> mInputs;
-    private CheckBox mCheckBox;
+    protected boolean mPersistWhileVisible;
+    protected int mPersistenceCount;
+    protected long mTimeout;
 
-    private int mPersistence;
-    private boolean mPersistWhileVisible;
-    private long mTimeout;
-
-    // Color used for dividers above and between buttons.
-    private int mDividerColor;
+    protected DoorHanger(Context context, DoorhangerConfig config, Type type) {
+        super(context);
+        mContext = context;
+        mTabId = config.getTabId();
+        mIdentifier = config.getId();
 
-    public static enum Theme {
-        LIGHT,
-        DARK
-    }
+        int resource;
+        switch (type) {
+            case LOGIN:
+                resource = R.layout.login_doorhanger;
+                break;
+            default:
+                resource = R.layout.doorhanger;
+        }
 
-    public interface OnButtonClickListener {
-        public void onButtonClick(DoorHanger dh, String tag);
-    }
+        LayoutInflater.from(context).inflate(resource, this);
+        mDivider = findViewById(R.id.divider_doorhanger);
+        mIcon = (ImageView) findViewById(R.id.doorhanger_icon);
+        mMessage = (TextView) findViewById(R.id.doorhanger_message);
+        if (type == Type.SITE) {
+            mMessage.setTextAppearance(getContext(), R.style.TextAppearance_DoorHanger_Small);
+        }
+        mButtonsContainer = (LinearLayout) findViewById(R.id.doorhanger_buttons);
 
-    public DoorHanger(Context context, Theme theme) {
-        this(context, 0, null, theme);
-    }
-
-    public DoorHanger(Context context, int tabId, String value) {
-        this(context, tabId, value, Theme.LIGHT);
+        mDividerColor = getResources().getColor(R.color.divider_light);
+        setOrientation(VERTICAL);
     }
 
-    private DoorHanger(Context context, int tabId, String value, Theme theme) {
-        super(context);
-
-        mTabId = tabId;
-        mValue = value;
-        mResources = getResources();
+    abstract protected void loadConfig(DoorhangerConfig config);
 
-        if (sInputPadding == -1) {
-            sInputPadding = mResources.getDimensionPixelSize(R.dimen.doorhanger_padding);
-        }
-        if (sSpinnerTextColor == -1) {
-            sSpinnerTextColor = mResources.getColor(R.color.text_color_primary_disable_only);
-        }
-        if (sSpinnerTextSize == -1) {
-            sSpinnerTextSize = mResources.getDimensionPixelSize(R.dimen.doorhanger_spinner_textsize);
+    protected void setOptions(final JSONObject options) {
+        final int persistence = options.optInt("persistence");
+        if (persistence > 0) {
+            mPersistenceCount = persistence;
         }
 
-        setOrientation(VERTICAL);
-
-        LayoutInflater.from(context).inflate(R.layout.doorhanger, this);
-        mTextView = (TextView) findViewById(R.id.doorhanger_title);
-        mIcon = (ImageView) findViewById(R.id.doorhanger_icon);
-        mChoicesLayout = (LinearLayout) findViewById(R.id.doorhanger_choices);
-        mDivider = findViewById(R.id.divider_doorhanger);
-
-        setTheme(theme);
-    }
+        mPersistWhileVisible = options.optBoolean("persistWhileVisible");
 
-    private void setTheme(Theme theme) {
-        if (theme == Theme.LIGHT) {
-            // The default styles declared in doorhanger.xml are light-themed, so we just
-            // need to set the divider color that we'll use in addButton.
-            mDividerColor = mResources.getColor(R.color.divider_light);
-
-        } else if (theme == Theme.DARK) {
-            mDividerColor = mResources.getColor(R.color.divider_dark);
-
-            // Set a dark background, and use a smaller text size for dark-themed DoorHangers.
-            setBackgroundColor(mResources.getColor(R.color.doorhanger_background_dark));
-            mTextView.setTextAppearance(getContext(), R.style.TextAppearance_Widget_DoorHanger_Small);
-
-            // Set the inter-doorhanger divider color
-            mDivider.setBackgroundColor(mDividerColor);
+        final long timeout = options.optLong("timeout");
+        if (timeout > 0) {
+            mTimeout = timeout;
         }
-    }
+   }
 
     public int getTabId() {
         return mTabId;
     }
 
-    public String getValue() {
-        return mValue;
-    }
-
-    public List<PromptInput> getInputs() {
-        return mInputs;
-    }
-
-    public CheckBox getCheckBox() {
-        return mCheckBox;
+    public String getIdentifier() {
+        return mIdentifier;
     }
 
     public void showDivider() {
         mDivider.setVisibility(View.VISIBLE);
     }
 
     public void hideDivider() {
         mDivider.setVisibility(View.GONE);
     }
 
-    public void setMessage(String message) {
-        Spanned markupMessage = Html.fromHtml(message);
-        mTextView.setMovementMethod(LinkMovementMethod.getInstance()); // Necessary for clickable links
-        mTextView.setText(markupMessage);
-    }
-
     public void setIcon(int resId) {
         mIcon.setImageResource(resId);
         mIcon.setVisibility(View.VISIBLE);
     }
 
-    public void addLink(String label, String url, String delimiter) {
-        String title = mTextView.getText().toString();
+    protected void setMessage(String message) {
+        Spanned markupMessage = Html.fromHtml(message);
+        mMessage.setText(markupMessage);
+    }
+
+    protected void addLink(String label, String url, String delimiter) {
+        String title = mMessage.getText().toString();
         SpannableString titleWithLink = new SpannableString(title + delimiter + label);
         URLSpan linkSpan = new URLSpan(url) {
             @Override
             public void onClick(View view) {
                 Tabs.getInstance().loadUrlInTab(getURL());
             }
         };
 
         // Prevent text outside the link from flashing when clicked.
-        ForegroundColorSpan colorSpan = new ForegroundColorSpan(mTextView.getCurrentTextColor());
+        ForegroundColorSpan colorSpan = new ForegroundColorSpan(mMessage.getCurrentTextColor());
         titleWithLink.setSpan(colorSpan, 0, title.length(), 0);
 
         titleWithLink.setSpan(linkSpan, title.length() + 1, titleWithLink.length(), 0);
-        mTextView.setText(titleWithLink);
-        mTextView.setMovementMethod(LinkMovementMethod.getInstance());
+        mMessage.setText(titleWithLink);
+        mMessage.setMovementMethod(LinkMovementMethod.getInstance());
     }
 
     public void addButton(final String text, final String tag, final OnButtonClickListener listener) {
         final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null);
         button.setText(text);
         button.setTag(tag);
 
         button.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
                 listener.onButtonClick(DoorHanger.this, tag);
             }
         });
 
-        if (mChoicesLayout.getChildCount() == 0) {
+        if (mButtonsContainer.getChildCount() == 0) {
             // If this is the first button we're adding, make the choices layout visible.
-            mChoicesLayout.setVisibility(View.VISIBLE);
+            mButtonsContainer.setVisibility(View.VISIBLE);
             // Make the divider above the buttons visible.
-            View divider = findViewById(R.id.divider_choices);
+            View divider = findViewById(R.id.divider_buttons);
             divider.setVisibility(View.VISIBLE);
-            divider.setBackgroundColor(mDividerColor);
         } else {
             // Add a vertical divider between additional buttons.
             Divider divider = new Divider(getContext(), null);
             divider.setOrientation(Divider.Orientation.VERTICAL);
             divider.setBackgroundColor(mDividerColor);
-            mChoicesLayout.addView(divider);
-        }
-
-        mChoicesLayout.addView(button, sButtonParams);
-    }
-
-    public void setOptions(final JSONObject options) {
-        final int persistence = options.optInt("persistence");
-        if (persistence > 0) {
-            mPersistence = persistence;
-        }
-
-        mPersistWhileVisible = options.optBoolean("persistWhileVisible");
-
-        final long timeout = options.optLong("timeout");
-        if (timeout > 0) {
-            mTimeout = timeout;
-        }
-
-        final JSONObject link = options.optJSONObject("link");
-        if (link != null) {
-            try {
-                final String linkLabel = link.getString("label");
-                final String linkUrl = link.getString("url");
-                addLink(linkLabel, linkUrl, " ");
-            } catch (JSONException e) { }
-        }
-
-        final JSONArray inputs = options.optJSONArray("inputs");
-        if (inputs != null) {
-            mInputs = new ArrayList<PromptInput>();
-
-            final ViewGroup group = (ViewGroup) findViewById(R.id.doorhanger_inputs);
-            group.setVisibility(VISIBLE);
-
-            for (int i = 0; i < inputs.length(); i++) {
-                try {
-                    PromptInput input = PromptInput.getInput(inputs.getJSONObject(i));
-                    mInputs.add(input);
-
-                    View v = input.getView(getContext());
-                    styleInput(input, v);
-                    group.addView(v);
-                } catch(JSONException ex) { }
-            }
+            mButtonsContainer.addView(divider);
         }
 
-        final String checkBoxText = options.optString("checkbox");
-        if (!TextUtils.isEmpty(checkBoxText)) {
-            mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox);
-            mCheckBox.setText(checkBoxText);
-            mCheckBox.setVisibility(VISIBLE);
-        }
-    }
-
-    private void styleInput(PromptInput input, View view) {
-        if (input instanceof PromptInput.MenulistInput) {
-            styleSpinner(input, view);
-        } else {
-            // add some top and bottom padding to separate inputs
-            view.setPadding(0, sInputPadding,
-                            0, sInputPadding);
-        }
+        mButtonsContainer.addView(button, sButtonParams);
     }
 
-    private void styleSpinner(PromptInput input, View view) {
-        PromptInput.MenulistInput spinInput = (PromptInput.MenulistInput) input;
-
-        /* Spinners have some intrinsic padding. To force the spinner's text to line up with
-         * the doorhanger text, we have to take that padding into account.
-         * 
-         * |-----A-------| <-- Normal doorhanger message
-         * |-B-|---C+D---| <-- (optional) Spinner Label
-         * |-B-|-C-|--D--| <-- Spinner
-         *
-         * A - Desired padding (sInputPadding)
-         * B - Final padding applied to input element (sInputPadding - rect.left - textPadding).
-         * C - Spinner background drawable padding (rect.left).
-         * D - Spinner inner TextView padding (textPadding).
-         */
-
-        // First get the padding of the selected view inside the spinner. Since the spinner
-        // hasn't been shown yet, we get this view directly from the adapter.
-        Spinner spinner = spinInput.spinner;
-        SpinnerAdapter adapter = spinner.getAdapter();
-        View dropView = adapter.getView(0, null, spinner);
-        int textPadding = 0;
-        if (dropView != null) {
-            textPadding = dropView.getPaddingLeft();
-        }
-
-        // Then get the intrinsic padding built into the background image of the spinner.
-        Rect rect = new Rect();
-        spinner.getBackground().getPadding(rect);
-
-        // Set the difference in padding to the spinner view to align it with doorhanger text.
-        view.setPadding(sInputPadding - rect.left - textPadding, 0, rect.right, sInputPadding);
-
-        if (spinInput.textView != null) {
-            spinInput.textView.setTextColor(sSpinnerTextColor);
-            spinInput.textView.setTextSize(sSpinnerTextSize);
-
-            // If this spinner has a label, offset it to also be aligned with the doorhanger text.
-            spinInput.textView.setPadding(rect.left + textPadding, 0, 0, 0);
-        }
-    }
-
-
     /*
      * Checks with persistence and timeout options to see if it's okay to remove a doorhanger.
      *
      * @param isShowing Whether or not this doorhanger is currently visible to the user.
      *                 (e.g. the DoorHanger view might be VISIBLE, but its parent could be hidden)
      */
     public boolean shouldRemove(boolean isShowing) {
         if (mPersistWhileVisible && isShowing) {
             // We still want to decrement mPersistence, even if the popup is showing
-            if (mPersistence != 0)
-                mPersistence--;
+            if (mPersistenceCount != 0)
+                mPersistenceCount--;
             return false;
         }
 
         // If persistence is set to -1, the doorhanger will never be
         // automatically removed.
-        if (mPersistence != 0) {
-            mPersistence--;
+        if (mPersistenceCount != 0) {
+            mPersistenceCount--;
             return false;
         }
 
         if (System.currentTimeMillis() <= mTimeout) {
             return false;
         }
 
         return true;
     }
+
+    // TODO: remove and expose through instance Button Handler.
+    public List<PromptInput> getInputs() {
+        return null;
+    }
+
+    public CheckBox getCheckBox() {
+        return null;
+    }
+
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/DoorhangerConfig.java
@@ -0,0 +1,93 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.widget;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.widget.DoorHanger.Type;
+
+public class DoorhangerConfig {
+
+    public static class Link {
+        public final String label;
+        public final String url;
+        public final String delimiter;
+
+        private Link(String label, String url, String delimiter) {
+            this.label = label;
+            this.url = url;
+            this.delimiter = delimiter;
+        }
+    }
+
+    private final int tabId;
+    private final String id;
+    private DoorHanger.Type type;
+    private String message;
+    private JSONObject options;
+    private Link link;
+    private JSONArray buttons;
+
+    public DoorhangerConfig() {
+        // XXX: This should only be used by SiteIdentityPopup doorhangers which
+        // don't need tab or id references, until bug 1141904 unifies doorhangers.
+        this(-1, null);
+    }
+
+    public DoorhangerConfig(int tabId, String id) {
+        this.tabId = tabId;
+        this.id = id;
+    }
+
+    public int getTabId() {
+        return tabId;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setType(Type type) {
+        this.type = type;
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setOptions(JSONObject options) {
+        this.options = options;
+    }
+
+    public JSONObject getOptions() {
+        return options;
+    }
+
+    public void setButtons(JSONArray buttons) {
+        this.buttons = buttons;
+    }
+
+    public JSONArray getButtons() {
+        return buttons;
+    }
+
+    public void setLink(String label, String url, String delimiter) {
+        this.link = new Link(label, url, delimiter);
+    }
+
+    public Link getLink() {
+        return link;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/LoginDoorHanger.java
@@ -0,0 +1,79 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.widget;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.favicons.Favicons;
+import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
+
+public class LoginDoorHanger extends DoorHanger {
+    private static final String LOGTAG = "LoginDoorHanger";
+
+    final TextView mTitle;
+    final TextView mLogin;
+
+    public LoginDoorHanger(Context context, DoorhangerConfig config) {
+        super(context, config, Type.LOGIN);
+
+        mTitle = (TextView) findViewById(R.id.doorhanger_title);
+        mLogin = (TextView) findViewById(R.id.doorhanger_login);
+
+        loadConfig(config);
+    }
+
+    @Override
+    protected void loadConfig(DoorhangerConfig config) {
+        setOptions(config.getOptions());
+        setMessage(config.getMessage());
+
+    }
+
+    @Override
+    protected void setOptions(final JSONObject options) {
+        super.setOptions(options);
+
+        final JSONObject titleObj = options.optJSONObject("title");
+        if (titleObj != null) {
+
+            try {
+                final String text = titleObj.getString("text");
+                mTitle.setText(text);
+            } catch (JSONException e) {
+                Log.e(LOGTAG, "Error loading title from options JSON");
+            }
+
+            final String resource = titleObj.optString("resource");
+            if (resource != null) {
+                Favicons.getSizedFaviconForPageFromLocal(mContext, resource, 32, new OnFaviconLoadedListener() {
+                    @Override
+                    public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
+                        if (favicon != null) {
+                            mTitle.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(mContext.getResources(), favicon), null, null, null);
+                            mTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding));
+                        }
+                    }
+                });
+            }
+        }
+
+        final String subtext = options.optString("subtext");
+        if (!TextUtils.isEmpty(subtext)) {
+            mLogin.setText(subtext);
+            mLogin.setVisibility(View.VISIBLE);
+        } else {
+            mLogin.setVisibility(View.GONE);
+        }
+    }
+}
--- a/mobile/android/chrome/content/aboutPasswords.js
+++ b/mobile/android/chrome/content/aboutPasswords.js
@@ -131,32 +131,48 @@ let Passwords = {
 
     loginItem.addEventListener("click", () => {
       let prompt = new Prompt({
         window: window,
       });
       let menuItems = [
         { label: gStringBundle.GetStringFromName("passwordsMenu.copyPassword") },
         { label: gStringBundle.GetStringFromName("passwordsMenu.copyUsername") },
-        { label: gStringBundle.GetStringFromName("passwordsMenu.details") } ];
+        { label: gStringBundle.GetStringFromName("passwordsMenu.details") },
+        { label: gStringBundle.GetStringFromName("passwordsMenu.delete") } ];
 
       prompt.setSingleChoiceItems(menuItems);
       prompt.show((data) => {
         // Switch on indices of buttons, as they were added when creating login item.
         switch (data.button) {
           case 0:
             copyStringAndToast(login.password, gStringBundle.GetStringFromName("passwordsDetails.passwordCopied"));
             break;
           case 1:
             copyStringAndToast(login.username, gStringBundle.GetStringFromName("passwordsDetails.usernameCopied"));
             break;
           case 2:
             this._showDetails(loginItem);
             history.pushState({ id: login.guid }, document.title);
             break;
+          case 3:
+            let confirmPrompt = new Prompt({
+              window: window,
+              message: gStringBundle.GetStringFromName("passwordsDialog.confirmDelete"),
+              buttons: [
+                gStringBundle.GetStringFromName("passwordsDialog.confirm"),
+                gStringBundle.GetStringFromName("passwordsDialog.cancel") ]
+            });
+            confirmPrompt.show((data) => {
+              switch (data.button) {
+                case 0:
+                  // Corresponds to "confirm" button.
+                  Services.logins.removeLogin(login);
+              }
+            });
         }
       });
 
     }, true);
 
     // Create item icon.
     let img = document.createElement("div");
     img.className = "icon";
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -2208,21 +2208,34 @@ var NativeWindow = {
    *        persistence: An integer. The notification will not automatically
    *                     dismiss for this many page loads. If persistence is set
    *                     to -1, the doorhanger will never automatically dismiss.
    *        persistWhileVisible:
    *                     A boolean. If true, a visible notification will always
    *                     persist across location changes.
    *        timeout:     A time in milliseconds. The notification will not
    *                     automatically dismiss before this time.
+   *
    *        checkbox:    A string to appear next to a checkbox under the notification
    *                     message. The button callback functions will be called with
    *                     the checked state as an argument.                   
+   *
+   *        title:       An object that specifies text to display as the title, and
+   *                     optionally a resource, such as a favicon cache url that can be
+   *                     used to fetch a favicon from the FaviconCache. (This can be
+   *                     generalized to other resources if the situation arises.)
+   *                     { text: <title>,
+   *                       resource: <resource_url> }
+   *
+   *        subtext:     A string to appear below the doorhanger message.
+   *
+   * @param aCategory
+   *        Doorhanger type to display (e.g., LOGIN)
    */
-    show: function(aMessage, aValue, aButtons, aTabID, aOptions) {
+    show: function(aMessage, aValue, aButtons, aTabID, aOptions, aCategory) {
       if (aButtons == null) {
         aButtons = [];
       }
 
       aButtons.forEach((function(aButton) {
         this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId };
         aButton.callback = this._callbacksId;
         this._callbacksId++;
@@ -2231,17 +2244,18 @@ var NativeWindow = {
       this._promptId++;
       let json = {
         type: "Doorhanger:Add",
         message: aMessage,
         value: aValue,
         buttons: aButtons,
         // use the current tab if none is provided
         tabID: aTabID || BrowserApp.selectedTab.id,
-        options: aOptions || {}
+        options: aOptions || {},
+        category: aCategory
       };
       Messaging.sendRequest(json);
     },
 
     hide: function(aValue, aTabID) {
       Messaging.sendRequest({
         type: "Doorhanger:Remove",
         value: aValue,
--- a/mobile/android/components/LoginManagerPrompter.js
+++ b/mobile/android/components/LoginManagerPrompter.js
@@ -58,18 +58,22 @@ LoginManagerPrompter.prototype = {
         return this.__promptService;
     },
     
     __strBundle : null, // String bundle for L10N
     get _strBundle() {
         if (!this.__strBundle) {
             var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
                              getService(Ci.nsIStringBundleService);
-            this.__strBundle = bunService.createBundle(
-                        "chrome://passwordmgr/locale/passwordmgr.properties");
+            this.__strBundle = {
+              pwmgr : bunService.createBundle(
+                        "chrome://passwordmgr/locale/passwordmgr.properties"),
+              brand : bunService.createBundle("chrome://branding/locale/brand.properties")
+            };
+
             if (!this.__strBundle)
                 throw "String bundle for Login Manager not present!";
         }
 
         return this.__strBundle;
     },
 
 
@@ -131,86 +135,99 @@ LoginManagerPrompter.prototype = {
         Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED);
     },
 
 
     /*
      * _showLoginNotification
      *
      * Displays a notification doorhanger.
+     * @param aName
+     *        Name of notification
+     * @param aTitle
+     *        Object with title and optional resource to display with the title, such as a favicon key
+     * @param aBody
+     *        String message to be displayed in the doorhanger
+     * @param aButtons
+     *        Buttons to display with the doorhanger
+     * @param aSubtext
+     *        String to be displayed below the aBody message
      *
      */
-    _showLoginNotification : function (aName, aText, aButtons) {
+    _showLoginNotification : function (aName, aTitle, aBody, aButtons, aSubtext) {
         this.log("Adding new " + aName + " notification bar");
         let notifyWin = this._window.top;
         let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
         let browser = chromeWin.BrowserApp.getBrowserForWindow(notifyWin);
         let tabID = chromeWin.BrowserApp.getTabForBrowser(browser).id;
 
         // The page we're going to hasn't loaded yet, so we want to persist
         // across the first location change.
 
         // Sites like Gmail perform a funky redirect dance before you end up
         // at the post-authentication page. I don't see a good way to
         // heuristically determine when to ignore such location changes, so
         // we'll try ignoring location changes based on a time interval.
 
         let options = {
             persistWhileVisible: true,
-            timeout: Date.now() + 10000
+            timeout: Date.now() + 10000,
+            title: aTitle,
+            subtext: aSubtext
         }
 
         var nativeWindow = this._getNativeWindow();
         if (nativeWindow)
-            nativeWindow.doorhanger.show(aText, aName, aButtons, tabID, options);
+            nativeWindow.doorhanger.show(aBody, aName, aButtons, tabID, options, "LOGIN");
     },
 
 
     /*
      * _showSaveLoginNotification
      *
      * Displays a notification doorhanger (rather than a popup), to allow the user to
      * save the specified login. This allows the user to see the results of
      * their login, and only save a login which they know worked.
      *
      */
     _showSaveLoginNotification : function (aLogin) {
-        var displayHost = this._getShortDisplayHost(aLogin.hostname);
-        var notificationText;
+        let brandShortName = this._strBundle.brand.GetStringFromName("brandShortName");
+        let notificationText  = this._getLocalizedString("saveLogin", [brandShortName]);
+
+        let displayHost = this._getShortDisplayHost(aLogin.hostname);
+        let title = { text: displayHost, resource: aLogin.hostname };
+        let subtext = null;
+
         if (aLogin.username) {
-            var displayUser = this._sanitizeUsername(aLogin.username);
-            notificationText  = this._getLocalizedString("savePassword", [displayUser, displayHost]);
-        } else {
-            notificationText  = this._getLocalizedString("savePasswordNoUser", [displayHost]);
+          subtext = this._sanitizeUsername(aLogin.username);
         }
-
         // The callbacks in |buttons| have a closure to access the variables
         // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
         // without a getService() call.
         var pwmgr = this._pwmgr;
         let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION");
 
         var buttons = [
             {
-                label: this._getLocalizedString("saveButton"),
+                label: this._getLocalizedString("neverButton"),
+                callback: function() {
+                    promptHistogram.add(PROMPT_NEVER);
+                    pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
+                }
+            },
+            {
+                label: this._getLocalizedString("rememberButton"),
                 callback: function() {
                     pwmgr.addLogin(aLogin);
                     promptHistogram.add(PROMPT_ADD);
                 }
-            },
-            {
-                label: this._getLocalizedString("dontSaveButton"),
-                callback: function() {
-                    promptHistogram.add(PROMPT_NOTNOW);
-                    // Don't set a permanent exception
-                }
             }
         ];
 
-        this._showLoginNotification("password-save", notificationText, buttons);
+        this._showLoginNotification("password-save", title, notificationText, buttons, subtext);
     },
 
     /*
      * promptToChangePassword
      *
      * Called when we think we detect a password change for an existing
      * login, when the form being submitted contains multiple password
      * fields.
@@ -231,40 +248,43 @@ LoginManagerPrompter.prototype = {
         var notificationText;
         if (aOldLogin.username) {
             let displayUser = this._sanitizeUsername(aOldLogin.username);
             notificationText  = this._getLocalizedString("updatePassword", [displayUser]);
         } else {
             notificationText  = this._getLocalizedString("updatePasswordNoUser");
         }
 
+        let displayHost = this._getShortDisplayHost(aOldLogin.hostname);
+        let title = { text: displayHost, resource: aOldLogin.hostname };
+
         // The callbacks in |buttons| have a closure to access the variables
         // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
         // without a getService() call.
         var self = this;
         let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION");
 
         var buttons = [
             {
+                label: this._getLocalizedString("dontUpdateButton"),
+                callback:  function() {
+                    promptHistogram.add(PROMPT_NOTNOW);
+                    // do nothing
+                }
+            },
+            {
                 label: this._getLocalizedString("updateButton"),
                 callback:  function() {
                     self._updateLogin(aOldLogin, aNewPassword);
                     promptHistogram.add(PROMPT_UPDATE);
                 }
-            },
-            {
-                label: this._getLocalizedString("dontUpdateButton"),
-                callback:  function() {
-                    promptHistogram.add(PROMPT_NOTNOW);
-                    // do nothing
-                }
             }
         ];
 
-        this._showLoginNotification("password-change", notificationText, buttons);
+        this._showLoginNotification("password-change", title, notificationText, buttons);
     },
 
 
     /*
      * promptToChangePasswordWithUsernames
      *
      * Called when we detect a password change in a form submission, but we
      * don't know which existing login (username) it's for. Asks the user
@@ -372,20 +392,20 @@ LoginManagerPrompter.prototype = {
      *   (etc)
      *
      * Returns the localized string for the specified key,
      * formatted if required.
      *
      */ 
     _getLocalizedString : function (key, formatArgs) {
         if (formatArgs)
-            return this._strBundle.formatStringFromName(
+            return this._strBundle.pwmgr.formatStringFromName(
                                         key, formatArgs, formatArgs.length);
         else
-            return this._strBundle.GetStringFromName(key);
+            return this._strBundle.pwmgr.GetStringFromName(key);
     },
 
 
     /*
      * _sanitizeUsername
      *
      * Sanitizes the specified username, by stripping quotes and truncating if
      * it's too long. This helps prevent an evil site from messing with the
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -83,16 +83,17 @@ SessionStore.prototype = {
         observerService.addObserver(this, "domwindowclosed", true);
         observerService.addObserver(this, "browser:purge-session-history", true);
         observerService.addObserver(this, "Session:Restore", true);
         observerService.addObserver(this, "application-background", true);
         observerService.addObserver(this, "ClosedTabs:StartNotifications", true);
         observerService.addObserver(this, "ClosedTabs:StopNotifications", true);
         observerService.addObserver(this, "last-pb-context-exited", true);
         observerService.addObserver(this, "Session:RestoreRecentTabs", true);
+        observerService.addObserver(this, "Tabs:OpenMultiple", true);
         break;
       case "final-ui-startup":
         observerService.removeObserver(this, "final-ui-startup");
         this.init();
         break;
       case "domwindowopened": {
         let window = aSubject;
         window.addEventListener("load", function() {
@@ -152,16 +153,21 @@ SessionStore.prototype = {
           let data = JSON.parse(aData);
           this.restoreLastSession(data.sessionString);
         } else {
           // Not doing a restore; just send restore message
           Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
         }
         break;
       }
+      case "Tabs:OpenMultiple": {
+        let data = JSON.parse(aData);
+        this._openTabs(data);
+        break;
+      }
       case "application-background":
         // We receive this notification when Android's onPause callback is
         // executed. After onPause, the application may be terminated at any
         // point without notice; therefore, we must synchronously write out any
         // pending save state to ensure that this data does not get lost.
         this.flushPendingState();
         break;
       case "ClosedTabs:StartNotifications":
@@ -905,16 +911,31 @@ SessionStore.prototype = {
 
         shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i);
       }
     }
 
     return shEntry;
   },
 
+  // This function iterates through a list of urls opening a new tab for each.
+  _openTabs: function ss_openTabs(aData) {
+    let window = Services.wm.getMostRecentWindow("navigator:browser");
+    for (let i = 0; i < aData.urls.length; i++) {
+      let url = aData.urls[i];
+      let params = {
+        selected: (i == aData.urls.length - 1),
+        isPrivate: false,
+        desktopMode: false,
+      };
+
+      let tab = window.BrowserApp.addTab(url, params);
+    }
+  },
+
   // This function iterates through a list of tab data restoring session for each of them.
   _restoreTabs: function ss_restoreTabs(aData) {
     let window = Services.wm.getMostRecentWindow("navigator:browser");
     for (let i = 0; i < aData.tabs.length; i++) {
       let tabData = JSON.parse(aData.tabs[i]);
       let params = {
         selected: (i == aData.tabs.length - 1),
         isPrivate: tabData.isPrivate,
--- a/mobile/android/locales/en-US/chrome/aboutPasswords.properties
+++ b/mobile/android/locales/en-US/chrome/aboutPasswords.properties
@@ -1,13 +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/.
 
 passwordsMenu.copyPassword=Copy password
 passwordsMenu.copyUsername=Copy username
 passwordsMenu.details=Details
+passwordsMenu.delete=Delete
+
+passwordsDialog.confirmDelete=Delete this login?
+passwordsDialog.confirm=OK
+passwordsDialog.cancel=Cancel
 
 passwordsDetails.age=Age: %S days
 
 passwordsDetails.copyFailed=Copy failed
 passwordsDetails.passwordCopied=Password copied
 passwordsDetails.usernameCopied=Username copied
--- a/mobile/android/themes/core/aboutReader.css
+++ b/mobile/android/themes/core/aboutReader.css
@@ -192,28 +192,27 @@ body {
 }
 
 /* Images marked to be shown edge-to-edge with an
    optional captio ntext */
 .content p > img:only-child,
 .content p > a:only-child > img:only-child,
 .content .wp-caption img,
 .content figure img {
-  max-width: none !important;
-  height: auto !important;
-  display: block !important;
-  margin-top: 0px !important;
-  margin-bottom: 32px !important;
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
 }
 
-/* If image is place inside one of these blocks
-   there's no need to add margin at the bottom */
-.content .wp-caption img,
-.content figure img {
-  margin-bottom: 0px !important;
+/* Account for body padding to make image full width */
+.content img[moz-reader-full-width] {
+  width: calc(100% + 40px);
+  margin-left: -20px;
+  margin-right: -20px;
+  max-width: none !important;
 }
 
 /* Image caption text */
 .content .caption,
 .content .wp-caption-text,
 .content figcaption {
   font-family: sans-serif;
   margin: 0px !important;
--- a/mobile/locales/en-US/overrides/passwordmgr.properties
+++ b/mobile/locales/en-US/overrides/passwordmgr.properties
@@ -1,19 +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/.
 
-# 1st string is the username for the login, 2nd is the login's hostname.
-# Note that long usernames may be truncated.
-savePassword=Save password for "%1$S" on %2$S?
-# String is the login's hostname
-savePasswordNoUser=Save password on %S?
-saveButton=Save
-dontSaveButton=Don't save
+# String will be replaced by brandShortName.
+saveLogin=Would you like %S to remember this login?
+rememberButton=Remember
+neverButton=Never
 
 # String is the login's hostname
 updatePassword=Update saved password for %S?
 updatePasswordNoUser=Update saved password for this login?
 updateButton=Update
 dontUpdateButton=Don't update
 
 userSelectText=Please confirm which user you are changing the password for
--- a/testing/docker/tester/Dockerfile
+++ b/testing/docker/tester/Dockerfile
@@ -5,16 +5,17 @@ MAINTAINER    Jonas Finnemann Jensen <jo
 COPY           b2g-desktop-config.py         /home/worker/b2g-desktop-config.py
 COPY           dot-config                    /home/worker/.config
 COPY           dot-pulse                     /home/worker/.pulse
 COPY           bin                           /home/worker/bin
 COPY           mozharness_configs            /home/worker/mozharness_configs
 COPY           buildprops.json               /home/worker/buildprops.json
 ADD            https://s3-us-west-2.amazonaws.com/test-caching/packages/linux64-stackwalk /usr/local/bin/linux64-minidump_stackwalk
 ADD            https://raw.githubusercontent.com/taskcluster/buildbot-step/master/buildbot_step /home/worker/bin/buildbot_step
+COPY           tc-vcs-config.yml /etc/taskcluster-vcs.yml
 
 
 # Run test setup script
 RUN chmod u+x /home/worker/bin/buildbot_step
 RUN chmod u+x /usr/local/bin/linux64-minidump_stackwalk
 RUN apt-get install -y python-pip && pip install virtualenv;
 RUN mkdir Documents; mkdir Pictures; mkdir Music; mkdir Videos; mkdir artifacts
 RUN chown -R worker:worker /home/worker/* /home/worker/.*
--- a/testing/docker/tester/VERSION
+++ b/testing/docker/tester/VERSION
@@ -1,1 +1,1 @@
-0.2.9
+0.2.10
new file mode 100644
--- /dev/null
+++ b/testing/docker/tester/tc-vcs-config.yml
@@ -0,0 +1,40 @@
+# Default configuration used by the tc-vs tools these can be overridden by
+# passing the config you wish to use over the command line...
+git: git
+hg: hg
+
+repoCache:
+  # Repo url to clone when running repo init..
+  repoUrl: https://git.mozilla.org/external/google/gerrit/git-repo.git
+  # Version of repo to utilize...
+  repoRevision: master
+  # The root where all downloaded cache files are stored on the local machine...
+  cacheDir: '{{env.HOME}}/.tc-vcs-repo/'
+  # Name/prefixed used as part of the base url.
+  cacheName: sources/{{name}}.tar.gz
+  # Command used to upload the tarball
+  uploadTar: "curl --header 'Content-Type: application/x-tar' --header 'Content-Encoding: gzip' -X PUT --data-binary @'{{source}}' '{{url}}'"
+  # Large http get requests are often slower using nodes built in http layer so
+  # we utilize a subprocess which is responsible for fetching...
+  get: curl --connect-timeout 30 --speed-limit 500000 -L -o {{dest}} {{url}}
+  # Used to create clone tarball
+  compress: tar -czf {{dest}} {{source}}
+  # All cache urls use tar + gz this is the command used to extract those files
+  # downloaded by the "get" command.
+  extract: tar -x -z -C {{dest}} -f {{source}}
+
+cloneCache:
+  # The root where all downloaded cache files are stored on the local machine...
+  cacheDir: '{{env.HOME}}/.tc-vcs/'
+  # Command used to upload the tarball
+  uploadTar: "curl --header 'Content-Type: application/x-tar' --header 'Content-Encoding: gzip' -X PUT --data-binary @'{{source}}' '{{url}}'"
+  # Large http get requests are often slower using nodes built in http layer so
+  # we utilize a subprocess which is responsible for fetching...
+  get: curl --connect-timeout 30 --speed-limit 500000 -L -o {{dest}} {{url}}
+  # Used to create clone tarball
+  compress: tar -czf {{dest}} {{source}}
+  # All cache urls use tar + gz this is the command used to extract those files
+  # downloaded by the "get" command.
+  extract: tar -x -z --strip-components 1 -C {{dest}} -f {{source}}
+  # Name/prefixed used as part of the base url.
+  cacheName: clones/{{name}}.tar.gz
new file mode 100755
--- /dev/null
+++ b/testing/taskcluster/scripts/builder/build-emulator-x86.sh
@@ -0,0 +1,71 @@
+#! /bin/bash -vex
+
+# Ensure all the scripts in this dir are on the path....
+DIRNAME=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+PATH=$DIRNAME:$PATH
+
+WORKSPACE=$1
+
+### Check that require variables are defined
+test -d $WORKSPACE
+test $GECKO_HEAD_REPOSITORY # Should be an hg repository url to pull from
+test $GECKO_BASE_REPOSITORY # Should be an hg repository url to clone from
+test $GECKO_HEAD_REV # Should be an hg revision to pull down
+test $MOZHARNESS_REPOSITORY # mozharness repository
+test $MOZHARNESS_REV # mozharness revision
+test $TARGET
+
+. setup-ccache.sh
+
+# First check if the mozharness directory is available. This is intended to be
+# used locally in development to test mozharness changes:
+#
+#   $ docker -v your_mozharness:/home/worker/mozharness ...
+#
+if [ ! -d mozharness ]; then
+  tc-vcs checkout mozharness $MOZHARNESS_REPOSITORY $MOZHARNESS_REPOSITORY $MOZHARNESS_REV
+fi
+
+# Figure out where the remote manifest is so we can use caches for it.
+
+if [ -z "$MANIFEST" ]; then
+  MANIFEST="$WORKSPACE/gecko/b2g/config/$TARGET/sources.xml"
+fi
+
+tc-vcs repo-checkout $WORKSPACE/B2G https://git.mozilla.org/b2g/B2G.git $MANIFEST
+
+# Ensure symlink has been created to gecko...
+rm -f $WORKSPACE/B2G/gecko
+ln -s $WORKSPACE/gecko $WORKSPACE/B2G/gecko
+
+debug_flag=""
+if [ 0$B2G_DEBUG -ne 0 ]; then
+  debug_flag='--debug'
+fi
+
+./mozharness/scripts/b2g_build.py \
+  --config b2g/taskcluster-emulator.py \
+  "$debug_flag" \
+  --disable-mock \
+  --work-dir=$WORKSPACE/B2G \
+  --log-level=debug \
+  --target=$TARGET \
+  --b2g-config-dir=$TARGET \
+  --checkout-revision=$GECKO_HEAD_REV \
+  --base-repo=$GECKO_BASE_REPOSITORY \
+  --repo=$GECKO_HEAD_REPOSITORY
+
+# Move files into artifact locations!
+mkdir -p $HOME/artifacts
+
+ls -lah $WORKSPACE/B2G/out
+ls -lah $WORKSPACE/B2G/objdir-gecko/dist/
+
+mv $WORKSPACE/B2G/sources.xml $HOME/artifacts/sources.xml
+mv $WORKSPACE/B2G/out/target/product/generic_x86/tests/gaia-tests.zip $HOME/artifacts/gaia-tests.zip
+mv $WORKSPACE/B2G/out/target/product/generic_x86/tests/b2g-*.zip $HOME/artifacts/b2g-tests.zip
+mv $WORKSPACE/B2G/out/emulator.tar.gz $HOME/artifacts/emulator.tar.gz
+mv $WORKSPACE/B2G/objdir-gecko/dist/b2g-*.crashreporter-symbols.zip $HOME/artifacts/b2g-crashreporter-symbols.zip
+
+ccache -s
+
--- a/testing/taskcluster/scripts/builder/build-emulator.sh
+++ b/testing/taskcluster/scripts/builder/build-emulator.sh
@@ -23,17 +23,21 @@ test $TARGET
 #
 #   $ docker -v your_mozharness:/home/worker/mozharness ...
 #
 if [ ! -d mozharness ]; then
   tc-vcs checkout mozharness $MOZHARNESS_REPOSITORY $MOZHARNESS_REPOSITORY $MOZHARNESS_REV $MOZHARNESS_REF
 fi
 
 # Figure out where the remote manifest is so we can use caches for it.
-MANIFEST=$(repository-url.py $GECKO_HEAD_REPOSITORY $GECKO_HEAD_REV b2g/config/$TARGET/sources.xml)
+
+if [ -z "$MANIFEST" ]; then
+  MANIFEST="$WORKSPACE/gecko/b2g/config/$TARGET/sources.xml"
+fi
+
 tc-vcs repo-checkout $WORKSPACE/B2G https://git.mozilla.org/b2g/B2G.git $MANIFEST
 
 # Ensure symlink has been created to gecko...
 rm -f $WORKSPACE/B2G/gecko
 ln -s $WORKSPACE/gecko $WORKSPACE/B2G/gecko
 
 debug_flag=""
 if [ 0$B2G_DEBUG -ne 0 ]; then
--- a/testing/taskcluster/tasks/branches/base_job_flags.yml
+++ b/testing/taskcluster/tasks/branches/base_job_flags.yml
@@ -4,16 +4,17 @@
 flags:
   aliases:
     mochitests: mochitest
 
   builds:
     - emulator
     - emulator-jb
     - emulator-kk
+    - emulator-x86-kk
     - emulator-l
     - linux32_gecko  # b2g desktop linux 32 bit
     - linux64_gecko  # b2g desktop linux 64 bit
     - linux64-mulet  # Firefox desktop - b2g gecko linux 64 bit
     - macosx64_gecko # b2g desktop osx 64 bit
     - win32_gecko    # b2g desktop win 32 bit
     - flame-kk       # b2g flame kitkat
     - flame-kk-eng   # b2g flame eng build
--- a/testing/taskcluster/tasks/branches/try/job_flags.yml
+++ b/testing/taskcluster/tasks/branches/try/job_flags.yml
@@ -31,16 +31,28 @@ builds:
   emulator-kk:
     platfoms:
       - b2g
     types:
       opt:
         task: tasks/builds/b2g_emulator_kk_opt.yml
       debug:
         task: tasks/builds/b2g_emulator_kk_debug.yml
+  emulator-x86-l:
+    platfoms:
+      - b2g
+    types:
+      opt:
+        task: tasks/builds/b2g_emulator_x86_l_opt.yml
+  emulator-x86-kk:
+    platfoms:
+      - b2g
+    types:
+      opt:
+        task: tasks/builds/b2g_emulator_x86_kk_opt.yml
   emulator-jb:
     platfoms:
       - b2g
     types:
       opt:
         task: tasks/builds/b2g_emulator_jb_opt.yml
       debug:
         task: tasks/builds/b2g_emulator_jb_debug.yml
@@ -129,38 +141,48 @@ tests:
       tasks/builds/b2g_emulator_ics_opt.yml:
         task: tasks/tests/b2g_emulator_marionette_webapi.yml
   mochitest:
     allowed_build_tasks:
       tasks/builds/b2g_emulator_kk_debug.yml:
         task: tasks/tests/b2g_emulator_mochitest.yml
       tasks/builds/b2g_emulator_kk_opt.yml:
         task: tasks/tests/b2g_emulator_mochitest.yml
+      tasks/builds/b2g_emulator_x86_l_opt.yml:
+        task: tasks/tests/b2g_emulator_mochitest.yml
+      tasks/builds/b2g_emulator_x86_kk_opt.yml:
+        task: tasks/tests/b2g_emulator_mochitest.yml
       tasks/builds/b2g_emulator_ics_opt.yml:
         task: tasks/tests/b2g_emulator_mochitest.yml
       tasks/builds/b2g_emulator_ics_debug.yml:
         task: tasks/tests/b2g_emulator_mochitest.yml
       tasks/builds/mulet_linux.yml:
         task: tasks/tests/mulet_mochitests.yml
   mochitest-oop:
     allowed_build_tasks:
       tasks/builds/b2g_desktop_opt.yml:
         task: tasks/tests/b2g_mochitest_oop.yml
   reftest:
     allowed_build_tasks:
       tasks/builds/mulet_linux.yml:
         task: tasks/tests/mulet_reftests.yml
       tasks/builds/b2g_emulator_kk_opt.yml:
         task: tasks/tests/b2g_emulator_reftest.yml
+      tasks/builds/b2g_emulator_x86_l_opt.yml:
+        task: tasks/tests/b2g_emulator_reftest.yml
+      tasks/builds/b2g_emulator_x86_kk_opt.yml:
+        task: tasks/tests/b2g_emulator_reftest.yml
       tasks/builds/b2g_emulator_ics_opt.yml:
         task: tasks/tests/b2g_emulator_reftest.yml
   reftest-sanity-oop:
     allowed_build_tasks:
       tasks/builds/b2g_desktop_opt.yml:
         task: tasks/tests/b2g_reftests_sanity_oop.yml
   xpcshell:
     allowed_build_tasks:
+      tasks/builds/b2g_emulator_x86_kk_opt.yml:
+        task: tasks/tests/b2g_emulator_xpcshell_chunked.yml
       tasks/builds/b2g_emulator_kk_opt.yml:
         task: tasks/tests/b2g_emulator_xpcshell_chunked.yml
       tasks/builds/b2g_emulator_ics_opt.yml:
         task: tasks/tests/b2g_emulator_xpcshell_chunked.yml
       tasks/builds/b2g_emulator_ics_debug.yml:
         task: tasks/tests/b2g_emulator_xpcshell_chunked.yml
new file mode 100644
--- /dev/null
+++ b/testing/taskcluster/tasks/builds/b2g_emulator_x86_base.yml
@@ -0,0 +1,34 @@
+$inherits:
+  from: 'tasks/build.yml'
+task:
+  metadata:
+    description: |
+      Android emulators + b2g environment used in full stack testing.
+  payload:
+    env:
+      TARGET: 'emulator'
+      B2G_DEBUG: 0
+
+    # Emulators can take a very long time to build!
+    maxRunTime: 14400
+
+    command:
+      - /bin/bash
+      - -c
+      - >
+        checkout-gecko workspace &&
+        cd ./workspace/gecko/testing/taskcluster/scripts/builder &&
+        buildbot_step 'Build' ./build-emulator-x86.sh $HOME/workspace
+
+  extra:
+    treeherder:
+      groupSymbol: x86
+    # Rather then enforcing particular conventions we require that all build
+    # tasks provide the "build" extra field to specify where the build and tests
+    # files are located.
+    locations:
+      build: 'public/build/emulator.tar.gz'
+      tests: 'public/build/b2g-tests.zip'
+      symbols: 'public/build/b2g-crashreporter-symbols.zip'
+      sources: 'public/build/sources.xml'
+
new file mode 100644
--- /dev/null
+++ b/testing/taskcluster/tasks/builds/b2g_emulator_x86_kk_opt.yml
@@ -0,0 +1,22 @@
+$inherits:
+  from: 'tasks/builds/b2g_emulator_x86_base.yml'
+task:
+  workerType: emualtor-x86-kk
+  scopes:
+    - 'docker-worker:cache:workspace-emulator-kk-x86-opt'
+  metadata:
+    name: '[TC] B2G KK X86 Emulator (Opt)'
+
+  extra:
+    treeherderEnv:
+      - staging
+    treeherder:
+      # Disable "TC" prefix...
+      machine:
+        platform: b2g-emu-kk
+
+  payload:
+    cache:
+      workspace-emulator-kk-x86-opt: /home/worker/workspace
+    env:
+      TARGET: 'emulator-x86-kk'
new file mode 100644
--- /dev/null
+++ b/testing/taskcluster/tasks/builds/b2g_emulator_x86_l_opt.yml
@@ -0,0 +1,34 @@
+$inherits:
+  from: 'tasks/builds/b2g_emulator_base.yml'
+task:
+  workerType: emulator-l
+  scopes:
+    - 'docker-worker:cache:workspace-emulator-l-x86-opt'
+  metadata:
+    name: '[TC] B2G X86 L Emulator (Opt)'
+
+  extra:
+    treeherderEnv:
+      - staging
+    treeherder:
+      # Disable "TC" prefix...
+      groupSymbol: "X86"
+      machine:
+        platform: b2g-emu-l
+
+  payload:
+    command:
+      - /bin/bash
+      - -c
+      - >
+        checkout-gecko workspace &&
+        cd ./workspace/gecko/testing/taskcluster/scripts/builder &&
+        buildbot_step 'Build' ./build-emulator-x86.sh $HOME/workspace
+
+    cache:
+      workspace-emulator-l-x86-opt: /home/worker/workspace
+    env:
+      TARGET: 'emulator-x86-l'
+
+
+
--- a/testing/taskcluster/tasks/builds/b2g_flame_kk_eng.yml
+++ b/testing/taskcluster/tasks/builds/b2g_flame_kk_eng.yml
@@ -9,27 +9,25 @@ task:
 
   payload:
     cache:
       build-flame-kk-eng: /home/worker/workspace
     env:
       TARGET: 'flame-kk'
       DEBUG: 0
       VARIANT: eng
+      GAIA_OPTIMIZE: '1'
+      B2G_SYSTEM_APPS: '1'
+      B2G_UPDATER: '1'
     command:
       - >
         checkout-gecko workspace &&
         cd ./workspace/gecko/testing/taskcluster/scripts/phone-builder &&
         buildbot_step 'Build' ./build-phone.sh $HOME/workspace
   extra:
     treeherder:
       symbol: Be
       groupSymbol: Flame-KK
       groupName: Flame KitKat Device Image
       machine:
         platform: b2g-device-image
     locations:
       img: 'private/build/flame-kk.zip'
-
-      GAIA_OPTIMIZE: '1'
-      B2G_SYSTEM_APPS: '1'
-      B2G_UPDATER: '1'
-
--- a/testing/taskcluster/tasks/job_flags.yml
+++ b/testing/taskcluster/tasks/job_flags.yml
@@ -3,16 +3,17 @@
 
 # List of all possible flags for each category of tests used in the case where
 # "all" is specified.
 flags:
   builds:
     - emulator
     - emulator-jb
     - emulator-kk
+    - emulator-x86-kk
     - linux32_gecko  # b2g desktop linux 32 bit
     - linux64_gecko  # b2g desktop linux 64 bit
     - linux64-mulet  # Firefox desktop - b2g gecko linux 64 bit
     - macosx64_gecko # b2g desktop osx 64 bit
     - win32_gecko    # b2g desktop win 32 bit
 
   tests:
     - cppunit
--- a/testing/taskcluster/tasks/tests/b2g_build_unit.yml
+++ b/testing/taskcluster/tasks/tests/b2g_build_unit.yml
@@ -1,11 +1,12 @@
 ---
 $inherits:
   from: 'tasks/test.yml'
+reruns: 2
 task:
   metadata:
     name: '[TC] - Gaia Build Unit Test'
     description: Gaia Build Unit Test
 
   payload:
     command:
       - entrypoint
--- a/testing/taskcluster/tasks/tests/b2g_gaia_js_integration_tests.yml
+++ b/testing/taskcluster/tasks/tests/b2g_gaia_js_integration_tests.yml
@@ -1,11 +1,12 @@
 ---
 $inherits:
   from: 'tasks/test.yml'
+reruns: 2
 task:
   metadata:
     name: '[TC] - Gaia JS Integration Test'
     description: Gaia JS Integration Test run {{chunk}}
 
   payload:
     command:
       - entrypoint # entrypoint ensures we are running in xvfb
--- a/testing/taskcluster/tasks/tests/b2g_gaia_ui_test_accessibility.yml
+++ b/testing/taskcluster/tasks/tests/b2g_gaia_ui_test_accessibility.yml
@@ -1,11 +1,12 @@
 ---
 $inherits:
   from: 'tasks/test.yml'
+reruns: 2
 task:
   metadata:
     name: '[TC] - Gaia Python Accessibility Integration Tests'
     description: Gaia Python Accessibility Integration Tests run {{chunk}}
 
   payload:
     command:
       - entrypoint # entrypoint ensures we are running in xvfb
--- a/testing/taskcluster/tasks/tests/b2g_gaia_ui_test_functional.yml
+++ b/testing/taskcluster/tasks/tests/b2g_gaia_ui_test_functional.yml
@@ -1,11 +1,12 @@
 ---
 $inherits:
   from: 'tasks/test.yml'
+reruns: 2
 task:
   metadata:
     name: '[TC] - Gaia Python Functional Integration Tests'
     description: Gaia Python Functional Integration Tests run {{chunk}}
 
   payload:
     command:
       - entrypoint # entrypoint ensures we are running in xvfb
--- a/testing/taskcluster/tasks/tests/b2g_gaia_ui_test_unit.yml
+++ b/testing/taskcluster/tasks/tests/b2g_gaia_ui_test_unit.yml
@@ -1,11 +1,12 @@
 ---
 $inherits:
   from: 'tasks/test.yml'
+reruns: 2
 task:
   metadata:
     name: '[TC] - Gaia Python Integration Unit Tests'
     description: Gaia Python Integration Unit Tests run {{chunk}}
 
   payload:
     command:
       - entrypoint # entrypoint ensures we are running in xvfb
--- a/testing/taskcluster/tasks/tests/b2g_gaia_unit.yml
+++ b/testing/taskcluster/tasks/tests/b2g_gaia_unit.yml
@@ -1,11 +1,12 @@
 ---
 $inherits:
   from: 'tasks/test.yml'
+reruns: 2
 task:
   metadata:
     name: '[TC] Gaia Unit Test'
     description: Gaia Unit Test
 
   payload:
     command:
       - entrypoint
--- a/testing/taskcluster/tasks/tests/b2g_gaia_unit_oop.yml
+++ b/testing/taskcluster/tasks/tests/b2g_gaia_unit_oop.yml
@@ -1,11 +1,12 @@
 ---
 $inherits:
   from: 'tasks/test.yml'
+reruns: 2
 task:
   metadata:
     name: '[TC] Gaia Unit Test OOP'
     description: Gaia Unit Test OOP
 
   payload:
     command:
       - entrypoint
--- a/testing/taskcluster/tasks/tests/b2g_gip_oop.yml
+++ b/testing/taskcluster/tasks/tests/b2g_gip_oop.yml
@@ -1,11 +1,12 @@
 ---
 $inherits:
   from: 'tasks/test.yml'
+reruns: 2
 task:
   metadata:
     name: '[TC] Gaia Python Integration Tests OOP'
     description: Gaia Python Functional Integration Tests OOP test run
 
   payload:
     command:
       - entrypoint # entrypoint ensures we are running in xvfb
--- a/testing/taskcluster/tasks/tests/mulet_gaia_js_integration_tests.yml
+++ b/testing/taskcluster/tasks/tests/mulet_gaia_js_integration_tests.yml
@@ -1,11 +1,12 @@
 ---
 $inherits:
   from: 'tasks/test.yml'
+reruns: 2
 task:
   metadata:
     name: '[TC] Mulet Gaia JS Integration Test'
     description: Mulet Gaia JS Integration Test run {{chunk}}
 
   payload:
     command:
       - entrypoint # entrypoint ensures we are running in xvfb
--- a/toolkit/components/passwordmgr/content/passwordManager.js
+++ b/toolkit/components/passwordmgr/content/passwordManager.js
@@ -1,10 +1,8 @@
-// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*** =================== SAVED SIGNONS CODE =================== ***/
 
 var kSignonBundle;
 var showingPasswords = false;
@@ -14,16 +12,20 @@ var dateAndTimeFormatter = new Intl.Date
                              { day: "numeric", month: "short", year: "numeric",
                                hour: "numeric", minute: "numeric" });
 
 function SignonsStartup() {
   kSignonBundle = document.getElementById("signonBundle");
   document.getElementById("togglePasswords").label = kSignonBundle.getString("showPasswords");
   document.getElementById("togglePasswords").accessKey = kSignonBundle.getString("showPasswordsAccessKey");
   document.getElementById("signonsIntro").textContent = kSignonBundle.getString("loginsSpielAll");
+
+  let treecols = document.getElementsByTagName("treecols")[0];
+  treecols.addEventListener("click", HandleTreeColumnClick.bind(null, SignonColumnSort));
+
   LoadSignons();
 
   // filter the table if requested by caller
   if (window.arguments &&
       window.arguments[0] &&
       window.arguments[0].filterString)
     setFilter(window.arguments[0].filterString);
 
--- a/toolkit/components/passwordmgr/content/passwordManager.xul
+++ b/toolkit/components/passwordmgr/content/passwordManager.xul
@@ -58,41 +58,41 @@
     <tree id="signonsTree" flex="1"
           width="750"
           style="height: 20em;"
           onkeypress="HandleSignonKeyPress(event)"
           onselect="SignonSelected();"
           context="signonsTreeContextMenu">
       <treecols>
         <treecol id="siteCol" label="&treehead.site.label;" flex="40"
-                 onclick="SignonColumnSort('hostname');" persist="width"
+                 data-field-name="hostname" persist="width"
                  ignoreincolumnpicker="true"
                  sortDirection="ascending"/>
         <splitter class="tree-splitter"/>
         <treecol id="userCol" label="&treehead.username.label;" flex="25"
                  ignoreincolumnpicker="true"
-                 onclick="SignonColumnSort('username');" persist="width"/>
+                 data-field-name="username" persist="width"/>
         <splitter class="tree-splitter"/>
         <treecol id="passwordCol" label="&treehead.password.label;" flex="15"
                  ignoreincolumnpicker="true"
-                 onclick="SignonColumnSort('password');" persist="width"
+                 data-field-name="password" persist="width"
                  hidden="true"/>
         <splitter class="tree-splitter"/>
         <treecol id="timeCreatedCol" label="&treehead.timeCreated.label;" flex="10"
-                 onclick="SignonColumnSort('timeCreated');" persist="width hidden"
+                 data-field-name="timeCreated" persist="width hidden"
                  hidden="true"/>
         <splitter class="tree-splitter"/>
         <treecol id="timeLastUsedCol" label="&treehead.timeLastUsed.label;" flex="20"
-                 onclick="SignonColumnSort('timeLastUsed');" persist="width hidden"/>
+                 data-field-name="timeLastUsed" persist="width hidden"/>
         <splitter class="tree-splitter"/>
         <treecol id="timePasswordChangedCol" label="&treehead.timePasswordChanged.label;" flex="10"
-                 onclick="SignonColumnSort('timePasswordChanged');" persist="width hidden"/>
+                 data-field-name="timePasswordChanged" persist="width hidden"/>
         <splitter class="tree-splitter"/>
         <treecol id="timesUsedCol" label="&treehead.timesUsed.label;" flex="1"
-                 onclick="SignonColumnSort('timesUsed');" persist="width hidden"
+                 data-field-name="timesUsed" persist="width hidden"
                  hidden="true"/>
         <splitter class="tree-splitter"/>
       </treecols>
       <treechildren/>
     </tree>
     <separator class="thin"/>
     <hbox id="SignonViewerButtons">
       <button id="removeSignon" disabled="true" icon="remove"
--- a/toolkit/components/passwordmgr/content/passwordManagerCommon.js
+++ b/toolkit/components/passwordmgr/content/passwordManagerCommon.js
@@ -1,10 +1,8 @@
-// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*** =================== INITIALISATION CODE =================== ***/
 
 var kObserverService;
 
@@ -155,16 +153,29 @@ function GetTreeSelections(tree) {
           selections[selections.length] = k;
         }
       }
     }
   }
   return selections;
 }
 
+function HandleTreeColumnClick(sortFunction, event) {
+  if (event.target.nodeName != "treecol" || event.button != 0) {
+    return;
+  }
+
+  let sortField = event.target.getAttribute("data-field-name");
+  if (!sortField) {
+    return;
+  }
+
+  sortFunction(sortField);
+}
+
 function SortTree(tree, view, table, column, lastSortColumn, lastSortAscending, updateSelection) {
 
   // remember which item was selected so we can restore it after the sort
   var selections = GetTreeSelections(tree);
   var selectedNumber = selections.length ? table[selections[0]].number : -1;
 
   // determine if sort is to be ascending or descending
   var ascending = (column == lastSortColumn) ? !lastSortAscending : true;
--- a/toolkit/components/passwordmgr/content/passwordManagerExceptions.js
+++ b/toolkit/components/passwordmgr/content/passwordManagerExceptions.js
@@ -1,18 +1,19 @@
-// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*** =================== REJECTED SIGNONS CODE =================== ***/
 
 function RejectsStartup() {
   LoadRejects();
+
+  let treecols = document.getElementsByTagName("treecols")[0];
+  treecols.addEventListener("click", HandleTreeColumnClick.bind(null, RejectColumnSort));
 }
 
 var rejectsTreeView = {
   rowCount : 0,
   setTree : function(tree){},
   getImageSrc : function(row,column) {},
   getProgressMode : function(row,column) {},
   getCellValue : function(row,column) {},
--- a/toolkit/components/passwordmgr/content/passwordManagerExceptions.xul
+++ b/toolkit/components/passwordmgr/content/passwordManagerExceptions.xul
@@ -25,17 +25,17 @@
     <vbox id="rejectedsites" flex="1">
         <description control="rejectsTree">&loginsSpielExceptions.label;</description>
         <separator class="thin"/>
         <tree id="rejectsTree" flex="1" style="height: 10em;" hidecolumnpicker="true"
               onkeypress="HandleRejectKeyPress(event)"
               onselect="RejectSelected();">
           <treecols>
             <treecol id="rejectCol" label="&treehead.site.label;" flex="5"
-                     onclick="RejectColumnSort('host');" sortDirection="ascending"/>
+                     data-field-name="host" sortDirection="ascending"/>
           </treecols>
           <treechildren/>
         </tree>
         <separator class="thin"/>
         <hbox>
           <button id="removeReject" disabled="true" icon="remove"
                   accesskey="&remove.accesskey;"
                   label="&remove.label;" oncommand="DeleteReject();"/>
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -927,18 +927,24 @@ this.PlacesUtils = {
       stmt.finalize();
     }
   },
 
   /**
    * Get the URI (and any associated POST data) for a given keyword.
    * @param aKeyword string keyword
    * @returns an array containing a string URL and a string of POST data
+   *
+   * @deprecated
    */
   getURLAndPostDataForKeyword(aKeyword) {
+    Deprecated.warning("getURLAndPostDataForKeyword() is deprecated, please " +
+                       "use PlacesUtils.keywords.fetch() instead",
+                       "https://bugzilla.mozilla.org/show_bug.cgi?id=1100294");
+
     let stmt = PlacesUtils.history.DBConnection.createStatement(
       `SELECT h.url, k.post_data
        FROM moz_keywords k
        JOIN moz_places h ON h.id = k.place_id
        WHERE k.keyword = :keyword`);
     stmt.params.keyword = aKeyword;
     try {
       if (!stmt.executeStep())
@@ -2014,17 +2020,17 @@ let Keywords = {
    *
    * @param keyword
    *        The keyword to fetch.
    * @return {Promise}
    * @resolves to an object in the form: { keyword, url, postData },
    *           or null if a keyword was not found.
    */
   fetch(keyword) {
-    if (!keyword || typeof(keyword) != "string")
+    if (typeof(keyword) != "string")
       throw new Error("Invalid keyword");
     keyword = keyword.trim().toLowerCase();
     return gKeywordsCachePromise.then(cache => cache.get(keyword) || null);
   },
 
   /**
    * Adds a new keyword and postData for the given URL.
    *
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -41,35 +41,34 @@ const PREF_SUGGEST_HISTORY_ONLYTYPED = [
 const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
 const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
 const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
 const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
 const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
 
 // AutoComplete query type constants.
 // Describes the various types of queries that we can process rows for.
-const QUERYTYPE_KEYWORD       = 0;
-const QUERYTYPE_FILTERED      = 1;
-const QUERYTYPE_AUTOFILL_HOST = 2;
-const QUERYTYPE_AUTOFILL_URL  = 3;
-const QUERYTYPE_AUTOFILL_PREDICTURL  = 4;
+const QUERYTYPE_FILTERED            = 0;
+const QUERYTYPE_AUTOFILL_HOST       = 1;
+const QUERYTYPE_AUTOFILL_URL        = 2;
+const QUERYTYPE_AUTOFILL_PREDICTURL = 3;
 
 // This separator is used as an RTL-friendly way to split the title and tags.
 // It can also be used by an nsIAutoCompleteResult consumer to re-split the
 // "comment" back into the title and the tag.
 const TITLE_TAGS_SEPARATOR = " \u2013 ";
 
 // This separator identifies the search engine name in the title.
 const TITLE_SEARCH_ENGINE_SEPARATOR = " \u00B7\u2013\u00B7 ";
 
 // Telemetry probes.
 const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
 const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
 // The default frecency value used when inserting matches with unknown frecency.
-const FRECENCY_SEARCHENGINES_DEFAULT = 1000;
+const FRECENCY_DEFAULT = 1000;
 
 // Sqlite result row index constants.
 const QUERYINDEX_QUERYTYPE     = 0;
 const QUERYINDEX_URL           = 1;
 const QUERYINDEX_TITLE         = 2;
 const QUERYINDEX_ICONURL       = 3;
 const QUERYINDEX_BOOKMARKED    = 4;
 const QUERYINDEX_BOOKMARKTITLE = 5;
@@ -144,33 +143,16 @@ const SQL_ADAPTIVE_QUERY =
    LEFT JOIN moz_openpages_temp t ON t.url = h.url
    WHERE AUTOCOMPLETE_MATCH(NULL, h.url,
                             IFNULL(btitle, h.title), tags,
                             h.visit_count, h.typed, bookmarked,
                             t.open_count,
                             :matchBehavior, :searchBehavior)
    ORDER BY rank DESC, h.frecency DESC`;
 
-const SQL_KEYWORD_QUERY =
-  `/* do not warn (bug 487787) */
-   SELECT :query_type,
-          REPLACE(h.url, '%s', :query_string) AS search_url, h.title,
-     IFNULL(f.url, (SELECT f.url
-                    FROM moz_places
-                    JOIN moz_favicons f ON f.id = favicon_id
-                    WHERE rev_host = h.rev_host
-                    ORDER BY frecency DESC
-                    LIMIT 1)
-           ),
-     1, NULL, NULL, h.visit_count, h.typed, h.id, t.open_count, h.frecency
-   FROM moz_keywords k
-   JOIN moz_places h ON k.place_id = h.id
-   LEFT JOIN moz_favicons f ON f.id = h.favicon_id
-   LEFT JOIN moz_openpages_temp t ON t.url = search_url
-   WHERE k.keyword = LOWER(:keyword)`;
 
 function hostQuery(conditions = "") {
   let query =
     `/* do not warn (bug NA): not worth to index on (typed, frecency) */
      SELECT :query_type, host || '/', IFNULL(prefix, '') || host || '/',
             NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency
      FROM moz_hosts
      WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
@@ -726,24 +708,26 @@ Search.prototype = {
 
     TelemetryStopwatch.start(TELEMETRY_1ST_RESULT);
     if (this._searchString)
       TelemetryStopwatch.start(TELEMETRY_6_FIRST_RESULTS);
 
     // Since we call the synchronous parseSubmissionURL function later, we must
     // wait for the initialization of PlacesSearchAutocompleteProvider first.
     yield PlacesSearchAutocompleteProvider.ensureInitialized();
+    if (!this.pending)
+      return;
 
     // For any given search, we run many queries/heuristics:
     // 1) by alias (as defined in SearchService)
     // 2) inline completion from search engine resultDomains
     // 3) inline completion for hosts (this._hostQuery) or urls (this._urlQuery)
     // 4) directly typed in url (ie, can be navigated to as-is)
     // 5) submission for the current search engine
-    // 6) keywords (this._keywordQuery)
+    // 6) Places keywords
     // 7) adaptive learning (this._adaptiveQuery)
     // 8) open pages not supported by history (this._switchToTabQuery)
     // 9) query based on match behavior
     //
     // (6) only gets ran if we get any filtered tokens, since if there are no
     // tokens, there is nothing to match. This is the *first* query we check if
     // we want to run, but it gets queued to be run later.
     //
@@ -767,25 +751,23 @@ Search.prototype = {
 
     // When actions are enabled, we run a series of heuristics to determine what
     // the first result should be - which is always a special result.
     // |hasFirstResult| is used to keep track of whether we've obtained such a
     // result yet, so we can skip further heuristics and not add any additional
     // special results.
     let hasFirstResult = false;
 
-    if (this._searchTokens.length > 0 &&
-        PlacesUtils.bookmarks.getURIForKeyword(this._searchTokens[0])) {
-      // This may be a keyword of a bookmark.
-      queries.unshift(this._keywordQuery);
-      hasFirstResult = true;
+    if (this._searchTokens.length > 0) {
+      // This may be a Places keyword.
+      hasFirstResult = yield this._matchPlacesKeyword();
     }
 
-    if (this._enableActions && !hasFirstResult) {
-      // If it's not a bookmarked keyword, then it may be a search engine
+    if (this.pending && this._enableActions && !hasFirstResult) {
+      // If it's not a Places keyword, then it may be a search engine
       // with an alias - which works like a keyword.
       hasFirstResult = yield this._matchSearchEngineAlias();
     }
 
     let shouldAutofill = this._shouldAutofill;
     if (this.pending && !hasFirstResult && shouldAutofill) {
       // It may also look like a URL we know from the database.
       // Here we can only try to predict whether the URL autofill query is
@@ -870,16 +852,45 @@ Search.prototype = {
     yield conn.executeCached(query, params, row => {
       gotResult = true;
       this._onResultRow(row);
     });
 
     return gotResult;
   },
 
+  _matchPlacesKeyword: function* () {
+    // The first word could be a keyword, so that's what we'll search.
+    let keyword = this._searchTokens[0];
+    let entry = yield PlacesUtils.keywords.fetch(this._searchTokens[0]);
+    if (!entry)
+      return false;
+
+    // Build the url.
+    let searchString = this._trimmedOriginalSearchString;
+    let queryString = "";
+    let queryIndex = searchString.indexOf(" ");
+    if (queryIndex != -1) {
+      queryString = searchString.substring(queryIndex + 1);
+    }
+    // We need to escape the parameters as if they were the query in a URL
+    queryString = encodeURIComponent(queryString).replace(/%20/g, "+");
+    let escapedURL = entry.url.href.replace("%s", queryString);
+
+    let style = (this._enableActions ? "action " : "") + "keyword";
+    let actionURL = makeActionURL("keyword", { url: escapedURL,
+                                               input: this._originalSearchString });
+    let value = this._enableActions ? actionURL : escapedURL;
+    // The title will end up being "host: queryString"
+    let comment = entry.url.host;
+
+    this._addMatch({ value, comment, style, frecency: FRECENCY_DEFAULT });
+    return true;
+  },
+
   _matchSearchEngineUrl: function* () {
     if (!Prefs.autofillSearchEngines)
       return false;
 
     let match = yield PlacesSearchAutocompleteProvider.findMatchByToken(
                                                            this._searchString);
     if (!match)
       return false;
@@ -917,17 +928,17 @@ Search.prototype = {
 
     this._result.setDefaultIndex(0);
     this._addMatch({
       value: value,
       comment: match.engineName,
       icon: match.iconUrl,
       style: "priority-search",
       finalCompleteValue: match.url,
-      frecency: FRECENCY_SEARCHENGINES_DEFAULT
+      frecency: FRECENCY_DEFAULT
     });
     return true;
   },
 
   _matchSearchEngineAlias: function* () {
     if (this._searchTokens.length < 2)
       return false;
 
@@ -964,17 +975,17 @@ Search.prototype = {
     }
     let value = makeActionURL("searchengine", actionURLParams);
 
     this._addMatch({
       value: value,
       comment: match.engineName,
       icon: match.iconUrl,
       style: "action searchengine",
-      frecency: FRECENCY_SEARCHENGINES_DEFAULT,
+      frecency: FRECENCY_DEFAULT,
     });
   },
 
   // These are separated out so we can run them in two distinct cases:
   // (1) We didn't match on anything that we know about
   // (2) Our predictive query for URL autofill thought we may get a result,
   //     but we didn't.
   _matchHeuristicFallback: function* () {
@@ -1047,17 +1058,16 @@ Search.prototype = {
       case QUERYTYPE_AUTOFILL_PREDICTURL:
         match = this._processHostRow(row);
         break;
       case QUERYTYPE_AUTOFILL_URL:
         this._result.setDefaultIndex(0);
         match = this._processUrlRow(row);
         break;
       case QUERYTYPE_FILTERED:
-      case QUERYTYPE_KEYWORD:
         match = this._processRow(row);
         break;
     }
     this._addMatch(match);
     // If the search has been canceled by the user or by _addMatch reaching the
     // maximum number of results, we can stop the underlying Sqlite query.
     if (!this.pending)
       throw StopIteration;
@@ -1231,27 +1241,16 @@ Search.prototype = {
     if (this._enableActions && openPageCount > 0 && this.hasBehavior("openpage")) {
       url = makeActionURL("switchtab", {url: escapedURL});
       action = "switchtab";
     }
 
     // Always prefer the bookmark title unless it is empty
     let title = bookmarkTitle || historyTitle;
 
-    if (queryType == QUERYTYPE_KEYWORD) {
-      match.style = "keyword";
-      if (this._enableActions) {
-        url = makeActionURL("keyword", {
-          url: escapedURL,
-          input: this._originalSearchString,
-        });
-        action = "keyword";
-      }
-    }
-
     // We will always prefer to show tags if we have them.
     let showTags = !!tags;
 
     // However, we'll act as if a page is not bookmarked if the user wants
     // only history and not bookmarks and there are no tags.
     if (this.hasBehavior("history") && !this.hasBehavior("bookmark") &&
         !showTags) {
       showTags = false;
@@ -1348,47 +1347,16 @@ Search.prototype = {
         // Limit the query to the the maximum number of desired results.
         // This way we can avoid doing more work than needed.
         maxResults: Prefs.maxRichResults
       }
     ];
   },
 
   /**
-   * Obtains the query to search for keywords.
-   *
-   * @return an array consisting of the correctly optimized query to search the
-   *         database with and an object containing the params to bound.
-   */
-  get _keywordQuery() {
-    // The keyword is the first word in the search string, with the parameters
-    // following it.
-    let searchString = this._trimmedOriginalSearchString;
-    let queryString = "";
-    let queryIndex = searchString.indexOf(" ");
-    if (queryIndex != -1) {
-      queryString = searchString.substring(queryIndex + 1);
-    }
-    // We need to escape the parameters as if they were the query in a URL
-    queryString = encodeURIComponent(queryString).replace(/%20/g, "+");
-
-    // The first word could be a keyword, so that's what we'll search.
-    let keyword = this._searchTokens[0];
-
-    return [
-      SQL_KEYWORD_QUERY,
-      {
-        keyword: keyword,
-        query_string: queryString,
-        query_type: QUERYTYPE_KEYWORD
-      }
-    ];
-  },
-
-  /**
    * Obtains the query to search for switch-to-tab entries.
    *
    * @return an array consisting of the correctly optimized query to search the
    *         database with and an object containing the params to bound.
    */
   get _switchToTabQuery() [
     SQL_SWITCHTAB_QUERY,
     {
--- a/toolkit/components/places/nsPlacesAutoComplete.js
+++ b/toolkit/components/places/nsPlacesAutoComplete.js
@@ -7,16 +7,18 @@
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
                                   "resource://gre/modules/TelemetryStopwatch.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Constants
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
@@ -1163,26 +1165,18 @@ nsPlacesAutoComplete.prototype = {
       aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null;
     let entryTags = aRow.getResultByIndex(kQueryIndexTags) || "";
 
     // Always prefer the bookmark title unless it is empty
     let title = entryBookmarkTitle || entryTitle;
 
     let style;
     if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) {
-      // If we do not have a title, then we must have a keyword, so let the UI
-      // know it is a keyword.  Otherwise, we found an exact page match, so just
-      // show the page like a regular result.  Because the page title is likely
-      // going to be more specific than the bookmark title (keyword title).
-      if (!entryTitle) {
-        style = "keyword";
-      }
-      else {
-        title = entryTitle;
-      }
+      style = "keyword";
+      title = NetUtil.newURI(escapedEntryURL).host;
     }
 
     // We will always prefer to show tags if we have them.
     let showTags = !!entryTags;
 
     // However, we'll act as if a page is not bookmarked if the user wants
     // only history and not bookmarks and there are no tags.
     if (this._hasBehavior("history") && !this._hasBehavior("bookmark") &&
@@ -1427,16 +1421,18 @@ urlInlineComplete.prototype = {
   startSearch: function UIC_startSearch(aSearchString, aSearchParam,
                                         aPreviousResult, aListener)
   {
     // Stop the search in case the controller has not taken care of it.
     if (this._pendingQuery) {
       this.stopSearch();
     }
 
+    let pendingSearch = this._pendingSearch = {};
+
     // We want to store the original string with no leading or trailing
     // whitespace for case sensitive searches.
     this._originalSearchString = aSearchString;
     this._currentSearchString =
       fixupSearchText(this._originalSearchString.toLowerCase());
     // The protocol and the host are lowercased by nsIURI, so it's fine to
     // lowercase the typed prefix to add it back to the results later.
     this._strippedPrefix = this._originalSearchString.slice(
@@ -1445,84 +1441,92 @@ urlInlineComplete.prototype = {
 
     this._result = Cc["@mozilla.org/autocomplete/simple-result;1"].
                    createInstance(Ci.nsIAutoCompleteSimpleResult);
     this._result.setSearchString(aSearchString);
     this._result.setTypeAheadResult(true);
 
     this._listener = aListener;
 
-    // Don't autoFill if the search term is recognized as a keyword, otherwise
-    // it will override default keywords behavior.  Note that keywords are
-    // hashed on first use, so while the first query may delay a little bit,
-    // next ones will just hit the memory hash.
-    if (this._currentSearchString.length == 0 || !this._db ||
-        PlacesUtils.bookmarks.getURIForKeyword(this._currentSearchString)) {
-      this._finishSearch();
-      return;
-    }
+    Task.spawn(function* () {
+      // Don't autoFill if the search term is recognized as a keyword, otherwise
+      // it will override default keywords behavior.  Note that keywords are
+      // hashed on first use, so while the first query may delay a little bit,
+      // next ones will just hit the memory hash.
+      let dontAutoFill = this._currentSearchString.length == 0 || !this._db ||
+                         (yield PlacesUtils.keywords.fetch(this._currentSearchString));
+      if (this._pendingSearch != pendingSearch)
+        return;
+      if (dontAutoFill) {
+        this._finishSearch();
+        return;
+      }
 
-    // Don't try to autofill if the search term includes any whitespace.
-    // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
-    // tokenizer ends up trimming the search string and returning a value
-    // that doesn't match it, or is even shorter.
-    if (/\s/.test(this._currentSearchString)) {
-      this._finishSearch();
-      return;
-    }
+      // Don't try to autofill if the search term includes any whitespace.
+      // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
+      // tokenizer ends up trimming the search string and returning a value
+      // that doesn't match it, or is even shorter.
+      if (/\s/.test(this._currentSearchString)) {
+        this._finishSearch();
+        return;
+      }
 
-    // Hosts have no "/" in them.
-    let lastSlashIndex = this._currentSearchString.lastIndexOf("/");
+      // Hosts have no "/" in them.
+      let lastSlashIndex = this._currentSearchString.lastIndexOf("/");
 
-    // Search only URLs if there's a slash in the search string...
-    if (lastSlashIndex != -1) {
-      // ...but not if it's exactly at the end of the search string.
-      if (lastSlashIndex < this._currentSearchString.length - 1)
-        this._queryURL();
-      else
-        this._finishSearch();
-      return;
-    }
+      // Search only URLs if there's a slash in the search string...
+      if (lastSlashIndex != -1) {
+        // ...but not if it's exactly at the end of the search string.
+        if (lastSlashIndex < this._currentSearchString.length - 1)
+          this._queryURL();
+        else
+          this._finishSearch();
+        return;
+      }
 
-    // Do a synchronous search on the table of hosts.
-    let query = this._hostQuery;
-    query.params.search_string = this._currentSearchString.toLowerCase();
-    // This is just to measure the delay to reach the UI, not the query time.
-    TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY);
-    let ac = this;
-    let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
-      handleResult: function (aResultSet) {
-        let row = aResultSet.getNextRow();
-        let trimmedHost = row.getResultByIndex(0);
-        let untrimmedHost = row.getResultByIndex(1);
-        // If the untrimmed value doesn't preserve the user's input just
-        // ignore it and complete to the found host.
-        if (untrimmedHost &&
-            !untrimmedHost.toLowerCase().contains(ac._originalSearchString.toLowerCase())) {
-          untrimmedHost = null;
-        }
+      // Do a synchronous search on the table of hosts.
+      let query = this._hostQuery;
+      query.params.search_string = this._currentSearchString.toLowerCase();
+      // This is just to measure the delay to reach the UI, not the query time.
+      TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY);
+      let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
+        handleResult: aResultSet => {
+          if (this._pendingSearch != pendingSearch)
+            return;
+          let row = aResultSet.getNextRow();
+          let trimmedHost = row.getResultByIndex(0);
+          let untrimmedHost = row.getResultByIndex(1);
+          // If the untrimmed value doesn't preserve the user's input just
+          // ignore it and complete to the found host.
+          if (untrimmedHost &&
+              !untrimmedHost.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
+            untrimmedHost = null;
+          }
 
-        ac._result.appendMatch(ac._strippedPrefix + trimmedHost, "", "", "", untrimmedHost);
+          this._result.appendMatch(this._strippedPrefix + trimmedHost, "", "", "", untrimmedHost);
 
-        // handleCompletion() will cause the result listener to be called, and
-        // will display the result in the UI.
-      },
+          // handleCompletion() will cause the result listener to be called, and
+          // will display the result in the UI.
+        },
 
-      handleError: function (aError) {
-        Components.utils.reportError(
-          "URL Inline Complete: An async statement encountered an " +
-          "error: " + aError.result + ", '" + aError.message + "'");
-      },
+        handleError: aError => {
+          Components.utils.reportError(
+            "URL Inline Complete: An async statement encountered an " +
+            "error: " + aError.result + ", '" + aError.message + "'");
+        },
 
-      handleCompletion: function (aReason) {
-        TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY);
-        ac._finishSearch();
-      }
-    }, this._db);
-    this._pendingQuery = wrapper.executeAsync([query]);
+        handleCompletion: aReason => {
+          if (this._pendingSearch != pendingSearch)
+            return;
+          TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY);
+          this._finishSearch();
+        }
+      }, this._db);
+      this._pendingQuery = wrapper.executeAsync([query]);
+    }.bind(this));
   },
 
   /**
    * Execute an asynchronous search through places, and complete
    * up to the next URL separator.
    */
   _queryURL: function UIC__queryURL()
   {
@@ -1541,70 +1545,70 @@ urlInlineComplete.prototype = {
     let params = query.params;
     params.matchBehavior = MATCH_BEGINNING_CASE_SENSITIVE;
     params.searchBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
                              Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED |
                              Ci.mozIPlacesAutoComplete.BEHAVIOR_URL;
     params.searchString = this._currentSearchString;
 
     // Execute the query.
-    let ac = this;
     let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
-      handleResult: function(aResultSet) {
+      handleResult: aResultSet => {
         let row = aResultSet.getNextRow();
         let value = row.getResultByIndex(0);
         let url = fixupSearchText(value);
 
         let prefix = value.slice(0, value.length - stripPrefix(value).length);
 
         // We must complete the URL up to the next separator (which is /, ? or #).
-        let separatorIndex = url.slice(ac._currentSearchString.length)
+        let separatorIndex = url.slice(this._currentSearchString.length)
                                 .search(/[\/\?\#]/);
         if (separatorIndex != -1) {
-          separatorIndex += ac._currentSearchString.length;
+          separatorIndex += this._currentSearchString.length;
           if (url[separatorIndex] == "/") {
             separatorIndex++; // Include the "/" separator
           }
           url = url.slice(0, separatorIndex);
         }
 
         // Add the result.
         // If the untrimmed value doesn't preserve the user's input just
         // ignore it and complete to the found url.
         let untrimmedURL = prefix + url;
         if (untrimmedURL &&
-            !untrimmedURL.toLowerCase().contains(ac._originalSearchString.toLowerCase())) {
+            !untrimmedURL.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
           untrimmedURL = null;
          }
 
-        ac._result.appendMatch(ac._strippedPrefix + url, "", "", "", untrimmedURL);
+        this._result.appendMatch(this._strippedPrefix + url, "", "", "", untrimmedURL);
 
         // handleCompletion() will cause the result listener to be called, and
         // will display the result in the UI.
       },
 
-      handleError: function(aError) {
+      handleError: aError => {
         Components.utils.reportError(
           "URL Inline Complete: An async statement encountered an " +
           "error: " + aError.result + ", '" + aError.message + "'");
       },
 
-      handleCompletion: function(aReason) {
-        ac._finishSearch();
+      handleCompletion: aReason => {
+        this._finishSearch();
       }
     }, this._db);
     this._pendingQuery = wrapper.executeAsync([query]);
   },
 
   stopSearch: function UIC_stopSearch()
   {
     delete this._originalSearchString;
     delete this._currentSearchString;
     delete this._result;
     delete this._listener;
+    delete this._pendingSearch;
 
     if (this._pendingQuery) {
       this._pendingQuery.cancel();
       delete this._pendingQuery;
     }
   },
 
   /**
--- a/toolkit/components/places/tests/autocomplete/test_keyword_search.js
+++ b/toolkit/components/places/tests/autocomplete/test_keyword_search.js
@@ -33,28 +33,30 @@ let kURIs = [
   keyBase,
   otherBase + "%s",
   keyBase + "twoKey",
   otherBase + "twoKey"
 ];
 let kTitles = [
   "Generic page title",
   "Keyword title",
+  "abc",
+  "xyz"
 ];
 
 // Add the keyword bookmark
 addPageBook(0, 0, 1, [], keyKey);
 // Add in the "fake pages" for keyword searches
-gPages[1] = [1,0];
-gPages[2] = [2,0];
-gPages[3] = [3,0];
-gPages[4] = [4,0];
+gPages[1] = [1,2];
+gPages[2] = [2,2];
+gPages[3] = [3,2];
+gPages[4] = [4,2];
 // Add a page into history
-addPageBook(5, 0);
-gPages[6] = [6,0];
+addPageBook(5, 2);
+gPages[6] = [6,2];
 
 // Provide for each test: description; search terms; array of gPages indices of
 // pages that should match; optional function to be run before the test
 let gTests = [
   ["0: Plain keyword query",
    keyKey + " term", [1]],
   ["1: Multi-word keyword query",
    keyKey + " multi word", [2]],
--- a/toolkit/components/places/tests/inline/head_autocomplete.js
+++ b/toolkit/components/places/tests/inline/head_autocomplete.js
@@ -117,84 +117,83 @@ function ensure_results(aSearchString, a
   controller.input = input;
 
   let numSearchesStarted = 0;
   input.onSearchBegin = function() {
     numSearchesStarted++;
     do_check_eq(numSearchesStarted, 1);
   };
 
-  input.onSearchComplete = function() {
-    // We should be running only one query.
-    do_check_eq(numSearchesStarted, 1);
 
-    // Check the autoFilled result.
-    do_check_eq(input.textValue, autoFilledValue);
+  let promise = new Promise(resolve => {
+    input.onSearchComplete = function() {
+      // We should be running only one query.
+      do_check_eq(numSearchesStarted, 1);
+
+      // Check the autoFilled result.
+      do_check_eq(input.textValue, autoFilledValue);
 
-    if (completedValue) {
-      // Now force completion and check correct casing of the result.
-      // This ensures the controller is able to do its magic case-preserving
-      // stuff and correct replacement of the user's casing with result's one.
-      controller.handleEnter(false);
-      do_check_eq(input.textValue, completedValue);
-    }
+      if (completedValue) {
+        // Now force completion and check correct casing of the result.
+        // This ensures the controller is able to do its magic case-preserving
+        // stuff and correct replacement of the user's casing with result's one.
+        controller.handleEnter(false);
+        do_check_eq(input.textValue, completedValue);
+      }
 
-    waitForCleanup(run_next_test);
-  };
+      // Cleanup.
+      remove_all_bookmarks();
+      PlacesTestUtils.clearHistory().then(resolve);
+    };
+  });
 
   do_print("Searching for: '" + aSearchString + "'");
   controller.startSearch(aSearchString);
+
+  return promise;
 }
 
 function run_test() {
   do_register_cleanup(function () {
     Services.prefs.clearUserPref("browser.urlbar.autocomplete.enabled");
     Services.prefs.clearUserPref("browser.urlbar.autoFill");
     Services.prefs.clearUserPref("browser.urlbar.autoFill.typed");
   });
 
   gAutoCompleteTests.forEach(function (testData) {
     let [description, searchString, expectedValue, setupFunc] = testData;
-    add_test(function () {
+    add_task(function* () {
       do_print(description);
       Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", true);
       Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
       Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
 
       if (setupFunc) {
-        setupFunc();
+        yield setupFunc();
       }
 
       // At this point frecency could still be updating due to latest pages
       // updates.
       // This is not a problem in real life, but autocomplete tests should
       // return reliable resultsets, thus we have to wait.
-      PlacesTestUtils.promiseAsyncUpdates()
-                     .then(() => ensure_results(searchString, expectedValue));
+      yield PlacesTestUtils.promiseAsyncUpdates();
+      yield ensure_results(searchString, expectedValue);
     })
   }, this);
 
   run_next_test();
 }
 
 let gAutoCompleteTests = [];
 function add_autocomplete_test(aTestData) {
   gAutoCompleteTests.push(aTestData);
 }
 
-function waitForCleanup(aCallback) {
-  remove_all_bookmarks();
-  PlacesTestUtils.clearHistory().then(aCallback);
-}
-
-function addBookmark(aBookmarkObj) {
+function* addBookmark(aBookmarkObj) {
   do_check_true(!!aBookmarkObj.url);
-  let parentId = aBookmarkObj.parentId ? aBookmarkObj.parentId
-                                       : PlacesUtils.unfiledBookmarksFolderId;
-  let itemId = PlacesUtils.bookmarks
-                          .insertBookmark(parentId,
-                                          NetUtil.newURI(aBookmarkObj.url),
-                                          PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                          "A bookmark");
+  yield PlacesUtils.bookmarks
+                   .insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+                             url: aBookmarkObj.url });
   if (aBookmarkObj.keyword) {
-    PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aBookmarkObj.keyword);
+    yield PlacesUtils.keywords.insert({ keyword: aBookmarkObj.keyword,
+                                        url: aBookmarkObj.url });
   }
 }
--- a/toolkit/components/places/tests/inline/test_autocomplete_functional.js
+++ b/toolkit/components/places/tests/inline/test_autocomplete_functional.js
@@ -3,131 +3,118 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Functional tests for inline autocomplete
 
 add_autocomplete_test([
   "Check disabling autocomplete disables autofill",
   "vis",
   "vis",
-  function ()
-  {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false);
-    PlacesTestUtils.addVisits({
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://visit.mozilla.org"),
       transition: TRANSITION_TYPED
     });
   }
 ]);
 
 add_autocomplete_test([
   "Check disabling autofill disables autofill",
   "vis",
   "vis",
-  function ()
-  {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
-    PlacesTestUtils.addVisits({
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://visit.mozilla.org"),
       transition: TRANSITION_TYPED
     });
   }
 ]);
 
 add_autocomplete_test([
   "Add urls, check for correct order",
   "vis",
   "visit2.mozilla.org/",
-  function ()
-  {
+  function* () {
     let places = [{ uri: NetUtil.newURI("http://visit1.mozilla.org") },
                   { uri: NetUtil.newURI("http://visit2.mozilla.org"),
                     transition: TRANSITION_TYPED }];
-    PlacesTestUtils.addVisits(places);
+    yield PlacesTestUtils.addVisits(places);
   }
 ]);
 
 add_autocomplete_test([
   "Add urls, make sure www and http are ignored",
   "visit1",
   "visit1.mozilla.org/",
-  function ()
-  {
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit1.mozilla.org"));
+  function* () {
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit1.mozilla.org"));
   }
 ]);
 
 add_autocomplete_test([
   "Autocompleting after an existing host completes to the url",
   "visit3.mozilla.org/",
   "visit3.mozilla.org/",
-  function ()
-  {
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit3.mozilla.org"));
+  function* () {
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit3.mozilla.org"));
   }
 ]);
 
 add_autocomplete_test([
   "Searching for www.me should yield www.me.mozilla.org/",
   "www.me",
   "www.me.mozilla.org/",
-  function ()
-  {
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://www.me.mozilla.org"));
+  function* () {
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://www.me.mozilla.org"));
   }
 ]);
 
 add_autocomplete_test([
   "With a bookmark and history, the query result should be the bookmark",
   "bookmark",
   "bookmark1.mozilla.org/",
-  function ()
-  {
-    addBookmark({ url: "http://bookmark1.mozilla.org/", });
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://bookmark1.mozilla.org/foo"));
+  function* () {
+    yield addBookmark({ url: "http://bookmark1.mozilla.org/", });
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://bookmark1.mozilla.org/foo"));
   }
 ]);
 
 add_autocomplete_test([
   "Check to make sure we get the proper results with full paths",
   "smokey",
   "smokey.mozilla.org/",
-  function ()
-  {
-
+  function* () {
     let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") },
                   { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }];
-    PlacesTestUtils.addVisits(places);
+    yield PlacesTestUtils.addVisits(places);
   }
 ]);
 
 add_autocomplete_test([
   "Check to make sure we autocomplete to the following '/'",
   "smokey.mozilla.org/fo",
   "smokey.mozilla.org/foo/",
-  function ()
-  {
-
+  function* () {
     let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") },
                   { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }];
-    PlacesTestUtils.addVisits(places);
+    yield PlacesTestUtils.addVisits(places);
   }
 ]);
 
 add_autocomplete_test([
   "Check to make sure we autocomplete after ?",
   "smokey.mozilla.org/foo?",
   "smokey.mozilla.org/foo?bacon=delicious",
-  function ()
-  {
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious"));
+  function* () {
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious"));
   }
 ]);
 
 add_autocomplete_test([
   "Check to make sure we autocomplete after #",
   "smokey.mozilla.org/foo?bacon=delicious#bar",
   "smokey.mozilla.org/foo?bacon=delicious#bar",
-  function ()
-  {
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar"));
+  function* () {
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar"));
   }
 ]);
--- a/toolkit/components/places/tests/inline/test_casing.js
+++ b/toolkit/components/places/tests/inline/test_casing.js
@@ -1,102 +1,102 @@
 /* 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/. */
 
 add_autocomplete_test([
   "Searching for cased entry 1",
   "MOZ",
   { autoFilled: "MOZilla.org/", completed: "mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/test/") });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for cased entry 2",
   "mozilla.org/T",
   { autoFilled: "mozilla.org/T", completed: "mozilla.org/T" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/test/") });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for cased entry 3",
   "mozilla.org/T",
   { autoFilled: "mozilla.org/Test/", completed: "http://mozilla.org/Test/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for cased entry 4",
   "mOzilla.org/t",
   { autoFilled: "mOzilla.org/t", completed: "mOzilla.org/t" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for cased entry 5",
   "mOzilla.org/T",
   { autoFilled: "mOzilla.org/Test/", completed: "http://mozilla.org/Test/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry",
   "http://mOz",
   { autoFilled: "http://mOzilla.org/", completed: "http://mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with www",
   "http://www.mOz",
   { autoFilled: "http://www.mOzilla.org/", completed: "http://www.mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with path",
   "http://mOzilla.org/t",
   { autoFilled: "http://mOzilla.org/t", completed: "http://mOzilla.org/t" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with path 2",
   "http://mOzilla.org/T",
   { autoFilled: "http://mOzilla.org/Test/", completed: "http://mozilla.org/Test/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with www and path",
   "http://www.mOzilla.org/t",
   { autoFilled: "http://www.mOzilla.org/t", completed: "http://www.mOzilla.org/t" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with www and path 2",
   "http://www.mOzilla.org/T",
   { autoFilled: "http://www.mOzilla.org/Test/", completed: "http://www.mozilla.org/Test/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
   },
 ]);
--- a/toolkit/components/places/tests/inline/test_do_not_trim.js
+++ b/toolkit/components/places/tests/inline/test_do_not_trim.js
@@ -4,76 +4,76 @@
 
 // Inline should never return matches shorter than the search string, since
 // that largely confuses completeDefaultIndex
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 1",
   "mozilla.org ",
   "mozilla.org ",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://mozilla.org/link/"),
       transition: TRANSITION_TYPED
     });
   }
 ]);
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 2",
   "mozilla.org/ ",
   "mozilla.org/ ",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://mozilla.org/link/"),
       transition: TRANSITION_TYPED
     });
   }
 ]);
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 3",
   "mozilla.org/link ",
   "mozilla.org/link ",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://mozilla.org/link/"),
       transition: TRANSITION_TYPED
     });
   }
 ]);
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 4",
   "mozilla.org/link/ ",
   "mozilla.org/link/ ",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://mozilla.org/link/"),
       transition: TRANSITION_TYPED
     });
   }
 ]);
 
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 5",
   "moz illa ",
   "moz illa ",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://mozilla.org/link/"),
       transition: TRANSITION_TYPED
     });
   }
 ]);
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 6",
   " mozilla",
   " mozilla",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://mozilla.org/link/"),
       transition: TRANSITION_TYPED
     });
   }
 ]);
--- a/toolkit/components/places/tests/inline/test_keywords.js
+++ b/toolkit/components/places/tests/inline/test_keywords.js
@@ -1,48 +1,48 @@
 /* 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/. */
 
 add_autocomplete_test([
   "Searching for non-keyworded entry should autoFill it",
   "moz",
   "mozilla.org/",
-  function () {
-    addBookmark({ url: "http://mozilla.org/test/" });
+  function* () {
+    yield addBookmark({ url: "http://mozilla.org/test/" });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for keyworded entry should not autoFill it",
   "moz",
   "moz",
-  function () {
-    addBookmark({ url: "http://mozilla.org/test/", keyword: "moz" });
+  function* () {
+    yield addBookmark({ url: "http://mozilla.org/test/", keyword: "moz" });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for more than keyworded entry should autoFill it",
   "mozi",
   "mozilla.org/",
-  function () {
-    addBookmark({ url: "http://mozilla.org/test/", keyword: "moz" });
+  function* () {
+    yield addBookmark({ url: "http://mozilla.org/test/", keyword: "moz" });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for less than keyworded entry should autoFill it",
   "mo",
   "mozilla.org/",
-  function () {
-    addBookmark({ url: "http://mozilla.org/test/", keyword: "moz" });
+  function* () {
+    yield addBookmark({ url: "http://mozilla.org/test/", keyword: "moz" });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for keyworded entry is case-insensitive",
   "MoZ",
   "MoZ",
-  function () {
-    addBookmark({ url: "http://mozilla.org/test/", keyword: "moz" });
+  function* () {
+    yield addBookmark({ url: "http://mozilla.org/test/", keyword: "moz" });
   }
 ]);
--- a/toolkit/components/places/tests/inline/test_queryurl.js
+++ b/toolkit/components/places/tests/inline/test_queryurl.js
@@ -1,60 +1,60 @@
 /* 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/. */
 
 add_autocomplete_test([
   "Searching for host match without slash should match host",
   "file",
   "file.org/",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://file.org/test/"),
       transition: TRANSITION_TYPED
     }, {
       uri: NetUtil.newURI("file:///c:/test.html"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching match with slash at the end should do nothing",
   "file.org/",
   "file.org/",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://file.org/test/"),
       transition: TRANSITION_TYPED
     }, {
       uri: NetUtil.newURI("file:///c:/test.html"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching match with slash in the middle should match url",
   "file.org/t",
   "file.org/test/",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://file.org/test/"),
       transition: TRANSITION_TYPED
     }, {
       uri: NetUtil.newURI("file:///c:/test.html"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for non-host match without slash should not match url",
   "file",
   "file",
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("file:///c:/test.html"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
--- a/toolkit/components/places/tests/inline/test_trimming.js
+++ b/toolkit/components/places/tests/inline/test_trimming.js
@@ -1,313 +1,306 @@
 /* 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/. */
 
 add_autocomplete_test([
   "Searching for untrimmed https://www entry",
   "mo",
   { autoFilled: "mozilla.org/", completed: "https://www.mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("https://www.mozilla.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed https://www entry with path",
   "mozilla.org/t",
   { autoFilled: "mozilla.org/test/", completed: "https://www.mozilla.org/test/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("https://www.mozilla.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed https:// entry",
   "mo",
   { autoFilled: "mozilla.org/", completed: "https://mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("https://mozilla.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed https:// entry with path",
   "mozilla.org/t",
   { autoFilled: "mozilla.org/test/", completed: "https://mozilla.org/test/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("https://mozilla.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed http://www entry",
   "mo",
   { autoFilled: "mozilla.org/", completed: "www.mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://www.mozilla.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed http://www entry with path",
   "mozilla.org/t",
   { autoFilled: "mozilla.org/test/", completed: "http://www.mozilla.org/test/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://www.mozilla.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed ftp:// entry",
   "mo",
   { autoFilled: "mozilla.org/", completed: "ftp://mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("ftp://mozilla.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed ftp:// entry with path",
   "mozilla.org/t",
   { autoFilled: "mozilla.org/test/", completed: "ftp://mozilla.org/test/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("ftp://mozilla.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Ensuring correct priority 1",
   "mo",
   { autoFilled: "mozilla.org/", completed: "mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("https://www.mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("https://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("ftp://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://www.mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
+  function* () {
+    yield PlacesTestUtils.addVisits([
+      { uri: NetUtil.newURI("https://www.mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("https://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("ftp://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://www.mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      }
+    ]);
   },
 ]);
 
 add_autocomplete_test([
   "Ensuring correct priority 2",
   "mo",
   { autoFilled: "mozilla.org/", completed: "mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("https://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("ftp://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://www.mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
+  function* () {
+    yield PlacesTestUtils.addVisits([
+      { uri: NetUtil.newURI("https://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("ftp://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://www.mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      }
+    ]);
   },
 ]);
 
 add_autocomplete_test([
   "Ensuring correct priority 3",
   "mo",
   { autoFilled: "mozilla.org/", completed: "mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("ftp://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://www.mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
+  function* () {
+    yield PlacesTestUtils.addVisits([
+      { uri: NetUtil.newURI("ftp://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://www.mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      }
+    ]);
   },
 ]);
 
 add_autocomplete_test([
   "Ensuring correct priority 4",
   "mo",
   { autoFilled: "mozilla.org/", completed: "mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://www.mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
+  function* () {
+    yield PlacesTestUtils.addVisits([
+      { uri: NetUtil.newURI("http://www.mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      }
+    ]);
   },
 ]);
 
 add_autocomplete_test([
   "Ensuring correct priority 5",
   "mo",
   { autoFilled: "mozilla.org/", completed: "ftp://mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("ftp://mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("ftp://www.mozilla.org/test/"),
-      transition: TRANSITION_TYPED
-    });
+  function* () {
+    yield PlacesTestUtils.addVisits([
+      { uri: NetUtil.newURI("ftp://mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("ftp://www.mozilla.org/test/"),
+        transition: TRANSITION_TYPED
+      }
+    ]);
   },
 ]);
 
 add_autocomplete_test([
   "Ensuring correct priority 6",
   "mo",
   { autoFilled: "mozilla.org/", completed: "www.mozilla.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://www.mozilla.org/test1/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://www.mozilla.org/test2/"),
-      transition: TRANSITION_TYPED
-    });
+  function* () {
+    yield PlacesTestUtils.addVisits([
+      { uri: NetUtil.newURI("http://www.mozilla.org/test1/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://www.mozilla.org/test2/"),
+        transition: TRANSITION_TYPED
+      }
+    ]);
   },
 ]);
 
 add_autocomplete_test([
   "Ensuring longer domain can't match",
   "mo",
   { autoFilled: "mozilla.co/", completed: "mozilla.co/" },
-  function () {
+  function* () {
     // The .co should be preferred, but should not get the https from the .com.
     // The .co domain must be added later to activate the trigger bug.
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("https://mozilla.com/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://mozilla.co/"),
-      transition: TRANSITION_TYPED
-    });
-    PlacesTestUtils.addVisits({
-      uri: NetUtil.newURI("http://mozilla.co/"),
-      transition: TRANSITION_TYPED
-    });
+    yield PlacesTestUtils.addVisits([
+      { uri: NetUtil.newURI("https://mozilla.com/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://mozilla.co/"),
+        transition: TRANSITION_TYPED
+      },
+      { uri: NetUtil.newURI("http://mozilla.co/"),
+        transition: TRANSITION_TYPED
+      }
+    ]);
   },
 ]);
 
 add_autocomplete_test([
   "Searching for URL with characters that are normally escaped",
   "https://www.mozilla.org/啊-test",
   { autoFilled: "https://www.mozilla.org/啊-test", completed: "https://www.mozilla.org/啊-test" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("https://www.mozilla.org/啊-test"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Don't return unsecure URL when searching for secure ones",
   "https://test.moz.org/t",
   { autoFilled: "https://test.moz.org/test/", completed: "https://test.moz.org/test/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://test.moz.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Don't return unsecure domain when searching for secure ones",
   "https://test.moz",
   { autoFilled: "https://test.moz.org/", completed: "https://test.moz.org/" },
-  function () {
-    PlacesTestUtils.addVisits({
+  function* () {
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://test.moz.org/test/"),
       transition: TRANSITION_TYPED
     });
   },
 ]);
 
 add_autocomplete_test([
   "Untyped is not accounted for www",
   "mo",
   { autoFilled: "moz.org/", completed: "moz.org/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.moz.org/test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.moz.org/test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Untyped is not accounted for ftp",
   "mo",
   { autoFilled: "moz.org/", completed: "moz.org/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("ftp://moz.org/test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("ftp://moz.org/test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Untyped is not accounted for https",
   "mo",
   { autoFilled: "moz.org/", completed: "moz.org/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("https://moz.org/test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("https://moz.org/test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Untyped is not accounted for https://www",
   "mo",
   { autoFilled: "moz.org/", completed: "moz.org/" },
-  function () {
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("https://www.moz.org/test/") });
+  function* () {
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("https://www.moz.org/test/") });
   },
 ]);
--- a/toolkit/components/places/tests/inline/test_typed.js
+++ b/toolkit/components/places/tests/inline/test_typed.js
@@ -4,67 +4,67 @@
 
 // First do searches with typed behavior forced to false, so later tests will
 // ensure autocomplete is able to dinamically switch behavior.
 
 add_autocomplete_test([
   "Searching for domain should autoFill it",
   "moz",
   "mozilla.org/",
-  function () {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
   }
 ]);
 
 add_autocomplete_test([
   "Searching for url should autoFill it",
   "mozilla.org/li",
   "mozilla.org/link/",
-  function () {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
   }
 ]);
 
 // Now do searches with typed behavior forced to true.
 
 add_autocomplete_test([
   "Searching for non-typed domain should not autoFill it",
   "moz",
   "moz",
-  function () {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
   }
 ]);
 
 add_autocomplete_test([
   "Searching for typed domain should autoFill it",
   "moz",
   "mozilla.org/",
-  function () {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/typed/"),
-                       transition: TRANSITION_TYPED });
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/typed/"),
+                                      transition: TRANSITION_TYPED });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for non-typed url should not autoFill it",
   "mozilla.org/li",
   "mozilla.org/li",
-  function () {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
-    PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
+    yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
   }
 ]);
 
 add_autocomplete_test([
   "Searching for typed url should autoFill it",
   "mozilla.org/li",
   "mozilla.org/link/",
-  function () {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
-    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"),
-                       transition: TRANSITION_TYPED });
+    yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"),
+                                      transition: TRANSITION_TYPED });
   }
 ]);
--- a/toolkit/components/places/tests/inline/test_zero_frecency.js
+++ b/toolkit/components/places/tests/inline/test_zero_frecency.js
@@ -3,29 +3,29 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Ensure inline autocomplete doesn't return zero frecency pages.
 
 add_autocomplete_test([
   "Searching for zero frecency domain should not autoFill it",
   "moz",
   "moz",
-  function () {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
-    PlacesTestUtils.addVisits({
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://mozilla.org/framed_link/"),
       transition: TRANSITION_FRAMED_LINK
     });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for zero frecency url should not autoFill it",
   "mozilla.org/f",
   "mozilla.org/f",
-  function () {
+  function* () {
     Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
-    PlacesTestUtils.addVisits({
+    yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://mozilla.org/framed_link/"),
       transition: TRANSITION_FRAMED_LINK
     });
   }
 ]);
--- a/toolkit/components/places/tests/migration/test_current_from_v26.js
+++ b/toolkit/components/places/tests/migration/test_current_from_v26.js
@@ -72,28 +72,27 @@ add_task(function* database_is_valid() {
 
   let db = yield PlacesUtils.promiseDBConnection();
   Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
 });
 
 add_task(function* test_keywords() {
   // When 2 urls have the same keyword, if one has postData it will be
   // preferred.
-  let [ url1, postData1 ] = PlacesUtils.getURLAndPostDataForKeyword("kw1");
-  Assert.equal(url1, "http://test2.com/");
-  Assert.equal(postData1, "postData1");
-  let [ url2, postData2 ] = PlacesUtils.getURLAndPostDataForKeyword("kw2");
-  Assert.equal(url2, "http://test2.com/");
-  Assert.equal(postData2, "postData2");
-  let [ url3, postData3 ] = PlacesUtils.getURLAndPostDataForKeyword("kw3");
-  Assert.equal(url3, "http://test1.com/");
-  Assert.equal(postData3, null);
-  let [ url4, postData4 ] = PlacesUtils.getURLAndPostDataForKeyword("kw4");
-  Assert.equal(url4, null);
-  Assert.equal(postData4, null);
-  let [ url5, postData5 ] = PlacesUtils.getURLAndPostDataForKeyword("kw5");
-  Assert.equal(url5, "http://test3.com/");
-  Assert.equal(postData5, "postData3");
+  let entry1 = yield PlacesUtils.keywords.fetch("kw1");
+  Assert.equal(entry1.url.href, "http://test2.com/");
+  Assert.equal(entry1.postData, "postData1");
+  let entry2 = yield PlacesUtils.keywords.fetch("kw2");
+  Assert.equal(entry2.url.href, "http://test2.com/");
+  Assert.equal(entry2.postData, "postData2");
+  let entry3 = yield PlacesUtils.keywords.fetch("kw3");
+  Assert.equal(entry3.url.href, "http://test1.com/");
+  Assert.equal(entry3.postData, null);
+  let entry4 = yield PlacesUtils.keywords.fetch("kw4");
+  Assert.equal(entry4, null);
+  let entry5 = yield PlacesUtils.keywords.fetch("kw5");
+  Assert.equal(entry5.url.href, "http://test3.com/");
+  Assert.equal(entry5.postData, "postData3");
 
   Assert.equal((yield foreign_count("http://test1.com/")), 5); // 4 bookmark2 + 1 keywords
   Assert.equal((yield foreign_count("http://test2.com/")), 4); // 2 bookmark2 + 2 keywords
   Assert.equal((yield foreign_count("http://test3.com/")), 3); // 2 bookmark2 + 1 keywords
 });
--- a/toolkit/components/places/tests/migration/test_current_from_v27.js
+++ b/toolkit/components/places/tests/migration/test_current_from_v27.js
@@ -60,18 +60,18 @@ add_task(function* database_is_valid() {
 
   let db = yield PlacesUtils.promiseDBConnection();
   Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
 });
 
 add_task(function* test_keywords() {
   // When 2 urls have the same keyword, if one has postData it will be
   // preferred.
-  let [ url1, postData1 ] = PlacesUtils.getURLAndPostDataForKeyword("kw1");
-  Assert.equal(url1, "http://test2.com/");
-  Assert.equal(postData1, "postData1");
-  let [ url2, postData2 ] = PlacesUtils.getURLAndPostDataForKeyword("kw2");
-  Assert.equal(url2, "http://test2.com/");
-  Assert.equal(postData2, "postData2");
-  let [ url3, postData3 ] = PlacesUtils.getURLAndPostDataForKeyword("kw3");
-  Assert.equal(url3, "http://test1.com/");
-  Assert.equal(postData3, null);
+  let entry1 = yield PlacesUtils.keywords.fetch("kw1");
+  Assert.equal(entry1.url.href, "http://test2.com/");
+  Assert.equal(entry1.postData, "postData1");
+  let entry2 = yield PlacesUtils.keywords.fetch("kw2");
+  Assert.equal(entry2.url.href, "http://test2.com/");
+  Assert.equal(entry2.postData, "postData2");
+  let entry3 = yield PlacesUtils.keywords.fetch("kw3");
+  Assert.equal(entry3.url.href, "http://test1.com/");
+  Assert.equal(entry3.postData, null);
 });
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -221,17 +221,18 @@ let addBookmark = Task.async(function* (
   let bm = yield PlacesUtils.bookmarks.insert({
     parentGuid: (yield PlacesUtils.promiseItemGuid(parentId)),
     title: aBookmarkObj.title || "A bookmark",
     url: aBookmarkObj.uri
   });
   let itemId = yield PlacesUtils.promiseItemId(bm.guid);
 
   if (aBookmarkObj.keyword) {
-    PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aBookmarkObj.keyword);
+    yield PlacesUtils.keywords.insert({ keyword: aBookmarkObj.keyword,
+                                        url: aBookmarkObj.uri.spec });
   }
 
   if (aBookmarkObj.tags) {
     PlacesUtils.tagging.tagURI(aBookmarkObj.uri, aBookmarkObj.tags);
   }
 });
 
 function addOpenPages(aUri, aCount=1) {
--- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js
@@ -19,49 +19,49 @@ add_task(function* test_keyword_searc() 
     { uri: uri1, title: "Generic page title" },
     { uri: uri2, title: "Generic page title" }
   ]);
   yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"});
 
   do_print("Plain keyword query");
   yield check_autocomplete({
     search: "key term",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "Generic page title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "abc", style: ["keyword"] } ]
   });
 
   do_print("Multi-word keyword query");
   yield check_autocomplete({
     search: "key multi word",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=multi+word"), title: "Generic page title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=multi+word"), title: "abc", style: ["keyword"] } ]
   });
 
   do_print("Keyword query with +");
   yield check_autocomplete({
     search: "key blocking+",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "Generic page title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "abc", style: ["keyword"] } ]
   });
 
   do_print("Unescaped term in query");
   yield check_autocomplete({
     search: "key ユニコード",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=ユニコード"), title: "Generic page title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=ユニコード"), title: "abc", style: ["keyword"] } ]
   });
 
   do_print("Keyword that happens to match a page");
   yield check_autocomplete({
     search: "key ThisPageIsInHistory",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"), title: "Generic page title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"), title: "abc", style: ["keyword"] } ]
   });
 
   do_print("Keyword without query (without space)");
   yield check_autocomplete({
     search: "key",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Generic page title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "abc", style: ["keyword"] } ]
   });
 
   do_print("Keyword without query (with space)");
   yield check_autocomplete({
     search: "key ",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Generic page title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "abc", style: ["keyword"] } ]
   });
 
   yield cleanup();
 });
--- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js
@@ -20,55 +20,55 @@ add_task(function* test_keyword_search()
     { uri: uri2, title: "Generic page title" }
   ]);
   yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"});
 
   do_print("Plain keyword query");
   yield check_autocomplete({
     search: "key term",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}), title: "abc", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Multi-word keyword query");
   yield check_autocomplete({
     search: "key multi word",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi+word", input: "key multi word"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi+word", input: "key multi word"}), title: "abc", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Keyword query with +");
   yield check_autocomplete({
     search: "key blocking+",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}), title: "abc", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Unescaped term in query");
   yield check_autocomplete({
     search: "key ユニコード",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ユニコード", input: "key ユニコード"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ユニコード", input: "key ユニコード"}), title: "abc", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Keyword that happens to match a page");
   yield check_autocomplete({
     search: "key ThisPageIsInHistory",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}), title: "abc", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Keyword without query (without space)");
   yield check_autocomplete({
     search: "key",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}), title: "abc", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Keyword without query (with space)");
   yield check_autocomplete({
     search: "key ",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}), title: "Generic page title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}), title: "abc", style: [ "action", "keyword" ] } ]
   });
 
   yield cleanup();
 });
deleted file mode 100644
--- a/toolkit/components/places/tests/unit/test_398914.js
+++ /dev/null
@@ -1,30 +0,0 @@
-function run_test() {
-  var testURI = uri("http://foo.com");
-
-  /*
-  1. Create a bookmark for a URI, with a keyword and post data.
-  2. Create a bookmark for the same URI, with a different keyword and different post data.
-  3. Confirm that our method for getting a URI+postdata retains bookmark affinity.
-  */
-  var bm1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, testURI, -1, "blah");
-  PlacesUtils.bookmarks.setKeywordForBookmark(bm1, "foo");
-  PlacesUtils.setPostDataForBookmark(bm1, "pdata1");
-  var bm2 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, testURI, -1, "blah");
-  PlacesUtils.bookmarks.setKeywordForBookmark(bm2, "bar");
-  PlacesUtils.setPostDataForBookmark(bm2, "pdata2");
-
-  // check kw, pd for bookmark 1
-  var url, postdata;
-  [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo");
-  do_check_eq(testURI.spec, url);
-  do_check_eq(postdata, "pdata1");
-
-  // check kw, pd for bookmark 2
-  [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("bar");
-  do_check_eq(testURI.spec, url);
-  do_check_eq(postdata, "pdata2");
-
-  // cleanup
-  PlacesUtils.bookmarks.removeItem(bm1);
-  PlacesUtils.bookmarks.removeItem(bm2);
-}
--- a/toolkit/components/places/tests/unit/test_keywords.js
+++ b/toolkit/components/places/tests/unit/test_keywords.js
@@ -69,17 +69,17 @@ function expectBookmarkNotifications() {
   });
   PlacesUtils.bookmarks.addObserver(observer, false);
   return observer;
 }
 
 add_task(function* test_invalid_input() {
   Assert.throws(() => PlacesUtils.keywords.fetch(null),
                 /Invalid keyword/);
-  Assert.throws(() => PlacesUtils.keywords.fetch(""),
+  Assert.throws(() => PlacesUtils.keywords.fetch({}),
                 /Invalid keyword/);
   Assert.throws(() => PlacesUtils.keywords.fetch(5),
                 /Invalid keyword/);
 
   Assert.throws(() => PlacesUtils.keywords.insert(null),
                 /Input should be a valid object/);
   Assert.throws(() => PlacesUtils.keywords.insert("test"),
                 /Input should be a valid object/);
--- a/toolkit/components/places/tests/unit/test_placesTxn.js
+++ b/toolkit/components/places/tests/unit/test_placesTxn.js
@@ -712,35 +712,55 @@ add_test(function test_sort_folder_by_na
   do_check_eq(0, bmsvc.getItemIndex(b1));
   do_check_eq(1, bmsvc.getItemIndex(b2));
   do_check_eq(2, bmsvc.getItemIndex(b3));
 
   run_next_test();
 });
 
 add_test(function test_edit_postData() {
-  const POST_DATA_ANNO = "bookmarkProperties/POSTData";
-  let postData = "post-test_edit_postData";
-  let testURI = NetUtil.newURI("http://test_edit_postData.com");
-  let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit Post Data");
-  PlacesUtils.bookmarks.setKeywordForBookmark(testBkmId, "kw");
-  let txn = new PlacesEditBookmarkPostDataTransaction(testBkmId, postData);
+  function* promiseKeyword(keyword, href, postData) {
+    while (true) {
+      let entry = yield PlacesUtils.keywords.fetch(keyword);
+      if (entry && entry.url.href == href && entry.postData == postData) {
+        break;
+      }
+
+      yield new Promise(resolve => do_timeout(100, resolve));
+    }
+  }
+
+  Task.spawn(function* () {
+    const POST_DATA_ANNO = "bookmarkProperties/POSTData";
+    let postData = "post-test_edit_postData";
+    let testURI = NetUtil.newURI("http://test_edit_postData.com");
 
-  txn.doTransaction();
-  let [url, post_data] = PlacesUtils.getURLAndPostDataForKeyword("kw");
-  Assert.equal(url, testURI.spec);
-  Assert.equal(postData, post_data);
+    let testBkm = yield PlacesUtils.bookmarks.insert({
+      parentGuid: PlacesUtils.bookmarks.menuGuid,
+      url: "http://test_edit_postData.com",
+      title: "Test edit Post Data"
+    });
+
+    yield PlacesUtils.keywords.insert({
+      keyword: "kw",
+      url: "http://test_edit_postData.com"
+    });
 
-  txn.undoTransaction();
-  [url, post_data] = PlacesUtils.getURLAndPostDataForKeyword("kw");
-  Assert.equal(url, testURI.spec);
-  // We don't allow anymore to set a null post data.
-  //Assert.equal(null, post_data);
+    let testBkmId = yield PlacesUtils.promiseItemId(testBkm.guid);
+    let txn = new PlacesEditBookmarkPostDataTransaction(testBkmId, postData);
+
+    txn.doTransaction();
+    yield promiseKeyword("kw", testURI.spec, postData);
 
-  run_next_test();
+    txn.undoTransaction();
+    entry = yield PlacesUtils.keywords.fetch("kw");
+    Assert.equal(entry.url.href, testURI.spec);
+    // We don't allow anymore to set a null post data.
+    //Assert.equal(null, post_data);
+  }).then(run_next_test);
 });
 
 add_test(function test_tagURI_untagURI() {
   const TAG_1 = "tag-test_tagURI_untagURI-bar";
   const TAG_2 = "tag-test_tagURI_untagURI-foo";
   let tagURI = NetUtil.newURI("http://test_tagURI_untagURI.com");
 
   // Test tagURI
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -18,17 +18,16 @@ support-files =
 [test_317472.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_331487.js]
 [test_384370.js]
 [test_385397.js]
 # Bug 676989: test fails consistently on Android
 fail-if = os == "android"
-[test_398914.js]
 [test_399264_query_to_string.js]
 [test_399264_string_to_query.js]
 [test_399266.js]
 # Bug 676989: test fails consistently on Android
 fail-if = os == "android"
 # Bug 821781: test fails intermittently on Linux
 skip-if = os == "linux"
 [test_402799.js]
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -567,17 +567,22 @@ AboutReader.prototype = {
     }
 
     if (this._windowUnloaded) {
       return;
     }
 
     if (article && article.url == url) {
       this._showContent(article);
+    } else if (this._articlePromise) {
+      // If we were promised an article, show an error message if there's a failure.
+      this._showError();
     } else {
+      // Otherwise, just load the original URL. We can encounter this case when
+      // loading an about:reader URL directly (e.g. opening a reading list item).
       this._win.location.href = url;
     }
   }),
 
   _getArticle: function(url) {
     return new Promise((resolve, reject) => {
       let listener = (message) => {
         this._mm.removeMessageListener("Reader:ArticleData", listener);
@@ -608,49 +613,39 @@ AboutReader.prototype = {
     link.rel = 'shortcut icon';
     link.href = faviconUrl;
 
     doc.getElementsByTagName('head')[0].appendChild(link);
   },
 
   _updateImageMargins: function Reader_updateImageMargins() {
     let windowWidth = this._win.innerWidth;
-    let contentWidth = this._contentElement.offsetWidth;
-    let maxWidthStyle = windowWidth + "px !important";
+    let bodyWidth = this._doc.body.clientWidth;
 
     let setImageMargins = function(img) {
-      if (!img._originalWidth)
-        img._originalWidth = img.offsetWidth;
-
-      let imgWidth = img._originalWidth;
-
-      // If the image is taking more than half of the screen, just make
-      // it fill edge-to-edge.
-      if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55)
-        imgWidth = windowWidth;
+      // If the image is at least as wide as the window, make it fill edge-to-edge on mobile.
+      if (img.naturalWidth >= windowWidth) {
+        img.setAttribute("moz-reader-full-width", true);
+      } else {
+        img.removeAttribute("moz-reader-full-width");
+      }
 
-      let sideMargin = Math.max((contentWidth - windowWidth) / 2,
-                                (contentWidth - imgWidth) / 2);
-
-      let imageStyle = sideMargin + "px !important";
-      let widthStyle = imgWidth + "px !important";
-
-      let cssText = "max-width: " + maxWidthStyle + ";" +
-                    "width: " + widthStyle + ";" +
-                    "margin-left: " + imageStyle + ";" +
-                    "margin-right: " + imageStyle + ";";
-
-      img.style.cssText = cssText;
+      // If the image is at least half as wide as the body, center it on desktop.
+      if (img.naturalWidth >= bodyWidth/2) {
+        img.setAttribute("moz-reader-center", true);
+      } else {
+        img.removeAttribute("moz-reader-center");
+      }
     }
 
     let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR);
     for (let i = imgs.length; --i >= 0;) {
       let img = imgs[i];
 
-      if (img.width > 0) {
+      if (img.naturalWidth > 0) {
         setImageMargins(img);
       } else {
         img.onload = function() {
           setImageMargins(img);
         }
       }
     }
   },
@@ -659,24 +654,27 @@ AboutReader.prototype = {
     if(!article.dir)
       return;
 
     //Set "dir" attribute on content
     this._contentElement.setAttribute("dir", article.dir);
     this._headerElement.setAttribute("dir", article.dir);
   },
 
-  _showError: function Reader_showError(error) {
+  _showError: function() {
     this._headerElement.style.display = "none";
     this._contentElement.style.display = "none";
 
-    this._messageElement.innerHTML = error;
+    let errorMessage = gStrings.GetStringFromName("aboutReader.loadError");
+    this._messageElement.textContent = errorMessage;
     this._messageElement.style.display = "block";
 
-    this._doc.title = error;
+    this._doc.title = errorMessage;
+
+    this._error = true;
   },
 
   // This function is the JS version of Java's StringUtils.stripCommonSubdomains.
   _stripHost: function Reader_stripHost(host) {
     if (!host)
       return host;
 
     let start = 0;
@@ -725,25 +723,26 @@ AboutReader.prototype = {
   _hideContent: function Reader_hideContent() {
     this._headerElement.style.display = "none";
     this._contentElement.style.display = "none";
   },
 
   _showProgressDelayed: function Reader_showProgressDelayed() {
     this._win.setTimeout(function() {
       // No need to show progress if the article has been loaded,
-      // or if the window has been unloaded.
-      if (this._article || this._windowUnloaded) {
+      // if the window has been unloaded, or if there was an error
+      // trying to load the article.
+      if (this._article || this._windowUnloaded || this._error) {
         return;
       }
 
       this._headerElement.style.display = "none";
       this._contentElement.style.display = "none";
 
-      this._messageElement.innerHTML = gStrings.GetStringFromName("aboutReader.loading");
+      this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading");
       this._messageElement.style.display = "block";
     }.bind(this), 300);
   },
 
   /**
    * Returns the original article URL for this about:reader view.
    */
   _getOriginalUrl: function() {
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -722,16 +722,17 @@ this.TelemetrySession = Object.freeze({
    * Used only for testing purposes.
    */
   reset: function() {
     Impl._sessionId = null;
     Impl._subsessionId = null;
     Impl._previousSubsessionId = null;
     Impl._subsessionCounter = 0;
     Impl._profileSubsessionCounter = 0;
+    Impl._subsessionStartActiveTicks = 0;
     this.uninstall();
     return this.setup();
   },
   /**
    * Used only for testing purposes.
    * @param {Boolean} [aForceSavePending=true] If true, always saves the ping whether Telemetry
    *        can send pings or not, which is used for testing.
    */
@@ -799,32 +800,36 @@ let Impl = {
   // null on first run.
   _previousSubsessionId: null,
   // The running no. of subsessions since the start of the browser session
   _subsessionCounter: 0,
   // The running no. of all subsessions for the whole profile life time
   _profileSubsessionCounter: 0,
   // Date of the last session split
   _subsessionStartDate: null,
+  // The active ticks counted when the subsession starts
+  _subsessionStartActiveTicks: 0,
   // A task performing delayed initialization of the chrome process
   _delayedInitTask: null,
   // The deferred promise resolved when the initialization task completes.
   _delayedInitTaskDeferred: null,
   // Used to serialize session state writes to disk.
   _stateSaveSerializer: new SaveSerializer(),
   // Used to serialize aborted session ping writes to disk.
   _abortedSessionSerializer: new SaveSerializer(),
 
   /**
    * Gets a series of simple measurements (counters). At the moment, this
    * only returns startup data from nsIAppStartup.getStartupInfo().
+   * @param {Boolean} isSubsession True if this is a subsession, false otherwise.
+   * @param {Boolean} clearSubsession True if a new subsession is being started, false otherwise.
    *
    * @return simple measurements as a dictionary.
    */
-  getSimpleMeasurements: function getSimpleMeasurements(forSavedSession) {
+  getSimpleMeasurements: function getSimpleMeasurements(forSavedSession, isSubsession, clearSubsession) {
     this._log.trace("getSimpleMeasurements");
 
     let si = Services.startup.getStartupInfo();
 
     // Measurements common to chrome and content processes.
     let elapsedTime = Date.now() - si.process;
     var ret = {
       totalTime: Math.round(elapsedTime / 1000), // totalTime, in seconds
@@ -904,17 +909,26 @@ let Impl = {
     ret.activeTicks = -1;
     if ("@mozilla.org/datareporting/service;1" in Cc) {
       let drs = Cc["@mozilla.org/datareporting/service;1"]
                   .getService(Ci.nsISupports)
                   .wrappedJSObject;
 
       let sr = drs.getSessionRecorder();
       if (sr) {
-        ret.activeTicks = sr.activeTicks;
+        let activeTicks = sr.activeTicks;
+        if (isSubsession) {
+          activeTicks = sr.activeTicks - this._subsessionStartActiveTicks;
+        }
+
+        if (clearSubsession) {
+          this._subsessionStartActiveTicks = activeTicks;
+        }
+
+        ret.activeTicks = activeTicks;
       }
     }
 
     ret.pingsOverdue = TelemetryFile.pingsOverdue;
     ret.pingsDiscarded = TelemetryFile.pingsDiscarded;
 
     return ret;
   },
@@ -1335,19 +1349,23 @@ let Impl = {
     this._subsessionCounter++;
     this._profileSubsessionCounter++;
   },
 
   getSessionPayload: function getSessionPayload(reason, clearSubsession) {
     this._log.trace("getSessionPayload - reason: " + reason + ", clearSubsession: " + clearSubsession);
 #if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
     clearSubsession = false;
+    const isSubsession = false;
+#else
+    const isSubsession = !this._isClassicReason(reason);
 #endif
 
-    let measurements = this.getSimpleMeasurements(reason == REASON_SAVED_SESSION);
+    let measurements =
+      this.getSimpleMeasurements(reason == REASON_SAVED_SESSION, isSubsession, clearSubsession);
     let info = !IS_CONTENT_PROCESS ? this.getMetadata(reason) : null;
     let payload = this.assemblePayloadWithMeasurements(measurements, info, reason, clearSubsession);
 
     if (!IS_CONTENT_PROCESS && clearSubsession) {
       this.startNewSubsession();
       // Persist session data to disk (don't wait until it completes).
       let sessionData = this._getSessionDataObject();
       this._stateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -659,17 +659,17 @@ add_task(function* test_saveLoadPing() {
   } else {
     checkPingFormat(ping1, PING_TYPE_SAVED_SESSION, true, true);
     checkPayload(ping1.payload, REASON_SAVED_SESSION, 1);
     checkPingFormat(ping2, PING_TYPE_MAIN, true, true);
     checkPayload(ping2.payload, REASON_TEST_PING, 1);
   }
 });
 
-add_task(function* test_checkSubsession() {
+add_task(function* test_checkSubsessionHistograms() {
   if (gIsAndroid) {
     // We don't support subsessions yet on Android.
     return;
   }
 
   let now = new Date(2020, 1, 1, 12, 0, 0);
   let expectedDate = new Date(2020, 1, 1, 0, 0, 0);
   fakeNow(now);
@@ -847,16 +847,65 @@ add_task(function* test_checkSubsession(
   Assert.ok(KEYED_ID in classic.keyedHistograms);
   Assert.ok(KEYED_ID in subsession.keyedHistograms);
   Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 2);
   Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 2);
   Assert.equal(subsession.keyedHistograms[KEYED_ID]["a"].sum, 1);
   Assert.equal(subsession.keyedHistograms[KEYED_ID]["b"].sum, 1);
 });
 
+add_task(function* test_checkSubsessionData() {
+  if (gIsAndroid || !SESSION_RECORDER_EXPECTED) {
+    // We don't support subsessions yet on Android. Also bail out if we
+    // can't use the session recorder.
+    return;
+  }
+
+  // Keep track of the active ticks count if the session recorder is available.
+  let sessionRecorder = gDatareportingService.getSessionRecorder();
+  let activeTicksAtSubsessionStart = sessionRecorder.activeTicks;
+  let expectedActiveTicks = activeTicksAtSubsessionStart;
+
+  incrementActiveTicks = () => {
+    sessionRecorder.incrementActiveTicks();
+    ++expectedActiveTicks;
+  }
+
+  yield TelemetrySession.reset();
+
+  // Both classic and subsession payload data should be the same on the first subsession.
+  incrementActiveTicks();
+  let classic = TelemetrySession.getPayload();
+  let subsession = TelemetrySession.getPayload("environment-change");
+  Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks,
+               "Classic pings must count active ticks since the beginning of the session.");
+  Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks,
+               "Subsessions must count active ticks as classic pings on the first subsession.");
+
+  // Start a new subsession and check that the active ticks are correctly reported.
+  incrementActiveTicks();
+  activeTicksAtSubsessionStart = sessionRecorder.activeTicks;
+  classic = TelemetrySession.getPayload();
+  subsession = TelemetrySession.getPayload("environment-change", true);
+  Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks,
+               "Classic pings must count active ticks since the beginning of the session.");
+  Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks,
+               "Pings must not loose the tick count when starting a new subsession.");
+
+  // Get a new subsession payload without clearing the subsession.
+  incrementActiveTicks();
+  classic = TelemetrySession.getPayload();
+  subsession = TelemetrySession.getPayload("environment-change");
+  Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks,
+               "Classic pings must count active ticks since the beginning of the session.");
+  Assert.equal(subsession.simpleMeasurements.activeTicks,
+               expectedActiveTicks - activeTicksAtSubsessionStart,
+               "Subsessions must count active ticks since the last new subsession.");
+});
+
 add_task(function* test_dailyCollection() {
   if (gIsAndroid) {
     // We don't do daily collections yet on Android.
     return;
   }
 
   let now = new Date(2030, 1, 1, 12, 0, 0);
   let nowDay = new Date(2030, 1, 1, 0, 0, 0);
--- a/toolkit/devtools/server/actors/common.js
+++ b/toolkit/devtools/server/actors/common.js
@@ -379,21 +379,22 @@ exports.OriginalLocation = OriginalLocat
  *
  * @param SourceActor actor
  *        A SourceActor representing a generated source.
  * @param Number line
  *        A line within the given source.
  * @param Number column
  *        A column within the given line.
  */
-function GeneratedLocation(actor, line, column) {
+function GeneratedLocation(actor, line, column, lastColumn) {
   this._connection = actor ? actor.conn : null;
   this._actorID = actor ? actor.actorID : undefined;
   this._line = line;
   this._column = column;
+  this._lastColumn = (lastColumn !== undefined) ? lastColumn : column + 1;
 }
 
 GeneratedLocation.fromOriginalLocation = function (originalLocation) {
   return new GeneratedLocation(
     originalLocation.originalSourceActor,
     originalLocation.originalLine,
     originalLocation.originalColumn
   );
@@ -425,16 +426,34 @@ GeneratedLocation.prototype = {
   },
 
   get generatedLine() {
     return this._line;
   },
 
   get generatedColumn() {
     return this._column;
+  },
+
+  get generatedLastColumn() {
+    return this._lastColumn;
+  },
+
+  equals: function (other) {
+    return this.generatedSourceActor.url == other.generatedSourceActor.url &&
+           this.generatedLine === other.originalLine;
+  },
+
+  toJSON: function () {
+    return {
+      source: this.generatedSourceActor.form(),
+      line: this.generatedLine,
+      column: this.generatedColumn,
+      lastColumn: this.generatedLastColumn
+    };
   }
 };
 
 exports.GeneratedLocation = GeneratedLocation;
 
 // TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when it is
 // implemented.
 exports.getOffsetColumn = function getOffsetColumn(aOffset, aScript) {
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2816,17 +2816,17 @@ SourceActor.prototype = {
    *
    * @param BreakpointActor actor
    *        The BreakpointActor to be set as a breakpoint handler.
    *
    * @returns A Promise that resolves to the given BreakpointActor.
    */
   _setBreakpoint: function (actor) {
     let { originalLocation } = actor;
-    let { originalColumn } = originalLocation;
+    let { originalSourceActor, originalLine, originalColumn } = originalLocation;
 
     return this._setBreakpointAtOriginalLocation(actor, originalLocation)
                .then((actualLocation) => {
       if (actualLocation) {
         return actualLocation;
       }
 
       if (!this.isSourceMapped) {
@@ -2862,32 +2862,26 @@ SourceActor.prototype = {
                   entryPoints = [];
                   lineToEntryPointsMap[line] = entryPoints;
                 }
                 entryPoints.push({ script, offsets });
               }
             }
           }
 
-          let {
-            originalSourceActor,
-            originalLine,
-            originalColumn
-          } = originalLocation;
-
           // Now that we have a map from line numbers to a list of entry points
           // for each line, we can use it to perform breakpoint sliding. Start
           // at the original line of the breakpoint actor, and keep incrementing
           // it by one, until either we find a line that has at least one entry
           // point, or we go past the last line in the map.
           //
           // Note that by computing the entire map up front, and implementing it
           // as a sparse array, we can easily tell when we went past the last line
           // in the map.
-          let actualLine = originalLine;
+          let actualLine = originalLine + 1;
           while (actualLine < lineToEntryPointsMap.length) {
             let entryPoints = lineToEntryPointsMap[actualLine];
             if (entryPoints) {
               setBreakpointAtEntryPoints(actor, entryPoints);
               break;
             }
             ++actualLine;
           }
@@ -2903,25 +2897,51 @@ SourceActor.prototype = {
             originalSourceActor,
             actualLine
           );
         } else {
           // TODO: Implement breakpoint sliding for column breakpoints
           return originalLocation;
         }
       } else {
-        // TODO: Refactor breakpoint sliding for source mapped sources.
-        return this.threadActor.sources.getGeneratedLocation(originalLocation)
-                                       .then((generatedLocation) => {
-          return generatedLocation.generatedSourceActor
-                                  ._setBreakpointAtLocationWithSliding(
-            actor,
-            generatedLocation
-          );
-        });
+        if (originalColumn === undefined) {
+          let loop = (actualLocation) => {
+            let {
+              originalLine: actualLine,
+              originalColumn: actualColumn
+            } = actualLocation;
+
+            return this.threadActor.sources.getAllGeneratedLocations(actualLocation)
+                                           .then((generatedLocations) => {
+              // Because getAllGeneratedLocations will always return the list of
+              // generated locations for the closest line that is greater than
+              // the one we are searching for if no exact match can be found, if
+              // the list of generated locations is empty, we've reached the end
+              // of the original source, and breakpoint sliding failed.
+              if (generatedLocations.length === 0) {
+                return originalLocation;
+              }
+
+              // If at least one script has an offset that matches one of the