Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 16 Jul 2014 15:58:57 +0200
changeset 216309 f6b084788adbcb9b0257996a093beb3c2a12f794
parent 216308 560bea8126e748de73a8fc3ee52e53db8411a64d (current diff)
parent 216260 f6e46d1fc9039269f2d59ae4f43d3dc1c0b1e3de (diff)
child 216310 4bd4e61980de590868061696e5f6a31b47cb973c
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound
b2g/app/b2g.js
browser/devtools/webide/content/cli.js
browser/devtools/webide/test/test_cli.html
modules/libpref/src/init/all.js
toolkit/components/telemetry/Histograms.json
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,18 +14,18 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="0910be495385d90acdeaddbeaf1fba315aff57b0"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5eb9b152764b4a4cf00af94ec02a808cdbb90a20"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d29773d2a011825fd77d1c0915a96eb0911417b6"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="7f792d756385bb894fba7645da59c67fe2c804bf"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="bf9aaf39dd5a6491925a022db167c460f8207d34"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="777194d772c831b5dab40cf919523d5665f2a46c"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,18 +12,18 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="0910be495385d90acdeaddbeaf1fba315aff57b0"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5eb9b152764b4a4cf00af94ec02a808cdbb90a20"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="d29773d2a011825fd77d1c0915a96eb0911417b6"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="7f792d756385bb894fba7645da59c67fe2c804bf"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="777194d772c831b5dab40cf919523d5665f2a46c"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,19 +10,19 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="276ce45e78b09c4a4ee643646f691d22804754c1">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="0910be495385d90acdeaddbeaf1fba315aff57b0"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="d29773d2a011825fd77d1c0915a96eb0911417b6"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5eb9b152764b4a4cf00af94ec02a808cdbb90a20"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="7f792d756385bb894fba7645da59c67fe2c804bf"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="777194d772c831b5dab40cf919523d5665f2a46c"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,18 +14,18 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="0910be495385d90acdeaddbeaf1fba315aff57b0"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5eb9b152764b4a4cf00af94ec02a808cdbb90a20"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d29773d2a011825fd77d1c0915a96eb0911417b6"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="7f792d756385bb894fba7645da59c67fe2c804bf"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="bf9aaf39dd5a6491925a022db167c460f8207d34"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="777194d772c831b5dab40cf919523d5665f2a46c"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,18 +12,18 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="0910be495385d90acdeaddbeaf1fba315aff57b0"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5eb9b152764b4a4cf00af94ec02a808cdbb90a20"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="d29773d2a011825fd77d1c0915a96eb0911417b6"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="7f792d756385bb894fba7645da59c67fe2c804bf"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="777194d772c831b5dab40cf919523d5665f2a46c"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "d329fe1f9c94cc3e17bde84a0d7ea3b3ed5242fb", 
+    "revision": "8cd6c73ef83257a569d148e246108b2c161127bb", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,18 +12,18 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="0910be495385d90acdeaddbeaf1fba315aff57b0"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5eb9b152764b4a4cf00af94ec02a808cdbb90a20"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d29773d2a011825fd77d1c0915a96eb0911417b6"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="7f792d756385bb894fba7645da59c67fe2c804bf"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="777194d772c831b5dab40cf919523d5665f2a46c"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="746bc48f34f5060f90801925dcdd964030c1ab6d"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,18 +10,18 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="0910be495385d90acdeaddbeaf1fba315aff57b0"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5eb9b152764b4a4cf00af94ec02a808cdbb90a20"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d29773d2a011825fd77d1c0915a96eb0911417b6"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="7f792d756385bb894fba7645da59c67fe2c804bf"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="575fdbf046e966a5915b1f1e800e5d6ad0ea14c0"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,18 +12,18 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="0910be495385d90acdeaddbeaf1fba315aff57b0"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5eb9b152764b4a4cf00af94ec02a808cdbb90a20"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="d29773d2a011825fd77d1c0915a96eb0911417b6"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="7f792d756385bb894fba7645da59c67fe2c804bf"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="777194d772c831b5dab40cf919523d5665f2a46c"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,18 +12,18 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="0910be495385d90acdeaddbeaf1fba315aff57b0"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5eb9b152764b4a4cf00af94ec02a808cdbb90a20"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="d29773d2a011825fd77d1c0915a96eb0911417b6"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="7f792d756385bb894fba7645da59c67fe2c804bf"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="dc5ca96695cab87b4c2fcd7c9f046ae3415a70a5"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="777194d772c831b5dab40cf919523d5665f2a46c"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -290,16 +290,17 @@ input[type=button] {
   padding: 0 8px;
   background: hsla(0,0%,100%,.9) padding-box;
   border: 1px solid;
   border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
   box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
               0 0 2px hsla(210,65%,9%,.1) inset,
               0 1px 0 hsla(0,0%,100%,.2);
   border-radius: 2.5px 0 0 2.5px;
+  color: inherit;
 }
 
 #newtab-search-text:-moz-dir(rtl) {
   border-radius: 0 2.5px 2.5px 0;
 }
 
 #newtab-search-text:focus,
 #newtab-search-text[autofocus] {
@@ -313,16 +314,17 @@ input[type=button] {
   background: linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
   padding: 0 9px;
   border: 1px solid;
   border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
   -moz-border-start: 1px solid transparent;
   border-radius: 0 2.5px 2.5px 0;
   box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset,
               0 1px 0 hsla(0,0%,100%,.2);
+  color: inherit;
   cursor: pointer;
   transition-property: background-color, border-color, box-shadow;
   transition-duration: 150ms;
 }
 
 #newtab-search-submit:-moz-dir(rtl) {
   border-radius: 2.5px 0 0 2.5px;
 }
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -855,21 +855,17 @@ StackFrames.prototype = {
       return promise.reject(new Error("No stack frame available."));
     }
 
     let deferred = promise.defer();
 
     this.activeThread.addOneTimeListener("paused", (aEvent, aPacket) => {
       let { type, frameFinished } = aPacket.why;
       if (type == "clientEvaluated") {
-        if (!("terminated" in frameFinished)) {
-          deferred.resolve(frameFinished);
-        } else {
-          deferred.reject(new Error("The execution was abruptly terminated."));
-        }
+        deferred.resolve(frameFinished);
       } else {
         deferred.reject(new Error("Active thread paused unexpectedly."));
       }
     });
 
     let meta = "meta" in aOptions ? aOptions.meta : FRAME_TYPE.PUBLIC_CLIENT_EVAL;
     this._currentFrameDescription = meta;
     this.activeThread.eval(frame.actor, aExpression);
@@ -970,20 +966,21 @@ StackFrames.prototype = {
     }
 
     // Evaluation causes the stack frames to be cleared and active thread to
     // pause, sending a 'clientEvaluated' packet and adding the frames again.
     let evaluationOptions = { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL };
     yield this.evaluate(watchExpressions, evaluationOptions);
     this._currentFrameDescription = FRAME_TYPE.NORMAL;
 
-    // If an error was thrown during the evaluation of the watch expressions,
-    // then at least one expression evaluation could not be performed. So
-    // remove the most recent watch expression and try again.
-    if (this._currentEvaluation.throw) {
+    // If an error was thrown during the evaluation of the watch expressions
+    // or the evaluation was terminated from the slow script dialog, then at
+    // least one expression evaluation could not be performed. So remove the
+    // most recent watch expression and try again.
+    if (this._currentEvaluation.throw || this._currentEvaluation.terminated) {
       DebuggerView.WatchExpressions.removeAt(0);
       yield DebuggerController.StackFrames.syncWatchExpressions();
     }
   }),
 
   /**
    * Adds the watch expressions evaluation results to a scope in the view.
    *
@@ -1233,19 +1230,19 @@ SourceScripts.prototype = {
 
   /**
    * Handler for the debugger client's 'blackboxchange' notification.
    */
   _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
     const item = DebuggerView.Sources.getItemByValue(url);
     if (item) {
       if (isBlackBoxed) {
-        item.target.classList.add("black-boxed");
+        item.prebuiltNode.classList.add("black-boxed");
       } else {
-        item.target.classList.remove("black-boxed");
+        item.prebuiltNode.classList.remove("black-boxed");
       }
     }
     DebuggerView.Sources.updateToolbarButtonsState();
     DebuggerView.maybeShowBlackBoxMessage();
   },
 
   /**
    * Set the black boxed status of the given source.
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -141,32 +141,32 @@ SourcesView.prototype = Heritage.extend(
 
     let contents = document.createElement("label");
     contents.className = "plain dbg-source-item";
     contents.setAttribute("value", label);
     contents.setAttribute("crop", "start");
     contents.setAttribute("flex", "1");
     contents.setAttribute("tooltiptext", unicodeUrl);
 
+    // If the source is blackboxed, apply the appropriate style.
+    if (gThreadClient.source(aSource).isBlackBoxed) {
+      contents.classList.add("black-boxed");
+    }
+
     // Append a source item to this container.
-    const item = this.push([contents, fullUrl], {
+    this.push([contents, fullUrl], {
       staged: aOptions.staged, /* stage the item to be appended later? */
       attachment: {
         label: label,
         group: group,
         checkboxState: !aSource.isBlackBoxed,
         checkboxTooltip: this._blackBoxCheckboxTooltip,
         source: aSource
       }
     });
-
-    // If source is blackboxed, apply appropriate style
-    if (gThreadClient.source(aSource).isBlackBoxed) {
-      item.target.classList.add("black-boxed");
-    }
   },
 
   /**
    * Adds a breakpoint to this sources container.
    *
    * @param object aBreakpointData
    *        Information about the breakpoint to be shown.
    *        This object must have the following properties:
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -747,18 +747,21 @@ function resumeDebuggerThenCloseAndFinis
 }
 
 // Blackboxing helpers
 
 function getBlackBoxButton(aPanel) {
   return aPanel.panelWin.document.getElementById("black-box");
 }
 
+/**
+ * Returns the node that has the black-boxed class applied to it.
+ */
 function getSelectedSourceElement(aPanel) {
-    return gPanel.panelWin.DebuggerView.Sources.selectedItem.target;
+    return aPanel.panelWin.DebuggerView.Sources.selectedItem.prebuiltNode;
 }
 
 function toggleBlackBoxing(aPanel, aSource = null) {
   function clickBlackBoxButton() {
     getBlackBoxButton(aPanel).click();
   }
 
   const blackBoxChanged = waitForThreadEvents(aPanel, "blackboxchange");
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -44,19 +44,19 @@ SimpleTest.registerCleanupFunction(() =>
   console.error("Here we are\n");
   let {DebuggerServer} = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
   console.error("DebuggerServer open connections: " + Object.getOwnPropertyNames(DebuggerServer._connections).length);
 
   Services.prefs.clearUserPref("devtools.dump.emit");
   Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
 });
 
-registerCleanupFunction(() => {
+registerCleanupFunction(function*() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
-  gDevTools.closeToolbox(target);
+  yield gDevTools.closeToolbox(target);
 
   // Move the mouse outside inspector. If the test happened fake a mouse event
   // somewhere over inspector the pointer is considered to be there when the
   // next test begins. This might cause unexpected events to be emitted when
   // another test moves the mouse.
   EventUtils.synthesizeMouseAtPoint(1, 1, {type: "mousemove"}, window);
 
   while (gBrowser.tabs.length > 1) {
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -499,16 +499,17 @@ function Item(aOwnerView, aElement, aVal
   this.attachment = aAttachment;
   this._value = aValue + "";
   this._prebuiltNode = aElement;
 };
 
 Item.prototype = {
   get value() { return this._value; },
   get target() { return this._target; },
+  get prebuiltNode() { return this._prebuiltNode; },
 
   /**
    * Immediately appends a child item to this item.
    *
    * @param nsIDOMNode aElement
    *        An nsIDOMNode representing the child element to append.
    * @param object aOptions [optional]
    *        Additional options or flags supported by this operation:
--- a/browser/devtools/webide/components/webideCli.js
+++ b/browser/devtools/webide/components/webideCli.js
@@ -6,66 +6,44 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
 
 /**
  * Handles -webide command line option.
- *
- * See webide/content/cli.js for a complete description of the command line.
  */
 
 function webideCli() { }
 
 webideCli.prototype = {
   handle: function(cmdLine) {
     let param;
 
-    try {
-      // Returns null if -webide is not present
-      // Throws if -webide is present with no params
-      param = cmdLine.handleFlagWithParam("webide", false);
-      if (!param) {
-        return;
-      }
-    } catch(e) {
-      // -webide is present with no params
-      cmdLine.handleFlag("webide", false);
+    if (!cmdLine.handleFlag("webide", false)) {
+      return;
     }
 
     // If -webide is used remotely, we don't want to open
     // a new tab.
     //
     // If -webide is used for a new Firefox instance, we
     // want to open webide only.
     cmdLine.preventDefault = true;
 
     let win = Services.wm.getMostRecentWindow("devtools:webide");
     if (win) {
       win.focus();
-      if (param) {
-        win.handleCommandline(param);
-      }
-      return;
-    }
-
-    win = Services.ww.openWindow(null,
-                                 "chrome://webide/content/",
-                                 "webide",
-                                 "chrome,centerscreen,resizable,dialog=no",
-                                 null);
-
-    if (param) {
-      win.addEventListener("load", function onLoad() {
-        win.removeEventListener("load", onLoad, true);
-        // next tick
-        win.setTimeout(() => win.handleCommandline(param), 0);
-      }, true);
+    } else {
+      win = Services.ww.openWindow(null,
+                                   "chrome://webide/content/",
+                                   "webide",
+                                   "chrome,centerscreen,resizable,dialog=no",
+                                   null);
     }
 
     if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
       // If this is a new Firefox instance, and because we will only start
       // webide, we need to notify "sessionstore-windows-restored" to trigger
       // addons registration (for simulators and adb helper).
       Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
     }
deleted file mode 100644
--- a/browser/devtools/webide/content/cli.js
+++ /dev/null
@@ -1,198 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/**
- *
- *  SYNOPSIS
- *    firefox --webide [OPTIONS]
- *
- *  DESCRIPTION
- *    Starts WebIDE (aka App Manager). If Firefox has already started, opens
- *    the WebIDE window. If Firefox is not running, no browser window will
- *    be open.
- *
- *    It's recommended to add the option `-jsconsole` to see potential errors
- *    occurring while processing the parameters.
- *
- *  OPTIONS
- *    A set of "key=value" pairs, separated by '&'.
- *
- *    actions=action1,action2,...,actionN
- *      Executed in order. actionN will be executed only if actionN-1 doesn't fail.
- *      Available actions:
- *        addPackagedApp:      Import and select app ('location' parameter must be a directory)
- *        addHostedApp:        Import and select app ('location' parameter must be a URL)
- *        connectToRuntime:    Connect to runtime (require 'runtimeType')
- *        play:                Start or reload selected app on connected runtime
- *        debug:               Debug selected app or debug 'appID'
- *
- *    location
- *      packaged app directory or hosted app manifest URL
- *
- *    runtimeType
- *      Type of runtime to connect to. "usb" or "simulator"
- *
- *    runtimeID
- *      Which runtime to use. By default, the most recent USB device or most recent simulator
- *
- *    appID
- *      App on runtime
- *
- *  EXAMPLES
- *
- *    $ firefox --webide "actions=addPackagedApp,connectToRuntime,play,debug&location=/home/bob/Downloads/foobar/&runtimeType=usb"
- *        Select app located in /home/bob/Downloads/foobar, then
- *        Connect to USB device, then
- *        Install app, then
- *        Start developer tools connected to the running app
- *
- */
-
-window.handleCommandline = function(cmdline) {
-  console.log("External query", cmdline);
-  let params = new Map();
-  for (let token of cmdline.split("&")) {
-    token = token.split("=");
-    params.set(token[0], token[1]);
-  }
-  if (params.has("actions")) {
-    return UI.busyUntil(Task.spawn(function* () {
-      let actions = params.get("actions").split(",");
-      for (let action of actions) {
-        if (action in CliActions) {
-          console.log("External query - running action", action);
-          yield CliActions[action].call(window, params);
-        } else {
-          console.log("External query - unknown action", action);
-        }
-      }
-    }), "Computing command line");
-  } else {
-    return promise.reject("No actions provided");
-  }
-}
-
-let CliActions = {
-  addPackagedApp: function(params) {
-    return Task.spawn(function* () {
-      let location = params.get("location");
-      if (!location) {
-        throw new Error("No location parameter");
-      }
-
-      yield AppProjects.load();
-
-      // Normalize location
-      let directory = new FileUtils.File(location);
-      if (AppProjects.get(directory.path)) {
-        // Already imported
-        return;
-      }
-
-      yield Cmds.importPackagedApp(location);
-    })
-  },
-  addHostedApp: function(params) {
-    return Task.spawn(function* () {
-      let location = params.get("location");
-      if (!location) {
-        throw new Error("No location parameter");
-      }
-      yield AppProjects.load();
-      if (AppProjects.get(location)) {
-        // Already imported
-        return;
-      }
-      yield Cmds.importHostedApp(location);
-    })
-  },
-  debug: function(params) {
-    return Task.spawn(function* () {
-
-      let appID = params.get("appID");
-
-      if (appID) {
-        let appToSelect;
-        for (let i = 0; i < AppManager.webAppsStore.object.all.length; i++) {
-          let app = AppManager.webAppsStore.object.all[i];
-          if (app.manifestURL == appID) {
-            appToSelect = app;
-            break;
-          }
-        }
-        if (!appToSelect) {
-          throw new Error("App not found on device");
-        }
-        AppManager.selectedProject = {
-          type: "runtimeApp",
-          app: appToSelect,
-          icon: appToSelect.iconURL,
-          name: appToSelect.name
-        };
-      }
-
-      UI.closeToolbox();
-
-      yield Cmds.toggleToolbox();
-    });
-  },
-  connectToRuntime: function(params) {
-    return Task.spawn(function* () {
-
-      let type = params.get("runtimeType");
-      if (type != "usb" && type != "simulator") {
-        return promise.reject("Unkown runtime type");
-      }
-
-      yield Cmds.disconnectRuntime();
-
-      if (AppManager.runtimeList[type].length == 0) {
-        let deferred = promise.defer();
-        function onRuntimeListUpdate(event, what) {
-          if (AppManager.runtimeList[type].length > 0) {
-            deferred.resolve();
-          }
-        }
-
-        let timeout = setTimeout(deferred.resolve, 3000);
-        AppManager.on("app-manager-update", onRuntimeListUpdate);
-        yield deferred.promise;
-
-        AppManager.off("app-manager-update", onRuntimeListUpdate);
-        clearTimeout(timeout);
-      }
-
-      let runtime;
-      let runtimeID = params.get("runtimeID");
-
-      if (runtimeID) {
-        for (let r of AppManager.runtimeList[type]) {
-          if (r.getID() == runtimeID) {
-            runtime = r;
-            break;
-          }
-        }
-      } else {
-        let list = AppManager.runtimeList[type];
-        runtime = list[list.length - 1];
-      }
-
-      if (!runtime) {
-        return promise.reject("Can't find any runtime to connect to");
-      }
-
-      let deferred = promise.defer();
-      // store-ready is fired when the list of installed apps has been
-      // received by the webAppsStore.
-      AppManager.webAppsStore.once("store-ready", deferred.resolve);
-      UI.connectToRuntime(runtime).then(null, deferred.reject);
-      return deferred.promise;
-    })
-  },
-  play: function(params) {
-    return Task.spawn(function* () {
-      yield Cmds.play();
-    })
-  },
-}
--- a/browser/devtools/webide/content/jar.mn
+++ b/browser/devtools/webide/content/jar.mn
@@ -5,21 +5,22 @@
 webide.jar:
 %   content webide %content/
     content/webide.xul                (webide.xul)
     content/webide.js                 (webide.js)
     content/newapp.xul                (newapp.xul)
     content/newapp.js                 (newapp.js)
     content/details.xhtml             (details.xhtml)
     content/details.js                (details.js)
-    content/cli.js                    (cli.js)
     content/addons.js                 (addons.js)
     content/addons.xhtml              (addons.xhtml)
     content/permissionstable.js       (permissionstable.js)
     content/permissionstable.xhtml    (permissionstable.xhtml)
     content/runtimedetails.js         (runtimedetails.js)
     content/runtimedetails.xhtml      (runtimedetails.xhtml)
+    content/prefs.js                  (prefs.js)
+    content/prefs.xhtml               (prefs.xhtml)
 
 # Temporarily include locales in content, until we're ready
 # to localize webide
 
     content/webide.dtd                (../locales/en-US/webide.dtd)
     content/webide.properties         (../locales/en-US/webide.properties)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/content/prefs.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cu = Components.utils;
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+
+window.addEventListener("load", function onLoad() {
+  window.removeEventListener("load", onLoad);
+
+  // Listen to preference changes
+  let inputs = document.querySelectorAll("[data-pref]");
+  for (let i of inputs) {
+    let pref = i.dataset.pref;
+    Services.prefs.addObserver(pref, FillForm, false);
+    i.addEventListener("change", SaveForm, false);
+  }
+
+  // Buttons
+  document.querySelector("#close").onclick = CloseUI;
+  document.querySelector("#restoreButton").onclick = RestoreDefaults;
+  document.querySelector("#manageSimulators").onclick = ShowAddons;
+
+  // Initialize the controls
+  FillForm();
+
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+  window.removeEventListener("unload", onUnload);
+  let inputs = document.querySelectorAll("[data-pref]");
+  for (let i of inputs) {
+    let pref = i.dataset.pref;
+    i.removeEventListener("change", SaveForm, false);
+    Services.prefs.removeObserver(pref, FillForm, false);
+  }
+}, true);
+
+function CloseUI() {
+  window.parent.UI.openProject();
+}
+
+function ShowAddons() {
+  window.parent.Cmds.showAddons();
+}
+
+function FillForm() {
+  let inputs = document.querySelectorAll("[data-pref]");
+  for (let i of inputs) {
+    let pref = i.dataset.pref;
+    let val = GetPref(pref);
+    if (i.type == "checkbox") {
+      i.checked = val;
+    } else {
+      i.value = val;
+    }
+  }
+}
+
+function SaveForm(e) {
+  let inputs = document.querySelectorAll("[data-pref]");
+  for (let i of inputs) {
+    let pref = i.dataset.pref;
+    if (i.type == "checkbox") {
+      SetPref(pref, i.checked);
+    } else {
+      SetPref(pref, i.value);
+    }
+  }
+}
+
+function GetPref(name) {
+  let type = Services.prefs.getPrefType(name);
+  switch (type) {
+    case Services.prefs.PREF_STRING:
+      return Services.prefs.getCharPref(name);
+    case Services.prefs.PREF_INT:
+      return Services.prefs.getIntPref(name);
+    case Services.prefs.PREF_BOOL:
+      return Services.prefs.getBoolPref(name);
+    default:
+      throw new Error("Unknown type");
+  }
+}
+
+function SetPref(name, value) {
+  let type = Services.prefs.getPrefType(name);
+  switch (type) {
+    case Services.prefs.PREF_STRING:
+      return Services.prefs.setCharPref(name, value);
+    case Services.prefs.PREF_INT:
+      return Services.prefs.setIntPref(name, value);
+    case Services.prefs.PREF_BOOL:
+      return Services.prefs.setBoolPref(name, value);
+    default:
+      throw new Error("Unknown type");
+  }
+}
+
+function RestoreDefaults() {
+  let inputs = document.querySelectorAll("[data-pref]");
+  for (let i of inputs) {
+    let pref = i.dataset.pref;
+    Services.prefs.clearUserPref(pref);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/content/prefs.xhtml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html [
+  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta charset="utf8"/>
+    <link rel="stylesheet" href="chrome://webide/skin/prefs.css" type="text/css"/>
+    <script type="application/javascript;version=1.8" src="chrome://webide/content/prefs.js"></script>
+  </head>
+  <body>
+
+    <div id="controls">
+      <a id="close">&deck_close;</a>
+    </div>
+
+    <h1>&prefs_title;</h1>
+
+    <h2>&prefs_general_title;</h2>
+
+    <ul>
+      <li>
+        <label title="&prefs_options_enablelocalruntime_tooltip;">
+          <input type="checkbox" data-pref="devtools.webide.enableLocalRuntime"/>
+          <span>&prefs_options_enablelocalruntime;</span>
+        </label>
+      </li>
+      <li>
+        <label title="&prefs_options_rememberlastproject_tooltip;">
+          <input type="checkbox" data-pref="devtools.webide.restoreLastProject"/>
+          <span>&prefs_options_rememberlastproject;</span>
+        </label>
+      </li>
+      <li>
+        <label title="&prefs_options_templatesurl_tooltip;">
+          <span>&prefs_options_templatesurl;</span>
+          <input data-pref="devtools.webide.templatesURL"/>
+        </label>
+      </li>
+    </ul>
+
+    <h2>&prefs_editor_title;</h2>
+
+    <ul>
+      <li>
+        <label title="&prefs_options_showeditor_tooltip;">
+          <input type="checkbox" data-pref="devtools.webide.showProjectEditor"/>
+          <span>&prefs_options_showeditor;</span>
+        </label>
+      </li>
+      <li>
+        <label title="&prefs_options_autoclosebrackets_tooltip;">
+          <input type="checkbox" data-pref="devtools.editor.autoclosebrackets"/>
+          <span>&prefs_options_autoclosebrackets;</span>
+        </label>
+      </li>
+      <li>
+        <label title="&prefs_options_autocomplete_tooltip;">
+          <input type="checkbox" data-pref="devtools.editor.autocomplete"/>
+          <span>&prefs_options_autocomplete;</span>
+        </label>
+      </li>
+      <li>
+        <label title="&prefs_options_detectindentation_tooltip;">
+          <input type="checkbox" data-pref="devtools.editor.detectindentation"/>
+          <span>&prefs_options_detectindentation;</span>
+        </label>
+      </li>
+      <li>
+        <label title="&prefs_options_expandtab_tooltip;">
+          <input type="checkbox" data-pref="devtools.editor.expandtab"/>
+          <span>&prefs_options_expandtab;</span>
+        </label>
+      </li>
+      <li>
+        <label><span>&prefs_options_tabsize;</span>
+          <select data-pref="devtools.editor.tabsize">
+            <option value="2">2</option>
+            <option value="4">4</option>
+            <option value="8">8</option>
+          </select>
+        </label>
+      </li>
+    </ul>
+
+    <button id="manageSimulators">&prefs_simulators;</button>
+    <button id="restoreButton">&prefs_restore;</button>
+
+  </body>
+</html>
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -69,21 +69,24 @@ let UI = {
     // If the user decides to uninstall the addon, we won't install it again.
     let autoInstallADBHelper = Services.prefs.getBoolPref("devtools.webide.autoinstallADBHelper");
     if (autoInstallADBHelper && !Devices.helperAddonInstalled) {
       GetAvailableAddons().then(addons => {
         addons.adb.install();
       }, console.error);
     }
     Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
+
+    this.setupDeck();
   },
 
   openLastProject: function() {
     let lastProjectLocation = Services.prefs.getCharPref("devtools.webide.lastprojectlocation");
-    if (lastProjectLocation) {
+    let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject");
+    if (lastProjectLocation && shouldRestore) {
       let lastProject = AppProjects.get(lastProjectLocation);
       if (lastProject) {
         AppManager.selectedProject = lastProject;
       } else {
         AppManager.selectedProject = null;
       }
     } else {
       AppManager.selectedProject = null;
@@ -464,16 +467,23 @@ let UI = {
 
     this.getProjectEditor().then(() => {
       this.updateProjectEditorHeader();
     }, console.error);
   },
 
   /********** DECK **********/
 
+  setupDeck: function() {
+    let iframes = document.querySelectorAll("#deck > iframe");
+    for (let iframe of iframes) {
+      iframe.tooltip = "aHTMLTooltip";
+    }
+  },
+
   resetFocus: function() {
     document.commandDispatcher.focusedElement = document.documentElement;
   },
 
   selectDeckPanel: function(id) {
     this.hidePanels();
     this.resetFocus();
     let deck = document.querySelector("#deck");
@@ -568,24 +578,22 @@ let UI = {
     let box = document.querySelector("#runtime-actions");
 
     let runtimePanelButton = document.querySelector("#runtime-panel-button");
     if (AppManager.connection.status == Connection.Status.CONNECTED) {
       screenshotCmd.removeAttribute("disabled");
       permissionsCmd.removeAttribute("disabled");
       disconnectCmd.removeAttribute("disabled");
       detailsCmd.removeAttribute("disabled");
-      box.removeAttribute("hidden");
       runtimePanelButton.setAttribute("active", "true");
     } else {
       screenshotCmd.setAttribute("disabled", "true");
       permissionsCmd.setAttribute("disabled", "true");
       disconnectCmd.setAttribute("disabled", "true");
       detailsCmd.setAttribute("disabled", "true");
-      box.setAttribute("hidden", "true");
       runtimePanelButton.removeAttribute("active");
     }
 
   },
 
   /********** TOOLBOX **********/
 
   onMessage: function(event) {
@@ -905,9 +913,13 @@ let Cmds = {
 
   showTroubleShooting: function() {
     UI.openInBrowser(HELP_URL);
   },
 
   showAddons: function() {
     UI.selectDeckPanel("addons");
   },
+
+  showPrefs: function() {
+    UI.selectDeckPanel("prefs");
+  },
 }
--- a/browser/devtools/webide/content/webide.xul
+++ b/browser/devtools/webide/content/webide.xul
@@ -22,17 +22,16 @@
         macanimationtype="document"
         fullscreenbutton="true"
         screenX="4" screenY="4"
         width="640" height="480"
         persist="screenX screenY width height">
 
   <script type="application/javascript" src="chrome://global/content/globalOverlay.js"></script>
   <script type="application/javascript" src="webide.js"></script>
-  <script type="application/javascript" src="cli.js"></script>
 
   <commandset id="mainCommandSet">
     <commandset id="editMenuCommands"/>
     <commandset id="webideCommands">
       <command id="cmd_quit" oncommand="Cmds.quit()"/>
       <command id="cmd_newApp" oncommand="Cmds.newApp()" label="&projectMenu_newApp_label;"/>
       <command id="cmd_importPackagedApp" oncommand="Cmds.importPackagedApp()" label="&projectMenu_importPackagedApp_label;"/>
       <command id="cmd_importHostedApp" oncommand="Cmds.importHostedApp()" label="&projectMenu_importHostedApp_label;"/>
@@ -40,16 +39,17 @@
       <command id="cmd_showProjectPanel" oncommand="Cmds.showProjectPanel()"/>
       <command id="cmd_showRuntimePanel" oncommand="Cmds.showRuntimePanel()"/>
       <command id="cmd_disconnectRuntime" oncommand="Cmds.disconnectRuntime()" label="&runtimeMenu_disconnect_label;"/>
       <command id="cmd_showPermissionsTable" oncommand="Cmds.showPermissionsTable()" label="&runtimeMenu_showPermissionTable_label;"/>
       <command id="cmd_showRuntimeDetails" oncommand="Cmds.showRuntimeDetails()" label="&runtimeMenu_showDetails_label;"/>
       <command id="cmd_takeScreenshot" oncommand="Cmds.takeScreenshot()" label="&runtimeMenu_takeScreenshot_label;"/>
       <command id="cmd_toggleEditor" oncommand="Cmds.toggleEditors()" label="&viewMenu_toggleEditor_label;"/>
       <command id="cmd_showAddons" oncommand="Cmds.showAddons()"/>
+      <command id="cmd_showPrefs" oncommand="Cmds.showPrefs()"/>
       <command id="cmd_showTroubleShooting" oncommand="Cmds.showTroubleShooting()"/>
       <command id="cmd_play" oncommand="Cmds.play()"/>
       <command id="cmd_stop" oncommand="Cmds.stop()" label="&projectMenu_stop_label;"/>
       <command id="cmd_toggleToolbox" oncommand="Cmds.toggleToolbox()"/>
     </commandset>
   </commandset>
 
   <menubar id="main-menubar">
@@ -60,16 +60,18 @@
         <menuitem command="cmd_importHostedApp" accesskey="&projectMenu_importHostedApp_accesskey;"/>
         <menuitem command="cmd_showProjectPanel" key="key_showProjectPanel" label="&projectMenu_selectApp_label;" accesskey="&projectMenu_selectApp_accessley;"/>
         <menuseparator/>
         <menuitem command="cmd_play" key="key_play" label="&projectMenu_play_label;" accesskey="&projectMenu_play_accesskey;"/>
         <menuitem command="cmd_stop" accesskey="&projectMenu_stop_accesskey;"/>
         <menuitem command="cmd_toggleToolbox" key="key_toggleToolbox" label="&projectMenu_debug_label;" accesskey="&projectMenu_debug_accesskey;"/>
         <menuseparator/>
         <menuitem command="cmd_removeProject" accesskey="&projectMenu_remove_accesskey;"/>
+        <menuseparator/>
+        <menuitem command="cmd_showPrefs" label="&projectMenu_showPrefs_label;" accesskey="&projectMenu_showPrefs_accesskey;"/>
       </menupopup>
     </menu>
 
     <menu id="menu-runtime" label="&runtimeMenu_label;" accesskey="&runtimeMenu_accesskey;">
       <menupopup id="menu-runtime-popup">
         <menuitem command="cmd_takeScreenshot" accesskey="&runtimeMenu_takeScreenshot_accesskey;"/>
         <menuitem command="cmd_showPermissionsTable" accesskey="&runtimeMenu_showPermissionTable_accesskey;"/>
         <menuitem command="cmd_showRuntimeDetails" accesskey="&runtimeMenu_showDetails_accesskey;"/>
@@ -90,16 +92,18 @@
   <keyset id="mainKeyset">
     <key key="&key_quit;" id="key_quit" command="cmd_quit" modifiers="accel"/>
     <key key="&key_showProjectPanel;" id="key_showProjectPanel" command="cmd_showProjectPanel" modifiers="accel"/>
     <key key="&key_play;" id="key_play" command="cmd_play" modifiers="accel"/>
     <key key="&key_toggleEditor;" id="key_toggleEditor" command="cmd_toggleEditor" modifiers="accel"/>
     <key keycode="&key_toggleToolbox;" id="key_toggleToolbox" command="cmd_toggleToolbox"/>
   </keyset>
 
+  <tooltip id="aHTMLTooltip" page="true"/>
+
   <toolbar id="main-toolbar">
 
     <vbox flex="1">
       <hbox id="action-buttons-container" class="busy">
         <toolbarbutton id="action-button-play"  class="action-button" command="cmd_play" tooltiptext="&projectMenu_play_label;"/>
         <toolbarbutton id="action-button-stop"  class="action-button" command="cmd_stop" tooltiptext="&projectMenu_stop_label;"/>
         <toolbarbutton id="action-button-debug" class="action-button" command="cmd_toggleToolbox" tooltiptext="&projectMenu_debug_label;"/>
         <hbox id="action-busy" align="center">
@@ -139,41 +143,43 @@
         <vbox flex="1" id="project-panel-runtimeapps"/>
       </vbox>
     </panel>
 
     <!-- Runtime panel -->
     <panel id="runtime-panel" type="arrow" position="bottomcenter topright" consumeoutsideclicks="true" animate="false">
       <vbox flex="1">
         <label class="panel-header">&runtimePanel_USBDevices;</label>
-        <toolbarbutton class="panel-item-help" label="&runtimePanel_nousbdevice;" id="runtime-panel-nousbdevice" command="cmd_showTroubleShooting"/>
-        <toolbarbutton class="panel-item-help" label="&runtimePanel_noadbhelper;" id="runtime-panel-noadbhelper" command="cmd_showAddons"/>
+        <toolbarbutton class="panel-item" label="&runtimePanel_nousbdevice;" id="runtime-panel-nousbdevice" command="cmd_showTroubleShooting"/>
+        <toolbarbutton class="panel-item" label="&runtimePanel_noadbhelper;" id="runtime-panel-noadbhelper" command="cmd_showAddons"/>
         <vbox id="runtime-panel-usbruntime"></vbox>
         <label class="panel-header" id="runtime-header-wifi-devices">&runtimePanel_WiFiDevices;</label>
         <vbox id="runtime-panel-wifi-devices"></vbox>
         <label class="panel-header">&runtimePanel_simulators;</label>
-        <toolbarbutton class="panel-item-help" label="&runtimePanel_nosimulator;" id="runtime-panel-nosimulator" command="cmd_showAddons"/>
+        <toolbarbutton class="panel-item" label="&runtimePanel_nosimulator;" id="runtime-panel-nosimulator" command="cmd_showAddons"/>
         <vbox id="runtime-panel-simulators"></vbox>
         <label class="panel-header">&runtimePanel_custom;</label>
         <vbox id="runtime-panel-custom"></vbox>
-        <vbox flex="1" id="runtime-actions" hidden="true">
+        <vbox flex="1" id="runtime-actions">
           <toolbarbutton class="panel-item" id="runtime-details" command="cmd_showRuntimeDetails"/>
           <toolbarbutton class="panel-item" id="runtime-permissions" command="cmd_showPermissionsTable"/>
           <toolbarbutton class="panel-item" id="runtime-screenshot"  command="cmd_takeScreenshot"/>
+          <toolbarbutton class="panel-item" id="runtime-disconnect"  command="cmd_disconnectRuntime"/>
         </vbox>
       </vbox>
     </panel>
 
   </popupset>
 
   <notificationbox flex="1" id="notificationbox">
     <deck flex="1" id="deck" selectedIndex="-1">
       <iframe id="deck-panel-details" flex="1" src="details.xhtml"/>
       <iframe id="deck-panel-projecteditor" flex="1"/>
       <iframe id="deck-panel-addons" flex="1" src="addons.xhtml"/>
+      <iframe id="deck-panel-prefs" flex="1" src="prefs.xhtml"/>
       <iframe id="deck-panel-permissionstable" flex="1" src="permissionstable.xhtml"/>
       <iframe id="deck-panel-runtimedetails" flex="1" src="runtimedetails.xhtml"/>
     </deck>
   </notificationbox>
 
   <splitter hidden="true" class="devtools-horizontal-splitter" orient="vertical"/>
 
   <!-- toolbox iframe will be inserted here -->
--- a/browser/devtools/webide/locales/en-US/webide.dtd
+++ b/browser/devtools/webide/locales/en-US/webide.dtd
@@ -17,16 +17,18 @@
 <!ENTITY projectMenu_play_label "Install and run">
 <!ENTITY projectMenu_play_accesskey "I">
 <!ENTITY projectMenu_stop_label "Stop App">
 <!ENTITY projectMenu_stop_accesskey "S">
 <!ENTITY projectMenu_debug_label "Debug App">
 <!ENTITY projectMenu_debug_accesskey "D">
 <!ENTITY projectMenu_remove_label "Remove Project">
 <!ENTITY projectMenu_remove_accesskey "R">
+<!ENTITY projectMenu_showPrefs_label "Preferences">
+<!ENTITY projectMenu_showPrefs_accesskey "e">
 
 <!ENTITY runtimeMenu_label "Runtime">
 <!ENTITY runtimeMenu_accesskey "R">
 <!ENTITY runtimeMenu_disconnect_label "Disconnect">
 <!ENTITY runtimeMenu_disconnect_accesskey "D">
 <!ENTITY runtimeMenu_showPermissionTable_label "Permissions Table">
 <!ENTITY runtimeMenu_showPermissionTable_accesskey "P">
 <!ENTITY runtimeMenu_takeScreenshot_label "Screenshot">
@@ -85,14 +87,38 @@
 <!-- Decks -->
 
 <!ENTITY deck_close "close">
 
 <!-- Addons -->
 <!ENTITY addons_title "Extra Components:">
 <!ENTITY addons_aboutaddons "Open Addons Manager">
 
+<!-- Prefs -->
+<!ENTITY prefs_title "Preferences">
+<!ENTITY prefs_editor_title "Editor">
+<!ENTITY prefs_general_title "General">
+<!ENTITY prefs_restore "Restore defaults">
+<!ENTITY prefs_simulators "Manage simulators">
+<!ENTITY prefs_options_enablelocalruntime "Enable local runtime">
+<!ENTITY prefs_options_enablelocalruntime_tooltip "Allow WebIDE to connect to its own runtime (running browser instance)">
+<!ENTITY prefs_options_rememberlastproject "Remember last project">
+<!ENTITY prefs_options_rememberlastproject_tooltip "Restore previous project when WebIDE starts">
+<!ENTITY prefs_options_templatesurl "Templates URL">
+<!ENTITY prefs_options_templatesurl_tooltip "Index of available templates">
+<!ENTITY prefs_options_showeditor "Show editor">
+<!ENTITY prefs_options_showeditor_tooltip "Show internal editor">
+<!ENTITY prefs_options_detectindentation "Detect indentation">
+<!ENTITY prefs_options_detectindentation_tooltip "Guess indentation based on source content">
+<!ENTITY prefs_options_autoclosebrackets "Autoclose brackets">
+<!ENTITY prefs_options_autoclosebrackets_tooltip "Automatically insert closing brackets">
+<!ENTITY prefs_options_expandtab "Indent using spaces">
+<!ENTITY prefs_options_expandtab_tooltip "Use spaces instead of the tab character">
+<!ENTITY prefs_options_autocomplete "Autocompletion">
+<!ENTITY prefs_options_autocomplete_tooltip "Enable code autocompletion">
+<!ENTITY prefs_options_tabsize "Tab size">
+
 <!-- Permissions Table -->
 <!ENTITY permissionstable_title "Permissions Table">
 <!ENTITY permissionstable_name_header "Name">
 
 <!-- Runtime Details -->
 <!ENTITY runtimedetails_title "Runtime Info">
--- a/browser/devtools/webide/test/chrome.ini
+++ b/browser/devtools/webide/test/chrome.ini
@@ -23,12 +23,11 @@ support-files =
   head.js
   hosted_app.manifest
   templates.json
 
 [test_basic.html]
 [test_newapp.html]
 [test_import.html]
 [test_runtime.html]
-[test_cli.html]
 [test_manifestUpdate.html]
 [test_addons.html]
 [test_deviceinfo.html]
deleted file mode 100644
--- a/browser/devtools/webide/test/test_cli.html
+++ /dev/null
@@ -1,64 +0,0 @@
-<!DOCTYPE html>
-
-<html>
-
-  <head>
-    <meta charset="utf8">
-    <title></title>
-
-    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-    <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
-    <script type="application/javascript;version=1.8" src="head.js"></script>
-    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
-  </head>
-
-  <body>
-
-    <script type="application/javascript;version=1.8">
-      window.onload = function() {
-        SimpleTest.waitForExplicitFinish();
-
-        Task.spawn(function* () {
-          Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
-          DebuggerServer.init(function () { return true; });
-          DebuggerServer.addBrowserActors();
-
-          let win = yield openWebIDE();
-
-          let packagedAppLocation = getTestFilePath("app");
-
-          let cli = "actions=addPackagedApp&location=" + packagedAppLocation;
-          yield win.handleCommandline(cli);
-
-          let project = win.AppManager.selectedProject;
-          is(project.location, packagedAppLocation, "Project imported");
-
-          win.AppManager.runtimeList.usb.push({
-            connect: function(connection) {
-              ok(connection, win.AppManager.connection, "connection is valid");
-              connection.host = null; // force connectPipe
-              connection.connect();
-              return promise.resolve();
-            },
-            getName: function() {
-              return "fakeRuntime";
-            }
-          });
-
-          yield win.handleCommandline("actions=connectToRuntime&runtimeType=usb");
-
-          is(win.AppManager.connection.status, "connected", "connected");
-          is(win.AppManager.selectedProject.name, "A name (in app directory)", "project imported");
-
-          yield removeAllProjects();
-          yield closeWebIDE(win);
-          DebuggerServer.destroy();
-          SimpleTest.finish();
-
-        });
-      }
-
-
-    </script>
-  </body>
-</html>
--- a/browser/devtools/webide/themes/addons.css
+++ b/browser/devtools/webide/themes/addons.css
@@ -1,14 +1,12 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-@import url("chrome://browser/skin/in-content/common.css");
-
 html {
   font: message-box;
   font-size: 15px;
   font-weight: normal;
   margin: 0;
   color: #737980;
   background-image: linear-gradient(#fff, #ededed 100px);
   height: 100%;
index 3a75582b9565c86e445ac554b1eaf1cc9b4916f2..3ef9c16d899d40a732606bf2bf745480922c4e0c
GIT binary patch
literal 28775
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
--- a/browser/devtools/webide/themes/jar.mn
+++ b/browser/devtools/webide/themes/jar.mn
@@ -5,9 +5,10 @@
 webide.jar:
 % skin webide classic/1.0 %skin/
 * skin/webide.css         (webide.css)
   skin/icons.png          (icons.png)
   skin/details.css        (details.css)
   skin/newapp.css         (newapp.css)
   skin/throbber.svg       (throbber.svg)
   skin/addons.css         (addons.css)
+  skin/prefs.css          (prefs.css)
   skin/tabledoc.css       (tabledoc.css)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/themes/prefs.css
@@ -0,0 +1,54 @@
+/* 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/. */
+
+html {
+  font: message-box;
+  font-size: 15px;
+  font-weight: normal;
+  margin: 0;
+  color: #737980;
+  background-image: linear-gradient(#fff, #ededed 100px);
+  height: 100%;
+}
+
+body {
+  padding: 20px;
+}
+
+h1 {
+  font-size: 2.5em;
+  font-weight: lighter;
+  line-height: 1.2;
+  margin: 0;
+  margin-bottom: .5em;
+}
+
+#controls {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+}
+
+#controls > a {
+  color: #4C9ED9;
+  font-size: small;
+  cursor: pointer;
+  border-bottom: 1px dotted;
+}
+
+#close {
+  margin-left: 10px;
+}
+
+li {
+  list-style: none;
+}
+
+li > label:hover {
+  background-color: rgba(0,0,0,0.02);
+}
+
+li > label > span {
+  display: inline-block;
+}
--- a/browser/devtools/webide/themes/webide.css
+++ b/browser/devtools/webide/themes/webide.css
@@ -1,12 +1,22 @@
 /* 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/. */
 
+/*
+ *
+ * Icons.png:
+ *
+ *  actions icons: 100x100. Starts at 0x0.
+ *  menu icons: 26x26. Starts at 312x0.
+ *  anchors icons: 27x16. Starts at 364x0.
+ *
+ */
+
 #main-toolbar {
   padding: 0 12px;
 }
 
 #action-buttons-container {
   -moz-box-pack: center;
   height: 50px;
 }
@@ -39,49 +49,54 @@ window.busy-determined #action-busy-unde
   -moz-appearance: none;
   -moz-box-align: center;
   border-width: 0;
   background: none;
 }
 
 .panel-button-anchor {
   list-style-image: url('icons.png');
-  -moz-image-region: rect(43px, 563px, 61px, 535px);
+  -moz-image-region: rect(0px,391px,16px,364px);
   width: 12px;
   height: 7px;
-  margin-bottom: -5px;
 }
 
 .panel-button:hover > .panel-button-anchor {
-  -moz-image-region: rect(243px, 563px, 261px, 535px);
+  -moz-image-region: rect(0px,445px,16px,418px);
 }
 
 /* Panel buttons - projects */
 
 #project-panel-button > .panel-button-image {
-  width: 18px;
-  height: 18px;
+  width: 13px;
+  height: 13px;
+}
+
+#project-panel-button > .panel-button-image[src] {
+  /* with app icon */
+  width: 20px;
+  height: 20px;
 }
 
 #project-panel-button.no-project > .panel-button-image {
   list-style-image: url("icons.png");
-  -moz-image-region: rect(0px, 740px, 40px, 700px);
+  -moz-image-region: rect(260px,338px,286px,312px);
 }
 
 /* Panel buttons - runtime */
 
 #runtime-panel-button > .panel-button-image {
   list-style-image: url('icons.png');
-  -moz-image-region: rect(25px, 475px, 75px, 425px);
-  width: 1.2em;
-  height: 1.2em;
+  -moz-image-region: rect(78px,338px,104px,312px);
+  width: 13px;
+  height: 13px;
 }
 
 #runtime-panel-button[active="true"] > .panel-button-image {
-  -moz-image-region: rect(125px, 475px, 175px, 425px);
+  -moz-image-region: rect(78px,364px,104px,338px);
 }
 
 /* Action buttons */
 
 .action-button {
   -moz-appearance: none;
   border-width: 0;
   margin: 0;
@@ -99,49 +114,44 @@ window.busy-determined #action-busy-unde
 }
 
 .action-button > .toolbarbutton-text {
   display: none;
 }
 
 #action-button-play  { -moz-image-region: rect(0,100px,100px,0) }
 #action-button-stop  { -moz-image-region: rect(0,200px,100px,100px) }
-#action-button-debug { -moz-image-region: rect(0,400px,100px,300px) }
+#action-button-debug { -moz-image-region: rect(0,300px,100px,200px) }
 
 #action-button-play:not([disabled="true"]):hover  { -moz-image-region: rect(200px,100px,300px,0) }
 #action-button-stop:not([disabled="true"]):hover  { -moz-image-region: rect(200px,200px,300px,100px) }
-#action-button-debug:not([disabled="true"]):not([active="true"]):hover { -moz-image-region: rect(200px,400px,300px,300px) }
+#action-button-debug:not([disabled="true"]):not([active="true"]):hover { -moz-image-region: rect(200px,300px,300px,200px) }
 
-#action-button-debug[active="true"] { -moz-image-region: rect(100px,400px,200px,300px) }
+#action-button-debug[active="true"] { -moz-image-region: rect(100px,300px,200px,200px) }
 
 /* Panels */
 
 panel > vbox {
   overflow-x: hidden;
 }
 
 panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 12px 0;
-  width: 180px;
+  width: 200px;
 }
 
-.panel-item,
-.panel-item-help {
+.panel-item {
   padding: 3px 12px;
   margin: 0;
   -moz-appearance: none;
   border-width: 0;
+  font-size: 13px; /* height of the icons */
 }
 
-.panel-item-help {
-  font-size: 0.9em;
-}
-
-.panel-item:hover,
-.panel-item-help:hover {
+.panel-item:hover {
   background: #CBF0FE;
 }
 
 .panel-header {
   /* We can't use borders or vertical padding here because
    * panels don't take these into account when calculated the
    * height of the panel.
    */
@@ -154,77 +164,91 @@ panel > .panel-arrowcontainer > .panel-a
   padding: 0 16px;
   line-height: 200%;
   margin: 5px 0;
   font-size: 90%;
   font-weight: bold;
 }
 
 .panel-item > .toolbarbutton-icon {
-  width: 18px;
-  height: 18px;
+  width: 13px;
+  height: 13px;
+  margin-right: 12px; /* to compensate panel-item padding */
 }
 
-.panel-item > .toolbarbutton-text,
-.panel-item-help > .toolbarbutton-text {
+.panel-item > .toolbarbutton-text {
   text-align: start;
 }
 
 /* project panel */
 
 .project-panel-item-newapp,
 .project-panel-item-openpackaged,
 .project-panel-item-openhosted {
   list-style-image: url("icons.png");
 }
 
-.project-panel-item-newapp       { -moz-image-region: rect(0px, 640px, 40px, 600px) }
-.project-panel-item-openpackaged { -moz-image-region: rect(0px, 740px, 40px, 700px) }
-.project-panel-item-openhosted   { -moz-image-region: rect(0px, 840px, 40px, 800px) }
+.project-panel-item-newapp       { -moz-image-region: rect(234px,338px,260px,312px) }
+.project-panel-item-openpackaged { -moz-image-region: rect(260px,338px,286px,312px) }
+.project-panel-item-openhosted   { -moz-image-region: rect(208px,338px,234px,312px) }
 
 /* runtime panel */
 
 #runtime-panel .panel-arrowcontent {
   padding: 12px 0 0;
 }
 
 #runtime-panel-custom {
   margin-bottom: 12px;
 }
 
+#runtime-details,
+#runtime-screenshot,
 #runtime-permissions,
-#runtime-screenshot,
+#runtime-disconnect,
+#runtime-panel-nousbdevice,
+#runtime-panel-noadbhelper,
+#runtime-panel-nosimulator,
 .runtime-panel-item-usb,
 .runtime-panel-item-wifi,
 .runtime-panel-item-custom,
 .runtime-panel-item-simulator {
   list-style-image: url("icons.png");
 }
 
-#runtime-screenshot             { -moz-image-region: rect(200px, 640px, 240px, 600px) }
-#runtime-permissions            { -moz-image-region: rect(100px, 840px, 140px, 800px) }
-.runtime-panel-item-usb         { -moz-image-region: rect(100px, 640px, 140px, 600px) }
-.runtime-panel-item-wifi        { -moz-image-region: rect(100px, 640px, 140px, 600px) }
-.runtime-panel-item-custom      { -moz-image-region: rect(100px, 640px, 140px, 600px) }
-.runtime-panel-item-simulator   { -moz-image-region: rect(100px, 740px, 140px, 700px) }
+#runtime-details                { -moz-image-region: rect(156px,338px,182px,312px) }
+#runtime-screenshot             { -moz-image-region: rect(130px,338px,156px,312px) }
+#runtime-permissions            { -moz-image-region: rect(104px,338px,130px,312px) }
+#runtime-disconnect             { -moz-image-region: rect(52px,338px,78px,312px) }
+#runtime-panel-nousbdevice      { -moz-image-region: rect(156px,338px,182px,312px) }
+#runtime-panel-noadbhelper      { -moz-image-region: rect(234px,338px,260px,312px) }
+#runtime-panel-nosimulator      { -moz-image-region: rect(0px,338px,26px,312px) }
+.runtime-panel-item-usb         { -moz-image-region: rect(52px,338px,78px,312px) }
+.runtime-panel-item-wifi        { -moz-image-region: rect(208px,338px,234px,312px) }
+.runtime-panel-item-custom      { -moz-image-region: rect(26px,338px,52px,312px) }
+.runtime-panel-item-simulator   { -moz-image-region: rect(0px,338px,26px,312px) }
 
 #runtime-actions {
   border-top: 1px solid rgba(221,221,221,1);
 }
 
 
 #runtime-actions > toolbarbutton {
   border-top: 1px solid rgba(221,221,221,1);
   background-color: rgba(233,233,233,1);
   color: rgba(87,87,87,1);
   padding-top: 8px;
   padding-bottom: 8px;
 }
 
-#runtime-actions > toolbarbutton:hover {
+#runtime-actions > toolbarbutton[disabled="true"] {
+  opacity: 0.4;
+}
+
+#runtime-actions > toolbarbutton:not([disabled="true"]):hover {
   background-color: #CBF0FE;
 }
 
 #runtime-actions > toolbarbutton:last-child {
   border-radius: 0 0 3px 3px;
 }
 
 /* Main view */
--- a/browser/devtools/webide/webide-prefs.js
+++ b/browser/devtools/webide/webide-prefs.js
@@ -2,14 +2,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 pref("devtools.webide.showProjectEditor", true);
 pref("devtools.webide.templatesURL", "http://code.cdn.mozilla.net/templates/list.json");
 pref("devtools.webide.autoinstallADBHelper", true);
 pref("devtools.webide.lastprojectlocation", "");
+pref("devtools.webide.restoreLastProject", true);
 pref("devtools.webide.enableLocalRuntime", false);
 pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json");
 pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi");
 pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org");
 pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
 pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
--- a/configure.in
+++ b/configure.in
@@ -3921,16 +3921,17 @@ fi
 USE_ARM_KUSER=
 BUILD_CTYPES=1
 MOZ_USE_NATIVE_POPUP_WINDOWS=
 MOZ_ANDROID_HISTORY=
 MOZ_WEBSMS_BACKEND=
 MOZ_ANDROID_BEAM=
 MOZ_LOCALE_SWITCHER=
 MOZ_ANDROID_SEARCH_ACTIVITY=
+MOZ_ANDROID_MLS_STUMBLER=
 ACCESSIBILITY=1
 MOZ_TIME_MANAGER=
 MOZ_PAY=
 MOZ_AUDIO_CHANNEL_MANAGER=
 NSS_NO_LIBPKIX=
 MOZ_CONTENT_SANDBOX=
 MOZ_CONTENT_SANDBOX_REPORTER=1
 JSGC_USE_EXACT_ROOTING=
@@ -4952,16 +4953,23 @@ fi
 dnl ========================================================
 dnl = Include Search Activity on Android
 dnl ========================================================
 if test -n "$MOZ_ANDROID_SEARCH_ACTIVITY"; then
     AC_DEFINE(MOZ_ANDROID_SEARCH_ACTIVITY)
 fi
 
 dnl ========================================================
+dnl = Include Mozilla Location Service Stumbler on Android
+dnl ========================================================
+if test -n "$MOZ_ANDROID_MLS_STUMBLER"; then
+    AC_DEFINE(MOZ_ANDROID_MLS_STUMBLER)
+fi
+
+dnl ========================================================
 dnl = Enable IPDL's "expensive" unit tests
 dnl ========================================================
 MOZ_IPDL_TESTS=
 
 MOZ_ARG_ENABLE_BOOL(ipdl-tests,
 [  --enable-ipdl-tests     Enable expensive IPDL tests],
     MOZ_IPDL_TESTS=1,
     MOZ_IPDL_TESTS=)
@@ -8507,16 +8515,17 @@ AC_SUBST(MOZ_D3DCOMPILER_XP_CAB)
 AC_SUBST(MOZ_METRO)
 
 AC_SUBST(MOZ_ANDROID_HISTORY)
 AC_SUBST(MOZ_WEBSMS_BACKEND)
 AC_SUBST(MOZ_ANDROID_BEAM)
 AC_SUBST(MOZ_LOCALE_SWITCHER)
 AC_SUBST(MOZ_DISABLE_GECKOVIEW)
 AC_SUBST(MOZ_ANDROID_SEARCH_ACTIVITY)
+AC_SUBST(MOZ_ANDROID_MLS_STUMBLER)
 AC_SUBST(ENABLE_STRIP)
 AC_SUBST(PKG_SKIP_STRIP)
 AC_SUBST(STRIP_FLAGS)
 AC_SUBST(USE_ELF_HACK)
 AC_SUBST(INCREMENTAL_LINKER)
 AC_SUBST(MOZ_COMPONENTS_VERSION_SCRIPT_LDFLAGS)
 AC_SUBST(MOZ_COMPONENT_NSPR_LIBS)
 
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1533,17 +1533,17 @@ Navigator::GetFeature(const nsAString& a
     if (feature.Equals(manifestFeatures[i])) {
       p->MaybeResolve(true);
       return p.forget();
     }
   }
 
   NS_NAMED_LITERAL_STRING(apiWindowPrefix, "api.window.");
   if (StringBeginsWith(aName, apiWindowPrefix)) {
-    const nsAString& featureName = Substring(aName, apiWindowPrefix.Length(), aName.Length()-apiWindowPrefix.Length());
+    const nsAString& featureName = Substring(aName, apiWindowPrefix.Length());
     if (IsFeatureDetectible(featureName)) {
       p->MaybeResolve(true);
     } else {
       p->MaybeResolve(JS::UndefinedHandleValue);
     }
     return p.forget();
   }
 
--- a/dom/bindings/parser/WebIDL.py
+++ b/dom/bindings/parser/WebIDL.py
@@ -564,24 +564,30 @@ class IDLInterface(IDLObjectWithScope):
             raise WebIDLError("%s inherits from %s which does not have "
                               "a definition" %
                               (self.identifier.name,
                                self.parent.identifier.name),
                               [self.location])
         assert not parent or isinstance(parent, IDLInterface)
 
         if self.getExtendedAttribute("FeatureDetectible"):
-            if self.getExtendedAttribute("NoInterfaceObject"):
-                raise WebIDLError("[FeatureDetectible] not allowed on interface "
-                                  " with [NoInterfaceObject]",
+            if not (self.getExtendedAttribute("Func") or
+                    self.getExtendedAttribute("AvailableIn") or
+                    self.getExtendedAttribute("CheckPermissions")):
+                raise WebIDLError("[FeatureDetectible] is only allowed in combination "
+                                  "with [Func], [AvailableIn] or [CheckPermissions]",
                                   [self.location])
             if self.getExtendedAttribute("Pref"):
                 raise WebIDLError("[FeatureDetectible] must not be specified "
                                   "in combination with [Pref]",
                                   [self.location])
+            if not self.hasInterfaceObject():
+                raise WebIDLError("[FeatureDetectible] not allowed on interface "
+                                  "with [NoInterfaceObject]",
+                                  [self.location])
 
         self.parent = parent
 
         assert iter(self.members)
 
         if self.parent:
             self.parent.finish(scope)
 
@@ -2900,19 +2906,19 @@ class IDLAttribute(IDLInterfaceMember):
         if not self.type.isInterface() and self.getExtendedAttribute("SameObject"):
             raise WebIDLError("An attribute with [SameObject] must have an "
                               "interface type as its type", [self.location])
 
         if self.getExtendedAttribute("FeatureDetectible"):
             if not (self.getExtendedAttribute("Func") or
                     self.getExtendedAttribute("AvailableIn") or
                     self.getExtendedAttribute("CheckPermissions")):
-                raise WebIDLError("[%s] is only allowed in combination with [Func], "
-                                  "[AvailableIn] or [CheckPermissions]" % identifier,
-                                  [attr.location, self.location])
+                raise WebIDLError("[FeatureDetectible] is only allowed in combination "
+                                  "with [Func], [AvailableIn] or [CheckPermissions]",
+                                  [self.location])
             if self.getExtendedAttribute("Pref"):
                 raise WebIDLError("[FeatureDetectible] must not be specified "
                                   "in combination with [Pref]",
                                   [self.location])
 
     def validate(self):
         if ((self.getExtendedAttribute("Cached") or
              self.getExtendedAttribute("StoreInSlot")) and
@@ -3026,36 +3032,30 @@ class IDLAttribute(IDLInterfaceMember):
             if self.isStatic():
                 raise WebIDLError("[%s] is only allowed on non-static "
                                   "attributes" % identifier,
                                   [attr.location, self.location])
             if self.getExtendedAttribute("LenientThis"):
                 raise WebIDLError("[LenientThis] is not allowed in combination "
                                   "with [%s]" % identifier,
                                   [attr.location, self.location])
-        elif identifier == "FeatureDetectible":
-            if not (self.getExtendedAttribute("Func") or
-                    self.getExtendedAttribute("AvailableIn") or
-                    self.getExtendedAttribute("CheckPermissions")):
-                raise WebIDLError("[%s] is only allowed in combination with [Func], "
-                                  "[AvailableIn] or [CheckPermissions]" % identifier,
-                                  [attr.location, self.location])
         elif (identifier == "Pref" or
               identifier == "SetterThrows" or
               identifier == "Pure" or
               identifier == "Throws" or
               identifier == "GetterThrows" or
               identifier == "ChromeOnly" or
               identifier == "SameObject" or
               identifier == "Constant" or
               identifier == "Func" or
               identifier == "Frozen" or
               identifier == "AvailableIn" or
               identifier == "NewObject" or
-              identifier == "CheckPermissions"):
+              identifier == "CheckPermissions" or
+              identifier == "FeatureDetectible"):
             # Known attributes that we don't need to do anything with here
             pass
         else:
             raise WebIDLError("Unknown extended attribute %s on attribute" % identifier,
                               [attr.location])
         IDLInterfaceMember.handleExtendedAttribute(self, attr)
 
     def resolve(self, parentScope):
@@ -3457,18 +3457,18 @@ class IDLMethod(IDLInterfaceMember, IDLS
             if not (self.getExtendedAttribute("Func") or
                     self.getExtendedAttribute("AvailableIn") or
                     self.getExtendedAttribute("CheckPermissions")):
                 raise WebIDLError("[FeatureDetectible] is only allowed in combination "
                                   "with [Func], [AvailableIn] or [CheckPermissions]",
                                   [self.location])
             if self.getExtendedAttribute("Pref"):
                 raise WebIDLError("[FeatureDetectible] must not be specified "
-                                      "in combination with [Pref]",
-                                      [self.location])
+                                  "in combination with [Pref]",
+                                  [self.location])
 
         overloadWithPromiseReturnType = None
         overloadWithoutPromiseReturnType = None
         for overload in self._overloads:
             variadicArgument = None
 
             arguments = overload.arguments
             for (idx, argument) in enumerate(arguments):
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -132,16 +132,23 @@
 
             <meta-data android:name="com.sec.minimode.icon.landscape.normal"
                        android:resource="@drawable/icon" />
 
             <intent-filter>
                 <action android:name="org.mozilla.gecko.ACTION_ALERT_CALLBACK" />
             </intent-filter>
 
+            <!-- Notification API V2 -->
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.helperBroadcastAction" />
+                <data android:scheme="moz-notification" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+
             <intent-filter>
                 <action android:name="org.mozilla.gecko.UPDATE"/>
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
 
             <!-- Default browser intents -->
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
@@ -408,16 +415,19 @@
 
 #include ../services/manifests/AnnouncementsAndroidManifest_services.xml.in
 #include ../services/manifests/FxAccountAndroidManifest_services.xml.in
 #include ../services/manifests/HealthReportAndroidManifest_services.xml.in
 #include ../services/manifests/SyncAndroidManifest_services.xml.in
 #ifdef MOZ_ANDROID_SEARCH_ACTIVITY
 #include ../search/manifests/SearchAndroidManifest_services.xml.in
 #endif
+#ifdef MOZ_ANDROID_MLS_STUMBLER
+#include ../stumbler/manifests/StumblerManifest_services.xml.in
+#endif
 
     </application>
 
     <permission android:name="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"
                 android:protectionLevel="signature"/>
 
     <permission android:name="@ANDROID_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"
                 android:protectionLevel="signature"/>
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -469,19 +469,17 @@ public class BrowserApp extends GeckoApp
         }
 
         // This has to be prepared prior to calling GeckoApp.onCreate, because
         // widget code and BrowserToolbar need it, and they're created by the
         // layout, which GeckoApp takes care of.
         ((GeckoApplication) getApplication()).prepareLightweightTheme();
         super.onCreate(savedInstanceState);
 
-        // Init suggested sites engine in BrowserDB.
-        final SuggestedSites suggestedSites = new SuggestedSites(getApplicationContext());
-        BrowserDB.setSuggestedSites(suggestedSites);
+        final Context appContext = getApplicationContext();
 
         mViewFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
         mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
 
         mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar);
         mProgressView = (ToolbarProgressView) findViewById(R.id.progress);
         mBrowserToolbar.setProgressBar(mProgressView);
         if (Intent.ACTION_VIEW.equals(intent.getAction())) {
@@ -545,34 +543,38 @@ public class BrowserApp extends GeckoApp
             "Menu:Remove",
             "Reader:ListStatusRequest",
             "Reader:Removed",
             "Reader:Share",
             "Settings:Show",
             "Telemetry:Gather",
             "Updater:Launch");
 
-        Distribution.init(this);
+        Distribution distribution = Distribution.init(this);
+
+        // Init suggested sites engine in BrowserDB.
+        final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
+        BrowserDB.setSuggestedSites(suggestedSites);
 
         // Shipping Native casting is optional and dependent on whether you've downloaded the support
         // and google play libraries
         if (AppConstants.MOZ_MEDIA_PLAYER) {
             try {
                 Class<?> mediaManagerClass = Class.forName("org.mozilla.gecko.MediaPlayerManager");
                 Method init = mediaManagerClass.getMethod("init", Context.class);
                 init.invoke(null, this);
             } catch(Exception ex) {
                 // Ignore failures
                 Log.i(LOGTAG, "No native casting support", ex);
             }
         }
 
-        JavaAddonManager.getInstance().init(getApplicationContext());
-        mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
-        mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());
+        JavaAddonManager.getInstance().init(appContext);
+        mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
+        mOrderedBroadcastHelper = new OrderedBroadcastHelper(appContext);
         mBrowserHealthReporter = new BrowserHealthReporter();
 
         if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
                 nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
                     @Override
                     public NdefMessage createNdefMessage(NfcEvent event) {
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1317,17 +1317,16 @@ public abstract class GeckoApp
                     public void run() {
                         GeckoApp.this.onLocaleReady(uiLocale);
                     }
                 });
             }
         });
 
         GeckoAppShell.setNotificationClient(makeNotificationClient());
-        NotificationHelper.init(getApplicationContext());
         IntentHelper.init(this);
     }
 
     /**
      * At this point, the resource system and the rest of the browser are
      * aware of the locale.
      *
      * Now we can display strings!
@@ -1622,16 +1621,18 @@ public abstract class GeckoApp
                 Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
             geckoConnected();
             GeckoAppShell.setLayerClient(mLayerView.getLayerClientObject());
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Viewport:Flush", null));
         }
 
         if (ACTION_ALERT_CALLBACK.equals(action)) {
             processAlertCallback(intent);
+        } else if (NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) {
+            NotificationHelper.getInstance(getApplicationContext()).handleNotificationIntent(intent);
         }
     }
 
     private String restoreSessionTabs(final boolean isExternalURL) throws SessionRestoreException {
         try {
             String sessionString = getProfile().readSessionFile(false);
             if (sessionString == null) {
                 throw new SessionRestoreException("Could not read from session file");
@@ -1896,32 +1897,34 @@ public abstract class GeckoApp
         } else if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
             String uri = getURIFromIntent(intent);
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBookmarkLoadEvent(uri));
         } else if (Intent.ACTION_SEARCH.equals(action)) {
             String uri = getURIFromIntent(intent);
             GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri));
         } else if (ACTION_ALERT_CALLBACK.equals(action)) {
             processAlertCallback(intent);
+        } else if (NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) {
+            NotificationHelper.getInstance(getApplicationContext()).handleNotificationIntent(intent);
         } else if (ACTION_LAUNCH_SETTINGS.equals(action)) {
             // Check if launched from data reporting notification.
             Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
             // Copy extras.
             settingsIntent.putExtras(intent);
             startActivity(settingsIntent);
         }
     }
 
     /*
      * Handles getting a uri from and intent in a way that is backwards
      * compatable with our previous implementations
      */
     protected String getURIFromIntent(Intent intent) {
         final String action = intent.getAction();
-        if (ACTION_ALERT_CALLBACK.equals(action))
+        if (ACTION_ALERT_CALLBACK.equals(action) || NotificationHelper.HELPER_BROADCAST_ACTION.equals(action))
             return null;
 
         String uri = intent.getDataString();
         if (uri != null)
             return uri;
 
         if ((action != null && action.startsWith(ACTION_WEBAPP_PREFIX)) || ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
             uri = intent.getStringExtra("args");
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -117,16 +117,18 @@ public class GeckoApplication extends Ap
 
     @Override
     public void onCreate() {
         HardwareUtils.init(getApplicationContext());
         Clipboard.init(getApplicationContext());
         FilePicker.init(getApplicationContext());
         GeckoLoader.loadMozGlue();
         HomePanelsManager.getInstance().init(getApplicationContext());
+        // This getInstance call will force initializatino of the NotificationHelper, but does nothing with the result
+        NotificationHelper.getInstance(getApplicationContext()).init();
         super.onCreate();
     }
 
     public boolean isApplicationInBackground() {
         return mInBackground;
     }
 
     public LightweightTheme getLightweightTheme() {
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -92,16 +92,22 @@ ALL_JARS += webrtc.jar
 endif
 
 ifdef MOZ_ANDROID_SEARCH_ACTIVITY
 extra_packages += org.mozilla.search
 ALL_JARS += search-activity.jar
 generated/org/mozilla/search/R.java: .aapt.deps ;
 endif
 
+ifdef MOZ_ANDROID_MLS_STUMBLER
+extra_packages += org.mozilla.mozstumbler
+ALL_JARS += ../stumbler/stumbler.jar
+generated/org/mozilla/mozstumbler/R.java: .aapt.deps ;
+endif
+
 include $(topsrcdir)/config/config.mk
 
 # Note that we're going to set up a dependency directly between embed_android.dex and the java files
 # Instead of on the .class files, since more than one .class file might be produced per .java file
 # Sync dependencies are provided in a single jar. Sync classes themselves are delivered as source,
 # because Android resource classes must be compiled together in order to avoid overlapping resource
 # indices.
 
--- a/mobile/android/base/NotificationClient.java
+++ b/mobile/android/base/NotificationClient.java
@@ -15,17 +15,17 @@ import java.util.concurrent.ConcurrentHa
 
 /**
  * Client for posting notifications through a NotificationHandler.
  */
 public abstract class NotificationClient {
     private static final String LOGTAG = "GeckoNotificationClient";
 
     private volatile NotificationHandler mHandler;
-    private boolean mReady;
+    private boolean mReady = false;
     private final LinkedList<Runnable> mTaskQueue = new LinkedList<Runnable>();
     private final ConcurrentHashMap<Integer, UpdateRunnable> mUpdatesMap =
             new ConcurrentHashMap<Integer, UpdateRunnable>();
 
     /**
      * Runnable that is reused between update notifications.
      *
      * Updates happen frequently, so reusing Runnables prevents frequent dynamic allocation.
@@ -137,27 +137,30 @@ public abstract class NotificationClient
     }
 
     /**
      * Removes a notification.
      *
      * @see NotificationHandler#remove(int)
      */
     public synchronized void remove(final int notificationID) {
-        if (!mReady) {
-            return;
-        }
-
         mTaskQueue.add(new Runnable() {
             @Override
             public void run() {
                 mHandler.remove(notificationID);
                 mUpdatesMap.remove(notificationID);
             }
         });
+
+        // If mReady == false, we haven't added any notifications yet. That can happen if Fennec is being
+        // started in response to clicking a notification. Call bind() to ensure the task we posted above is run.
+        if (!mReady) {
+            bind();
+        }
+
         notify();
     }
 
     /**
      * Determines whether a notification is showing progress.
      *
      * @see NotificationHandler#isProgressStyle(int)
      */
--- a/mobile/android/base/NotificationHelper.java
+++ b/mobile/android/base/NotificationHelper.java
@@ -19,24 +19,24 @@ import android.content.IntentFilter;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.support.v4.app.NotificationCompat;
 import android.util.Log;
 
 import java.util.Iterator;
-import java.util.Set;
-import java.util.HashSet;
+import java.util.HashMap;
 
 public final class NotificationHelper implements GeckoEventListener {
+    public static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction";
+
     public static final String NOTIFICATION_ID = "NotificationHelper_ID";
-    private static final String LOGTAG = "GeckoNotificationManager";
+    private static final String LOGTAG = "GeckoNotificationHelper";
     private static final String HELPER_NOTIFICATION = "helperNotif";
-    private static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction";
 
     // Attributes mandatory to be used while sending a notification from js.
     private static final String TITLE_ATTR = "title";
     private static final String TEXT_ATTR = "text";
     private static final String ID_ATTR = "id";
     private static final String SMALLICON_ATTR = "smallIcon";
 
     // Attributes that can be used while sending a notification from js.
@@ -49,145 +49,164 @@ public final class NotificationHelper im
     private static final String PRIORITY_ATTR = "priority";
     private static final String LARGE_ICON_ATTR = "largeIcon";
     private static final String EVENT_TYPE_ATTR = "eventType";
     private static final String ACTIONS_ATTR = "actions";
     private static final String ACTION_ID_ATTR = "buttonId";
     private static final String ACTION_TITLE_ATTR = "title";
     private static final String ACTION_ICON_ATTR = "icon";
     private static final String PERSISTENT_ATTR = "persistent";
+    private static final String HANDLER_ATTR = "handlerKey";
+    private static final String COOKIE_ATTR = "cookie";
 
     private static final String NOTIFICATION_SCHEME = "moz-notification";
 
     private static final String BUTTON_EVENT = "notification-button-clicked";
     private static final String CLICK_EVENT = "notification-clicked";
     private static final String CLEARED_EVENT = "notification-cleared";
     private static final String CLOSED_EVENT = "notification-closed";
 
-    private static Context mContext;
-    private static Set<String> mClearableNotifications;
-    private static BroadcastReceiver mReceiver;
-    private static NotificationHelper mInstance;
+    private Context mContext;
+
+    // Holds a list of notifications that should be cleared if the Fennec Activity is shut down.
+    // Will not include ongoing or persistent notifications that are tied to Gecko's lifecycle.
+    private HashMap<String, String> mClearableNotifications;
 
-    private NotificationHelper() {
+    private boolean mInitialized = false;
+    private static NotificationHelper sInstance;
+
+    private NotificationHelper(Context context) {
+        mContext = context;
     }
 
-    public static void init(Context context) {
-        if (mInstance != null) {
-            Log.w(LOGTAG, "NotificationHelper.init() called twice!");
-            return;
-        }
-        mInstance = new NotificationHelper();
-        mContext = context;
-        mClearableNotifications = new HashSet<String>();
-        EventDispatcher.getInstance().registerGeckoThreadListener(mInstance,
+    public void init() {
+        mClearableNotifications = new HashMap<String, String>();
+        EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "Notification:Show",
             "Notification:Hide");
-        registerReceiver(context);
+        mInitialized = true;
+    }
+
+    public static NotificationHelper getInstance(Context context) {
+        // If someone else created this singleton, but didn't initialize it, something has gone wrong.
+        if (sInstance != null && !sInstance.mInitialized) {
+            throw new IllegalStateException("NotificationHelper was created by someone else but not initialized");
+        }
+
+        if (sInstance == null) {
+            sInstance = new NotificationHelper(context.getApplicationContext());
+        }
+        return sInstance;
     }
 
     @Override
     public void handleMessage(String event, JSONObject message) {
         if (event.equals("Notification:Show")) {
             showNotification(message);
         } else if (event.equals("Notification:Hide")) {
             hideNotification(message);
         }
     }
 
     public boolean isHelperIntent(Intent i) {
         return i.getBooleanExtra(HELPER_NOTIFICATION, false);
     }
 
-    private static void registerReceiver(Context context) {
-        IntentFilter filter = new IntentFilter(HELPER_BROADCAST_ACTION);
-        // Scheme is needed, otherwise only broadcast with no data will be catched.
-        filter.addDataScheme(NOTIFICATION_SCHEME);
-        mReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                mInstance.handleNotificationIntent(intent);
-            }
-        };
-        context.registerReceiver(mReceiver, filter);
-    }
-
-
-    private void handleNotificationIntent(Intent i) {
+    public void handleNotificationIntent(Intent i) {
         final Uri data = i.getData();
         if (data == null) {
-            Log.w(LOGTAG, "handleNotificationEvent: empty data");
+            Log.e(LOGTAG, "handleNotificationEvent: empty data");
             return;
         }
         final String id = data.getQueryParameter(ID_ATTR);
         final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR);
         if (id == null || notificationType == null) {
-            Log.w(LOGTAG, "handleNotificationEvent: invalid intent parameters");
+            Log.e(LOGTAG, "handleNotificationEvent: invalid intent parameters");
             return;
         }
 
-        // In case the user swiped out the notification, we empty the id
-        // set.
+        // In case the user swiped out the notification, we empty the id set.
         if (CLEARED_EVENT.equals(notificationType)) {
             mClearableNotifications.remove(id);
+            // If Gecko isn't running, we throw away events where the notification was cancelled.
+            // i.e. Don't bug the user if they're just closing a bunch of notifications.
+            if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
+                return;
+            }
         }
 
-        if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
-            JSONObject args = new JSONObject();
-            try {
-                args.put(ID_ATTR, id);
-                args.put(EVENT_TYPE_ATTR, notificationType);
+        JSONObject args = new JSONObject();
+
+        // The handler and cookie parameters are optional
+        final String handler = data.getQueryParameter(HANDLER_ATTR);
+        final String cookie = i.getStringExtra(COOKIE_ATTR);
+
+        try {
+            args.put(ID_ATTR, id);
+            args.put(EVENT_TYPE_ATTR, notificationType);
+            args.put(HANDLER_ATTR, handler);
+            args.put(COOKIE_ATTR, cookie);
 
-                if (BUTTON_EVENT.equals(notificationType)) {
-                    final String actionName = data.getQueryParameter(ACTION_ID_ATTR);
-                    args.put(ACTION_ID_ATTR, actionName);
-                }
+            if (BUTTON_EVENT.equals(notificationType)) {
+                final String actionName = data.getQueryParameter(ACTION_ID_ATTR);
+                args.put(ACTION_ID_ATTR, actionName);
+            }
 
-                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString()));
-            } catch (JSONException e) {
-                Log.w(LOGTAG, "Error building JSON notification arguments.", e);
-            }
+            Log.i(LOGTAG, "Send " + args.toString());
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString()));
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Error building JSON notification arguments.", e);
         }
+
         // If the notification was clicked, we are closing it. This must be executed after
         // sending the event to js side because when the notification is canceled no event can be
         // handled.
         if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) {
-            hideNotification(id);
+            hideNotification(id, handler, cookie);
         }
 
     }
 
     private Uri.Builder getNotificationBuilder(JSONObject message, String type) {
         Uri.Builder b = new Uri.Builder();
         b.scheme(NOTIFICATION_SCHEME).appendQueryParameter(EVENT_TYPE_ATTR, type);
 
         try {
             final String id = message.getString(ID_ATTR);
             b.appendQueryParameter(ID_ATTR, id);
         } catch (JSONException ex) {
             Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
         }
+
+        try {
+            final String id = message.getString(HANDLER_ATTR);
+            b.appendQueryParameter(HANDLER_ATTR, id);
+        } catch (JSONException ex) {
+            Log.i(LOGTAG, "Notification doesn't have a handler");
+        }
+
         return b;
     }
 
     private Intent buildNotificationIntent(JSONObject message, Uri.Builder builder) {
         Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION);
         final boolean ongoing = message.optBoolean(ONGOING_ATTR);
         notificationIntent.putExtra(ONGOING_ATTR, ongoing);
 
         final Uri dataUri = builder.build();
         notificationIntent.setData(dataUri);
         notificationIntent.putExtra(HELPER_NOTIFICATION, true);
+        notificationIntent.putExtra(COOKIE_ATTR, message.optString(COOKIE_ATTR));
         return notificationIntent;
     }
 
     private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) {
         Uri.Builder builder = getNotificationBuilder(message, type);
         final Intent notificationIntent = buildNotificationIntent(message, builder);
-        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        PendingIntent pi = PendingIntent.getActivity(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
         return pi;
     }
 
     private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) {
         Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT);
         try {
             // Action name must be in query uri, otherwise buttons pending intents
             // would be collapsed.
@@ -195,17 +214,17 @@ public final class NotificationHelper im
                 builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR));
             } else {
                 Log.i(LOGTAG, "button event with no name");
             }
         } catch (JSONException ex) {
             Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
         }
         final Intent notificationIntent = buildNotificationIntent(message, builder);
-        PendingIntent res = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        PendingIntent res = PendingIntent.getActivity(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
         return res;
     }
 
     private void showNotification(JSONObject message) {
         NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
 
         // These attributes are required
         final String id;
@@ -285,63 +304,76 @@ public final class NotificationHelper im
         PendingIntent deletePendingIntent = buildNotificationPendingIntent(message, CLEARED_EVENT);
         builder.setDeleteIntent(deletePendingIntent);
 
         GeckoAppShell.notificationClient.add(id.hashCode(), builder.build());
 
         boolean persistent = message.optBoolean(PERSISTENT_ATTR);
         // We add only not persistent notifications to the list since we want to purge only
         // them when geckoapp is destroyed.
-        if (!persistent && !mClearableNotifications.contains(id)) {
-            mClearableNotifications.add(id);
+        if (!persistent && !mClearableNotifications.containsKey(id)) {
+            mClearableNotifications.put(id, message.toString());
         }
     }
 
     private void hideNotification(JSONObject message) {
-        String id;
+        final String id;
+        final String handler;
+        final String cookie;
         try {
             id = message.getString("id");
+            handler = message.optString("handlerKey");
+            cookie  = message.optString("cookie");
         } catch (JSONException ex) {
             Log.i(LOGTAG, "Error parsing", ex);
             return;
         }
 
-        hideNotification(id);
+        hideNotification(id, handler, cookie);
     }
 
-    private void sendNotificationWasClosed(String id) {
-        if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
-            return;
-        }
-        JSONObject args = new JSONObject();
+    private void sendNotificationWasClosed(String id, String handlerKey, String cookie) {
+        final JSONObject args = new JSONObject();
         try {
             args.put(ID_ATTR, id);
+            args.put(HANDLER_ATTR, handlerKey);
+            args.put(COOKIE_ATTR, cookie);
             args.put(EVENT_TYPE_ATTR, CLOSED_EVENT);
+            Log.i(LOGTAG, "Send " + args.toString());
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString()));
         } catch (JSONException ex) {
-            Log.w(LOGTAG, "sendNotificationWasClosed: error building JSON notification arguments.", ex);
+            Log.e(LOGTAG, "sendNotificationWasClosed: error building JSON notification arguments.", ex);
         }
     }
 
-    private void closeNotification(String id) {
+    private void closeNotification(String id, String handlerKey, String cookie) {
         GeckoAppShell.notificationClient.remove(id.hashCode());
-        sendNotificationWasClosed(id);
+        sendNotificationWasClosed(id, handlerKey, cookie);
     }
 
-    public void hideNotification(String id) {
+    public void hideNotification(String id, String handlerKey, String cookie) {
         mClearableNotifications.remove(id);
-        closeNotification(id);
+        closeNotification(id, handlerKey, cookie);
     }
 
     private void clearAll() {
-        for (Iterator<String> i = mClearableNotifications.iterator(); i.hasNext();) {
+        for (Iterator<String> i = mClearableNotifications.keySet().iterator(); i.hasNext();) {
             final String id = i.next();
+            final String json = mClearableNotifications.get(id);
             i.remove();
-            closeNotification(id);
+
+            JSONObject obj;
+            try {
+                obj = new JSONObject(json);
+            } catch(JSONException ex) {
+                obj = new JSONObject();
+            }
+
+            closeNotification(id, obj.optString(HANDLER_ATTR), obj.optString(COOKIE_ATTR));
         }
     }
 
     public static void destroy() {
-        if (mInstance != null) {
-            mInstance.clearAll();
+        if (sInstance != null) {
+            sInstance.clearAll();
         }
     }
 }
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -3,23 +3,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/. */
 
 package org.mozilla.gecko;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.URLMetadata;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
@@ -298,16 +300,31 @@ public class Tab {
         else
             setErrorType(ErrorType.NONE);
     }
 
     public void setErrorType(ErrorType type) {
         mErrorType = type;
     }
 
+    public void setMetadata(JSONObject metadata) {
+        if (metadata == null) {
+            return;
+        }
+
+        final ContentResolver cr = mAppContext.getContentResolver();
+        final Map<String, Object> data = URLMetadata.fromJSON(metadata);
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                URLMetadata.save(cr, mUrl, data);
+            }
+        });
+    }
+
     public ErrorType getErrorType() {
         return mErrorType;
     }
 
     public void setContentType(String contentType) {
         mContentType = (contentType == null) ? "" : contentType;
     }
 
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -491,16 +491,17 @@ public class Tabs implements GeckoEventL
                 String backgroundColor = message.getString("bgColor");
                 if (backgroundColor != null) {
                     tab.setBackgroundColor(backgroundColor);
                 } else {
                     // Default to white if no color is given
                     tab.setBackgroundColor(Color.WHITE);
                 }
                 tab.setErrorType(message.optString("errorType"));
+                tab.setMetadata(message.optJSONObject("metadata"));
                 notifyListeners(tab, Tabs.TabEvents.LOADED);
             } else if (event.equals("DOMTitleChanged")) {
                 tab.updateTitle(message.getString("title"));
             } else if (event.equals("Link:Favicon")) {
                 tab.updateFaviconURL(message.getString("href"), message.getInt("size"));
                 notifyListeners(tab, TabEvents.LINK_FAVICON);
             } else if (event.equals("Link:Feed")) {
                 tab.setHasFeeds(true);
--- a/mobile/android/base/db/AbstractTransactionalProvider.java
+++ b/mobile/android/base/db/AbstractTransactionalProvider.java
@@ -1,17 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.db;
 
 import android.content.ContentProvider;
 import android.content.ContentValues;
-import android.database.Cursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
 import android.os.Build;
 import android.text.TextUtils;
 import android.util.Log;
 
 /**
@@ -93,30 +92,16 @@ public abstract class AbstractTransactio
      * Return true if OS version and database parallelism support indicates
      * that this provider should bundle writes into transactions.
      */
     @SuppressWarnings("static-method")
     protected boolean shouldUseTransactions() {
         return Build.VERSION.SDK_INT >= 11;
     }
 
-    protected static String computeSQLInClause(int items, String field) {
-        final StringBuilder builder = new StringBuilder(field);
-        builder.append(" IN (");
-        int i = 0;
-        for (; i < items - 1; ++i) {
-            builder.append("?, ");
-        }
-        if (i < items) {
-            builder.append("?");
-        }
-        builder.append(")");
-        return builder.toString();
-    }
-
     private boolean isInBatch() {
         final Boolean isInBatch = isInBatchOperation.get();
         if (isInBatch == null) {
             return false;
         }
         return isInBatch.booleanValue();
     }
 
@@ -186,36 +171,16 @@ public abstract class AbstractTransactio
     }
 
     protected void endBatch(final SQLiteDatabase db) {
         trace("Ending batch.");
         db.endTransaction();
         isInBatchOperation.set(Boolean.FALSE);
     }
 
-    /**
-     * Turn a single-column cursor of longs into a single SQL "IN" clause.
-     * We can do this without using selection arguments because Long isn't
-     * vulnerable to injection.
-     */
-    protected static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
-        final StringBuilder builder = new StringBuilder(field);
-        builder.append(" IN (");
-        final int commaLimit = cursor.getCount() - 1;
-        int i = 0;
-        while (cursor.moveToNext()) {
-            builder.append(cursor.getLong(0));
-            if (i++ < commaLimit) {
-                builder.append(", ");
-            }
-        }
-        builder.append(")");
-        return builder.toString();
-    }
-
     @Override
     public int delete(Uri uri, String selection, String[] selectionArgs) {
         trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         int deleted = 0;
 
         try {
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/BaseTable.java
@@ -0,0 +1,72 @@
+/* -*- 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.db;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.util.Log;
+
+// BaseTable provides a basic implementation of a Table for tables that don't require advanced operations during
+// insert, delete, update, or query operations. Implementors must still provide onCreate and onUpgrade operations.
+public abstract class BaseTable implements Table {
+    private static final String LOGTAG = "GeckoBaseTable";
+
+    private static final boolean DEBUG = false;
+
+    protected static void log(String msg) {
+        if (DEBUG) {
+            Log.i(LOGTAG, msg);
+        }
+    }
+
+    // Table implementation
+    @Override
+    public Table.ContentProviderInfo[] getContentProviderInfo() {
+        return new Table.ContentProviderInfo[0];
+    }
+
+    // Table implementation
+    @Override
+    public abstract void onCreate(SQLiteDatabase db);
+
+    // Table implementation
+    @Override
+    public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
+
+    // Returns the name of the table to modify/query
+    protected abstract String getTable();
+
+    // Table implementation
+    @Override
+    public Cursor query(SQLiteDatabase db, Uri uri, int dbId, String[] columns, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit) {
+        Cursor c = db.query(getTable(), columns, selection, selectionArgs, groupBy, null, sortOrder, limit);
+        log("query " + columns + " in " + selection + " = " + c);
+        return c;
+    }
+
+    @Override
+    public int update(SQLiteDatabase db, Uri uri, int dbId, ContentValues values, String selection, String[] selectionArgs) {
+        int updated = db.updateWithOnConflict(getTable(), values, selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE);
+        log("update " + values + " in " + selection + " = " + updated);
+        return updated;
+    }
+
+    @Override
+    public long insert(SQLiteDatabase db, Uri uri, int dbId, ContentValues values) {
+        long inserted = db.insertOrThrow(getTable(), null, values);
+        log("insert " + values + " = " + inserted);
+        return inserted;
+    }
+
+    @Override
+    public int delete(SQLiteDatabase db, Uri uri, int dbId, String selection, String[] selectionArgs) {
+        int deleted = db.delete(getTable(), selection, selectionArgs);
+        log("delete " + selection + " = " + deleted);
+        return deleted;
+    }
+};
--- a/mobile/android/base/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/db/BrowserDatabaseHelper.java
@@ -30,17 +30,17 @@ import android.database.sqlite.SQLiteOpe
 import android.net.Uri;
 import android.os.Build;
 import android.util.Log;
 
 
 final class BrowserDatabaseHelper extends SQLiteOpenHelper {
 
     private static final String LOGTAG = "GeckoBrowserDBHelper";
-    public static final int DATABASE_VERSION = 20;
+    public static final int DATABASE_VERSION = 21;
     public static final String DATABASE_NAME = "browser.db";
 
     final protected Context mContext;
 
     static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
     static final String TABLE_HISTORY = History.TABLE_NAME;
     static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
     static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
@@ -740,16 +740,20 @@ final class BrowserDatabaseHelper extend
                 " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
                     " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
     }
 
     @Override
     public void onCreate(SQLiteDatabase db) {
         debug("Creating browser.db: " + db.getPath());
 
+        for (Table table : BrowserProvider.sTables) {
+            table.onCreate(db);
+        }
+
         createBookmarksTableOn13(db);
         createHistoryTableOn13(db);
         createFaviconsTable(db);
         createThumbnailsTable(db);
 
         createBookmarksWithFaviconsView(db);
         createHistoryWithFaviconsView(db);
         createCombinedViewOn19(db);
@@ -1508,16 +1512,20 @@ final class BrowserDatabaseHelper extend
                     break;
 
                 case 20:
                     upgradeDatabaseFrom19to20(db);
                     break;
             }
         }
 
+        for (Table table : BrowserProvider.sTables) {
+            table.onUpgrade(db, oldVersion, newVersion);
+        }
+
         // If an upgrade after 12->13 fails, the entire upgrade is rolled
         // back, but we can't undo the deletion of favicon_urls.db if we
         // delete this in step 13; therefore, we wait until all steps are
         // complete before removing it.
         if (oldVersion < 13 && newVersion >= 13
                             && mContext.getDatabasePath(Obsolete.FAVICON_DB).exists()
                             && !mContext.deleteDatabase(Obsolete.FAVICON_DB)) {
             throw new SQLException("Could not delete " + Obsolete.FAVICON_DB);
--- a/mobile/android/base/db/BrowserProvider.java
+++ b/mobile/android/base/db/BrowserProvider.java
@@ -110,18 +110,22 @@ public class BrowserProvider extends Sha
 
     static final Map<String, String> BOOKMARKS_PROJECTION_MAP;
     static final Map<String, String> HISTORY_PROJECTION_MAP;
     static final Map<String, String> COMBINED_PROJECTION_MAP;
     static final Map<String, String> SCHEMA_PROJECTION_MAP;
     static final Map<String, String> SEARCH_SUGGEST_PROJECTION_MAP;
     static final Map<String, String> FAVICONS_PROJECTION_MAP;
     static final Map<String, String> THUMBNAILS_PROJECTION_MAP;
+    static final Table[] sTables;
 
     static {
+        sTables = new Table[] {
+            new URLMetadataTable()
+        };
         // We will reuse this.
         HashMap<String, String> map;
 
         // Bookmarks
         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS);
         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID);
         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT);
         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/positions", BOOKMARKS_POSITIONS);
@@ -223,16 +227,22 @@ public class BrowserProvider extends Sha
         map = new HashMap<String, String>();
         map.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
                 Combined.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
         map.put(SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
                 Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
         map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA,
                 Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA);
         SEARCH_SUGGEST_PROJECTION_MAP = Collections.unmodifiableMap(map);
+
+        for (Table table : sTables) {
+            for (Table.ContentProviderInfo type : table.getContentProviderInfo()) {
+                URI_MATCHER.addURI(BrowserContract.AUTHORITY, type.name, type.id);
+            }
+        }
     }
 
     private static boolean hasFaviconsInProjection(String[] projection) {
         if (projection == null) return true;
         for (int i = 0; i < projection.length; ++i) {
             if (projection[i].equals(FaviconColumns.FAVICON) ||
                 projection[i].equals(FaviconColumns.FAVICON_URL))
                 return true;
@@ -346,21 +356,26 @@ public class BrowserProvider extends Sha
                 trace("URI is HISTORY_ID: " + uri);
                 return History.CONTENT_ITEM_TYPE;
             case SEARCH_SUGGEST:
                 trace("URI is SEARCH_SUGGEST: " + uri);
                 return SearchManager.SUGGEST_MIME_TYPE;
             case FLAGS:
                 trace("URI is FLAGS.");
                 return Bookmarks.CONTENT_ITEM_TYPE;
-        }
+            default:
+                String type = getContentItemType(match);
+                if (type != null) {
+                    trace("URI is " + type);
+                    return type;
+                }
 
-        debug("URI has unrecognized type: " + uri);
-
-        return null;
+                debug("URI has unrecognized type: " + uri);
+                return null;
+        }
     }
 
     @SuppressWarnings("fallthrough")
     @Override
     public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
         trace("Calling delete in transaction on URI: " + uri);
         final SQLiteDatabase db = getWritableDatabase(uri);
 
@@ -435,18 +450,25 @@ public class BrowserProvider extends Sha
                 // fall through
             case THUMBNAILS: {
                 trace("Deleting thumbnails: " + uri);
                 beginWrite(db);
                 deleted = deleteThumbnails(uri, selection, selectionArgs);
                 break;
             }
 
-            default:
-                throw new UnsupportedOperationException("Unknown delete URI " + uri);
+            default: {
+                Table table = findTableFor(match);
+                if (table == null) {
+                    throw new UnsupportedOperationException("Unknown delete URI " + uri);
+                }
+                trace("Deleting TABLE: " + uri);
+                beginWrite(db);
+                deleted = table.delete(db, uri, match, selection, selectionArgs);
+            }
         }
 
         debug("Deleted " + deleted + " rows for URI: " + uri);
 
         return deleted;
     }
 
     @Override
@@ -476,18 +498,27 @@ public class BrowserProvider extends Sha
             }
 
             case THUMBNAILS: {
                 trace("Insert on THUMBNAILS: " + uri);
                 id = insertThumbnail(uri, values);
                 break;
             }
 
-            default:
-                throw new UnsupportedOperationException("Unknown insert URI " + uri);
+            default: {
+                Table table = findTableFor(match);
+                if (table == null) {
+                    throw new UnsupportedOperationException("Unknown insert URI " + uri);
+                }
+
+                trace("Insert on TABLE: " + uri);
+                final SQLiteDatabase db = getWritableDatabase(uri);
+                beginWrite(db);
+                id = table.insert(db, uri, match, values);
+            }
         }
 
         debug("Inserted ID in database: " + id);
 
         if (id >= 0)
             return ContentUris.withAppendedId(uri, id);
 
         return null;
@@ -595,18 +626,31 @@ public class BrowserProvider extends Sha
                                                       new String[] { url });
                 } else {
                     updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?",
                                                       new String[] { url });
                 }
                 break;
             }
 
-            default:
-                throw new UnsupportedOperationException("Unknown update URI " + uri);
+            default: {
+                Table table = findTableFor(match);
+                if (table == null) {
+                    throw new UnsupportedOperationException("Unknown update URI " + uri);
+                }
+                trace("Update TABLE: " + uri);
+
+                beginWrite(db);
+                updated = table.update(db, uri, match, values, selection, selectionArgs);
+                if (shouldUpdateOrInsert(uri) && updated == 0) {
+                    trace("No update, inserting for URL: " + uri);
+                    table.insert(db, uri, match, values);
+                    updated = 1;
+                }
+            }
         }
 
         debug("Updated " + updated + " rows for URI: " + uri);
         return updated;
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
@@ -788,18 +832,24 @@ public class BrowserProvider extends Sha
                     sortOrder = DEFAULT_HISTORY_SORT_ORDER;
 
                 qb.setProjectionMap(SEARCH_SUGGEST_PROJECTION_MAP);
                 qb.setTables(VIEW_COMBINED_WITH_FAVICONS);
 
                 break;
             }
 
-            default:
-                throw new UnsupportedOperationException("Unknown query URI " + uri);
+            default: {
+                Table table = findTableFor(match);
+                if (table == null) {
+                    throw new UnsupportedOperationException("Unknown query URI " + uri);
+                }
+                trace("Update TABLE: " + uri);
+                return table.query(db, uri, match, projection, selection, selectionArgs, sortOrder, groupBy, limit);
+            }
         }
 
         trace("Running built query.");
         Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy,
                 null, sortOrder, limit);
         cursor.setNotificationUri(getContext().getContentResolver(),
                 BrowserContract.AUTHORITY_URI);
 
@@ -881,23 +931,17 @@ public class BrowserProvider extends Sha
             if (guids[i] == null) {
                 // We don't want to issue the query if not every GUID is specified.
                 debug("updateBookmarkPositions called with null GUID at index " + i);
                 return 0;
             }
             b.append(" WHEN ? THEN " + i);
         }
 
-        // TODO: use computeSQLInClause
-        b.append(" END WHERE " + Bookmarks.GUID + " IN (");
-        i = 1;
-        while (i++ < processCount) {
-            b.append("?, ");
-        }
-        b.append("?)");
+        b.append(" END WHERE " + DBUtils.computeSQLInClause(processCount, Bookmarks.GUID));
         db.execSQL(b.toString(), args);
 
         // We can't easily get a modified count without calling something like changes().
         return processCount;
     }
 
     /**
      * Construct an update expression that will modify the parents of any records
@@ -977,17 +1021,17 @@ public class BrowserProvider extends Sha
 
         // Compute matching IDs.
         final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
                                        selection, selectionArgs, null, null, null);
 
         // Now that we're done reading, open a transaction.
         final String inClause;
         try {
-            inClause = computeSQLInClauseFromLongs(cursor, Bookmarks._ID);
+            inClause = DBUtils.computeSQLInClauseFromLongs(cursor, Bookmarks._ID);
         } finally {
             cursor.close();
         }
 
         beginWrite(db);
         return db.update(TABLE_BOOKMARKS, values, inClause, null);
     }
 
@@ -1430,9 +1474,35 @@ public class BrowserProvider extends Sha
         endBatch(db);
 
         if (failures) {
             throw new OperationApplicationException();
         }
 
         return results;
     }
+
+    private static Table findTableFor(int id) {
+        for (Table table : sTables) {
+            for (Table.ContentProviderInfo type : table.getContentProviderInfo()) {
+                if (type.id == id) {
+                    return table;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static void addTablesToMatcher(Table[] tables, final UriMatcher matcher) {
+    }
+
+    private static String getContentItemType(final int match) {
+        for (Table table : sTables) {
+            for (Table.ContentProviderInfo type : table.getContentProviderInfo()) {
+                if (type.id == match) {
+                    return "vnd.android.cursor.item/" + type.name;
+                }
+            }
+        }
+
+        return null;
+    }
 }
--- a/mobile/android/base/db/DBUtils.java
+++ b/mobile/android/base/db/DBUtils.java
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.db;
 
 import org.mozilla.gecko.GeckoAppShell;
 
 import android.content.ContentValues;
+import android.database.Cursor;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.text.TextUtils;
 import android.util.Log;
 
 public class DBUtils {
     private static final String LOGTAG = "GeckoDBUtils";
 
     public static final String qualifyColumn(String table, String column) {
@@ -91,9 +92,50 @@ public class DBUtils {
         if (values.containsKey(columnName)) {
             byte[] data = values.getAsByteArray(columnName);
             if (data == null || data.length == 0) {
                 Log.w(LOGTAG, "Tried to insert an empty or non-byte-array image. Ignoring.");
                 values.putNull(columnName);
             }
         }
     }
+
+    /**
+     * Builds a selection string that searches for a list of arguments in a particular column.
+     * For example URL in (?,?,?). Callers should pass the actual arguments into their query
+     * as selection args.
+     * @para columnName   The column to search in
+     * @para size         The number of arguments to search for
+     */
+    public static String computeSQLInClause(int items, String field) {
+        final StringBuilder builder = new StringBuilder(field);
+        builder.append(" IN (");
+        int i = 0;
+        for (; i < items - 1; ++i) {
+            builder.append("?, ");
+        }
+        if (i < items) {
+            builder.append("?");
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+
+    /**
+     * Turn a single-column cursor of longs into a single SQL "IN" clause.
+     * We can do this without using selection arguments because Long isn't
+     * vulnerable to injection.
+     */
+    public static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
+        final StringBuilder builder = new StringBuilder(field);
+        builder.append(" IN (");
+        final int commaLimit = cursor.getCount() - 1;
+        int i = 0;
+        while (cursor.moveToNext()) {
+            builder.append(cursor.getLong(0));
+            if (i++ < commaLimit) {
+                builder.append(", ");
+            }
+        }
+        builder.append(")");
+        return builder.toString();
+    }
 }
--- a/mobile/android/base/db/LocalBrowserDB.java
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -1192,39 +1192,29 @@ public class LocalBrowserDB implements B
      * Returns null if the provided list of URLs is empty or null.
      */
     @Override
     public Cursor getThumbnailsForUrls(ContentResolver cr, List<String> urls) {
         if (urls == null) {
             return null;
         }
 
-        int urlCount = urls.size();
+        final int urlCount = urls.size();
         if (urlCount == 0) {
             return null;
         }
 
         // Don't match against null thumbnails.
-        StringBuilder selection = new StringBuilder(
-                Thumbnails.DATA + " IS NOT NULL AND " +
-                Thumbnails.URL + " IN ("
-        );
-
-        // Compute a (?, ?, ?) sequence to match the provided URLs.
-        int i = 1;
-        while (i++ < urlCount) {
-            selection.append("?, ");
-        }
-        selection.append("?)");
-
-        String[] selectionArgs = urls.toArray(new String[urlCount]);
+        final String selection = Thumbnails.DATA + " IS NOT NULL AND " +
+                           DBUtils.computeSQLInClause(urlCount, Thumbnails.URL);
+        final String[] selectionArgs = urls.toArray(new String[urlCount]);
 
         return cr.query(mThumbnailsUriWithProfile,
                         new String[] { Thumbnails.URL, Thumbnails.DATA },
-                        selection.toString(),
+                        selection,
                         selectionArgs,
                         null);
     }
 
     @Override
     public void removeThumbnails(ContentResolver cr) {
         cr.delete(mThumbnailsUriWithProfile, null, null);
     }
--- a/mobile/android/base/db/SharedBrowserDatabaseProvider.java
+++ b/mobile/android/base/db/SharedBrowserDatabaseProvider.java
@@ -95,26 +95,20 @@ public abstract class SharedBrowserDatab
         // IDs of matching rows, then delete them in one go.
         final long now = System.currentTimeMillis();
         final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
                 SyncColumns.DATE_MODIFIED + " <= " +
                 (now - MAX_AGE_OF_DELETED_RECORDS);
 
         final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
         final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
-        final String[] ids;
         final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
         final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
+        final String inClause;
         try {
-            ids = new String[cursor.getCount()];
-            int i = 0;
-            while (cursor.moveToNext()) {
-                ids[i++] = Long.toString(cursor.getLong(0), 10);
-            }
+            inClause = DBUtils.computeSQLInClauseFromLongs(cursor, CommonColumns._ID);
         } finally {
             cursor.close();
         }
 
-        final String inClause = computeSQLInClause(ids.length,
-                CommonColumns._ID);
-        db.delete(tableName, inClause, ids);
+        db.delete(tableName, inClause, null);
     }
 }
--- a/mobile/android/base/db/SuggestedSites.java
+++ b/mobile/android/base/db/SuggestedSites.java
@@ -1,39 +1,50 @@
 /* -*- 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.db;
 
 import android.content.Context;
+import android.content.ContentResolver;
 import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
 import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Scanner;
 import java.util.Set;
 
 import org.json.JSONArray;
+import org.json.JSONException;
 import org.json.JSONObject;
 
+import org.mozilla.gecko.BrowserLocaleManager;
 import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.RawResource;
 import org.mozilla.gecko.util.ThreadUtils;
 
 /**
  * {@code SuggestedSites} provides API to get a list of locale-specific
@@ -56,16 +67,22 @@ import org.mozilla.gecko.util.ThreadUtil
  */
 @RobocopTarget
 public class SuggestedSites {
     private static final String LOGTAG = "GeckoSuggestedSites";
 
     // SharedPreference key for suggested sites that should be hidden.
     public static final String PREF_SUGGESTED_SITES_HIDDEN = "suggestedSites.hidden";
 
+    // Locale used to generate the current suggested sites.
+    public static final String PREF_SUGGESTED_SITES_LOCALE = "suggestedSites.locale";
+
+    // File in profile dir with the list of suggested sites.
+    private static final String FILENAME = "suggestedsites.json";
+
     private static final String[] COLUMNS = new String[] {
         BrowserContract.SuggestedSites._ID,
         BrowserContract.SuggestedSites.URL,
         BrowserContract.SuggestedSites.TITLE
     };
 
     private static final String JSON_KEY_URL = "url";
     private static final String JSON_KEY_TITLE = "title";
@@ -73,96 +90,307 @@ public class SuggestedSites {
     private static final String JSON_KEY_BG_COLOR = "bgcolor";
 
     private static class Site {
         public final String url;
         public final String title;
         public final String imageUrl;
         public final String bgColor;
 
+        public Site(JSONObject json) throws JSONException {
+            this.url = json.getString(JSON_KEY_URL);
+            this.title = json.getString(JSON_KEY_TITLE);
+            this.imageUrl = json.getString(JSON_KEY_IMAGE_URL);
+            this.bgColor = json.getString(JSON_KEY_BG_COLOR);
+
+            validate();
+        }
+
         public Site(String url, String title, String imageUrl, String bgColor) {
             this.url = url;
             this.title = title;
             this.imageUrl = imageUrl;
             this.bgColor = bgColor;
+
+            validate();
+        }
+
+        private void validate() {
+            // Site instances must have non-empty values for all properties.
+            if (TextUtils.isEmpty(url) ||
+                TextUtils.isEmpty(title) ||
+                TextUtils.isEmpty(imageUrl) ||
+                TextUtils.isEmpty(bgColor)) {
+                throw new IllegalStateException("Suggested sites must have a URL, title, " +
+                                                "image URL, and background color.");
+            }
         }
 
         @Override
         public String toString() {
             return "{ url = " + url + "\n" +
                      "title = " + title + "\n" +
                      "imageUrl = " + imageUrl + "\n" +
                      "bgColor = " + bgColor + " }";
         }
+
+        public JSONObject toJSON() throws JSONException {
+            final JSONObject json = new JSONObject();
+
+            json.put(JSON_KEY_URL, url);
+            json.put(JSON_KEY_TITLE, title);
+            json.put(JSON_KEY_IMAGE_URL, imageUrl);
+            json.put(JSON_KEY_BG_COLOR, bgColor);
+
+            return json;
+        }
     }
 
     private final Context context;
+    private final Distribution distribution;
+    private final File file;
     private Map<String, Site> cachedSites;
-    private Locale cachedLocale;
     private Set<String> cachedBlacklist;
 
     public SuggestedSites(Context appContext) {
-        context = appContext;
+        this(appContext, null);
+    }
+
+    public SuggestedSites(Context appContext, Distribution distribution) {
+        this(appContext, distribution,
+             GeckoProfile.get(appContext).getFile(FILENAME));
+    }
+
+    public SuggestedSites(Context appContext, Distribution distribution, File file) {
+        this.context = appContext;
+        this.distribution = distribution;
+        this.file = file;
+    }
+
+    private static boolean isNewLocale(Context context, Locale requestedLocale) {
+        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
+
+        String locale = prefs.getString(PREF_SUGGESTED_SITES_LOCALE, null);
+        if (locale == null) {
+            // Initialize config with the current locale
+            updateSuggestedSitesLocale(context);
+            return true;
+        }
+
+        return !TextUtils.equals(requestedLocale.toString(), locale);
     }
 
-    private String loadFromFile() {
-        // Do nothing for now
-        return null;
+    /**
+     * Return the current locale and its fallback (en_US) in order.
+     */
+    private static List<Locale> getAcceptableLocales() {
+        final List<Locale> locales = new ArrayList<Locale>();
+
+        final Locale defaultLocale = Locale.getDefault();
+        locales.add(defaultLocale);
+
+        if (!defaultLocale.equals(Locale.US)) {
+            locales.add(Locale.US);
+        }
+
+        return locales;
     }
 
-    private String loadFromResource() {
+    private static Map<String, Site> loadSites(File f) throws IOException {
+        Scanner scanner = null;
+
         try {
-            return RawResource.getAsString(context, R.raw.suggestedsites);
-        } catch (IOException e) {
-            return null;
+            scanner = new Scanner(f, "UTF-8");
+            return loadSites(scanner.useDelimiter("\\A").next());
+        } finally {
+            if (scanner != null) {
+                scanner.close();
+            }
         }
     }
 
-    /**
-     * Refreshes the cached list of sites either from the default raw
-     * source or standard file location. This will be called on every
-     * cache miss during a {@code get()} call.
-     */
-    private void refresh() {
-        Log.d(LOGTAG, "Refreshing tiles from file");
-
-        String jsonString = loadFromFile();
+    private static Map<String, Site> loadSites(String jsonString) {
         if (TextUtils.isEmpty(jsonString)) {
-            Log.d(LOGTAG, "No suggested sites file, loading from resource.");
-            jsonString = loadFromResource();
+            return null;
         }
 
         Map<String, Site> sites = null;
 
         try {
             final JSONArray jsonSites = new JSONArray(jsonString);
             sites = new LinkedHashMap<String, Site>(jsonSites.length());
 
             final int count = jsonSites.length();
             for (int i = 0; i < count; i++) {
-                final JSONObject jsonSite = (JSONObject) jsonSites.get(i);
-                final String url = jsonSite.getString(JSON_KEY_URL);
+                final Site site = new Site(jsonSites.getJSONObject(i));
+                sites.put(site.url, site);
+            }
+        } catch (Exception e) {
+            Log.e(LOGTAG, "Failed to refresh suggested sites", e);
+            return null;
+        }
+
+        return sites;
+    }
 
-                final Site site = new Site(url,
-                                           jsonSite.getString(JSON_KEY_TITLE),
-                                           jsonSite.getString(JSON_KEY_IMAGE_URL),
-                                           jsonSite.getString(JSON_KEY_BG_COLOR));
+    /**
+     * Saves suggested sites file to disk. Access to this method should
+     * be synchronized on 'file'.
+     */
+    private static void saveSites(File f, Map<String, Site> sites) {
+        ThreadUtils.assertNotOnUiThread();
 
-                sites.put(url, site);
+        if (sites == null || sites.isEmpty()) {
+            return;
+        }
+
+        OutputStreamWriter osw = null;
+
+        try {
+            final JSONArray jsonSites = new JSONArray();
+            for (Site site : sites.values()) {
+                jsonSites.put(site.toJSON());
             }
 
-            Log.d(LOGTAG, "Successfully parsed suggested sites.");
+            osw = new OutputStreamWriter(new FileOutputStream(f), "UTF-8");
+
+            final String jsonString = jsonSites.toString();
+            osw.write(jsonString, 0, jsonString.length());
         } catch (Exception e) {
-            Log.e(LOGTAG, "Failed to refresh suggested sites", e);
+            Log.e(LOGTAG, "Failed to save suggested sites", e);
+        } finally {
+            if (osw != null) {
+                try {
+                    osw.close();
+                } catch (IOException e) {
+                    // Ignore.
+                }
+            }
+        }
+    }
+
+    private void maybeWaitForDistribution() {
+        if (distribution == null) {
             return;
         }
 
-        // Update cached list of sites
+        distribution.addOnDistributionReadyCallback(new Runnable() {
+            @Override
+            public void run() {
+                Log.d(LOGTAG, "Running post-distribution task: suggested sites.");
+
+                // If distribution doesn't exist, simply continue to load
+                // suggested sites directly from resources. See refresh().
+                if (!distribution.exists()) {
+                    return;
+                }
+
+                // Merge suggested sites from distribution with the
+                // default ones. Distribution takes precedence.
+                Map<String, Site> sites = loadFromDistribution(distribution);
+                if (sites == null) {
+                    sites = new LinkedHashMap<String, Site>();
+                }
+                sites.putAll(loadFromResource());
+
+                // Update cached list of sites.
+                setCachedSites(sites);
+
+                // Save the result to disk.
+                synchronized (file) {
+                    saveSites(file, sites);
+                }
+
+                // Then notify any active loaders about the changes.
+                final ContentResolver cr = context.getContentResolver();
+                cr.notifyChange(BrowserContract.SuggestedSites.CONTENT_URI, null);
+            }
+        });
+    }
+
+    /**
+     * Loads suggested sites from a distribution file either matching the
+     * current locale or with the fallback locale (en-US).
+     *
+     * It's assumed that the given distribution instance is ready to be
+     * used and exists.
+     */
+    private static Map<String, Site> loadFromDistribution(Distribution dist) {
+        for (Locale locale : getAcceptableLocales()) {
+            try {
+                final String languageTag = BrowserLocaleManager.getLanguageTag(locale);
+                final String path = String.format("suggestedsites/locales/%s/%s",
+                                                  languageTag, FILENAME);
+
+                final File f = dist.getDistributionFile(path);
+                if (f == null) {
+                    Log.d(LOGTAG, "No suggested sites for locale: " + languageTag);
+                    continue;
+                }
+
+                return loadSites(f);
+            } catch (Exception e) {
+                Log.e(LOGTAG, "Failed to open suggested sites for locale " +
+                              locale + " in distribution.", e);
+            }
+        }
+
+        return null;
+    }
+
+    private Map<String, Site> loadFromProfile() {
+        try {
+            synchronized (file) {
+                return loadSites(file);
+            }
+        } catch (FileNotFoundException e) {
+            maybeWaitForDistribution();
+        } catch (IOException e) {
+            // Fall through, return null.
+        }
+
+        return null;
+    }
+
+    private Map<String, Site> loadFromResource() {
+        try {
+            return loadSites(RawResource.getAsString(context, R.raw.suggestedsites));
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    private synchronized void setCachedSites(Map<String, Site> sites) {
         cachedSites = Collections.unmodifiableMap(sites);
-        cachedLocale = Locale.getDefault();
+        updateSuggestedSitesLocale(context);
+    }
+
+    /**
+     * Refreshes the cached list of sites either from the default raw
+     * source or standard file location. This will be called on every
+     * cache miss during a {@code get()} call.
+     */
+    private void refresh() {
+        Log.d(LOGTAG, "Refreshing suggested sites from file");
+
+        Map<String, Site> sites = loadFromProfile();
+        if (sites == null) {
+            sites = loadFromResource();
+        }
+
+        // Update cached list of sites.
+        if (sites != null) {
+            setCachedSites(sites);
+        }
+    }
+
+    private static void updateSuggestedSitesLocale(Context context) {
+        final Editor editor = GeckoSharedPrefs.forProfile(context).edit();
+        editor.putString(PREF_SUGGESTED_SITES_LOCALE, Locale.getDefault().toString());
+        editor.commit();
     }
 
     private boolean isEnabled() {
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
         return prefs.getBoolean(GeckoPreferences.PREFS_SUGGESTED_SITES, true);
     }
 
     private synchronized Site getSiteForUrl(String url) {
@@ -213,17 +441,25 @@ public class SuggestedSites {
         final MatrixCursor cursor = new MatrixCursor(COLUMNS);
 
         // Return an empty cursor if suggested sites have been
         // disabled by the user.
         if (!isEnabled()) {
             return cursor;
         }
 
-        if (cachedSites == null || !locale.equals(cachedLocale)) {
+        final boolean isNewLocale = isNewLocale(context, locale);
+
+        // Force the suggested sites file in profile dir to be re-generated
+        // if the locale has changed.
+        if (isNewLocale) {
+            file.delete();
+        }
+
+        if (cachedSites == null || isNewLocale) {
             Log.d(LOGTAG, "No cached sites, refreshing.");
             refresh();
         }
 
         // Return empty cursor if there was an error when
         // loading the suggested sites or the list is empty.
         if (cachedSites == null || cachedSites.isEmpty()) {
             return cursor;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/Table.java
@@ -0,0 +1,47 @@
+/* -*- 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.db;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+
+// Tables provide a basic wrapper around ContentProvider methods to make it simpler to add new tables into storage.
+// If you create a new Table type, make sure to add it to the sTables list in BrowserProvider to ensure it is queried.
+interface Table {
+    // Provides information to BrowserProvider about the type of URIs this Table can handle.
+    public static class ContentProviderInfo {
+        public final int id; // A number of ID for this table. Used by the UriMatcher in BrowserProvider
+        public final String name; // A name for this table. Will be appended onto uris querying this table
+                                  // This is also used to define the mimetype of data returned from this db, i.e.
+                                  // BrowserProvider will return "vnd.android.cursor.item/" + name
+
+        public ContentProviderInfo(int id, String name) {
+            if (name == null) {
+                throw new IllegalArgumentException("Content provider info must specify a name");
+            }
+            this.id = id;
+            this.name = name;
+        }
+    }
+
+    // Return a list of Info about the ContentProvider URIs this will match
+    ContentProviderInfo[] getContentProviderInfo();
+
+    // Called by BrowserDBHelper whenever the database is created or upgraded.
+    // Order in which tables are created/upgraded isn't guaranteed (yet), so be careful if your Table depends on something in a
+    // separate table.
+    void onCreate(SQLiteDatabase db);
+    void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
+
+    // Called by BrowserProvider when this database queried/modified
+    // The dbId here should match the dbId's you returned in your getContentProviderInfo() call
+    Cursor query(SQLiteDatabase db, Uri uri, int dbId, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit);
+    int update(SQLiteDatabase db, Uri uri, int dbId, ContentValues values, String selection, String[] selectionArgs);
+    long insert(SQLiteDatabase db, Uri uri, int dbId, ContentValues values);
+    int delete(SQLiteDatabase db, Uri uri, int dbId, String selection, String[] selectionArgs);
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/URLMetadata.java
@@ -0,0 +1,190 @@
+/* -*- 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.db;
+
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.Telemetry;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.ContentValues;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.support.v4.util.LruCache;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+// Holds metadata info about urls. Supports some helper functions for getting back a HashMap of key value data.
+public class URLMetadata {
+    private static final String LOGTAG = "GeckoURLMetadata";
+
+    // This returns a list of columns in the table. It's used to simplify some loops for reading/writing data.
+    @SuppressWarnings("serial")
+    private static final Set<String> getModel() {
+        return new HashSet<String>() {{
+            add(URLMetadataTable.URL_COLUMN);
+            add(URLMetadataTable.TILE_IMAGE_URL_COLUMN);
+            add(URLMetadataTable.TILE_COLOR_COLUMN);
+        }};
+    }
+
+    // Store a cache of recent results. This number is chosen to match the max number of tiles on about:home
+    private static final int CACHE_SIZE = 9;
+    // Note: Members of this cache are unmodifiable.
+    private static final LruCache<String, Map<String, Object>> cache = new LruCache<String, Map<String, Object>>(CACHE_SIZE);
+
+    /**
+     * Converts a JSON object into a unmodifiable Map of known metadata properties.
+     * Will throw away any properties that aren't stored in the database.
+     */
+    public static Map<String, Object> fromJSON(JSONObject obj) {
+        Map<String, Object> data = new HashMap<String, Object>();
+
+        Set<String> model = getModel();
+        for (String key : model) {
+            if (obj.has(key)) {
+                data.put(key, obj.optString(key));
+            }
+        }
+
+        return Collections.unmodifiableMap(data);
+    }
+
+    /**
+     * Converts a Cursor into a unmodifiable Map of known metadata properties.
+     * Will throw away any properties that aren't stored in the database.
+     * Will also not iterate through multiple rows in the cursor.
+     */
+    private static Map<String, Object> fromCursor(Cursor c) {
+        Map<String, Object> data = new HashMap<String, Object>();
+
+        Set<String> model = getModel();
+        String[] columns = c.getColumnNames();
+        for (String column : columns) {
+            if (model.contains(column)) {
+                try {
+                    data.put(column, c.getString(c.getColumnIndexOrThrow(column)));
+                } catch (Exception ex) {
+                    Log.i(LOGTAG, "Error getting data for " + column, ex);
+                }
+            }
+        }
+
+        return Collections.unmodifiableMap(data);
+    }
+
+    /**
+     * Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls.
+     * Must not be called from UI or Gecko threads.
+     */
+    public static Map<String, Map<String, Object>> getForUrls(final ContentResolver cr,
+                                                              final List<String> urls,
+                                                              final List<String> columns) {
+        ThreadUtils.assertNotOnUiThread();
+        ThreadUtils.assertNotOnGeckoThread();
+
+        final Map<String, Map<String, Object>> data = new HashMap<String, Map<String, Object>>();
+
+        // Nothing to query for
+        if (urls.isEmpty() || columns.isEmpty()) {
+            Log.e(LOGTAG, "Queried metadata for nothing");
+            return data;
+        }
+
+        // Search the cache for any of these urls
+        List<String> urlsToQuery = new ArrayList<String>();
+        for (String url : urls) {
+            final Map<String, Object> hit = cache.get(url);
+            if (hit != null) {
+                // Cache hit!
+                data.put(url, hit);
+            } else {
+                urlsToQuery.add(url);
+            }
+        }
+
+        Telemetry.HistogramAdd("FENNEC_TILES_CACHE_HIT", data.size());
+
+        // If everything was in the cache, we're done!
+        if (urlsToQuery.size() == 0) {
+            return Collections.unmodifiableMap(data);
+        }
+
+        final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN);
+        // We need the url to build our final HashMap, so we force it to be included in the query.
+        if (!columns.contains(URLMetadataTable.URL_COLUMN)) {
+            columns.add(URLMetadataTable.URL_COLUMN);
+        }
+
+        final Cursor cursor = cr.query(URLMetadataTable.CONTENT_URI,
+                                       columns.toArray(new String[columns.size()]), // columns,
+                                       selection, // selection
+                                       urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs
+                                       null);
+        try {
+            if (!cursor.moveToFirst()) {
+                return Collections.unmodifiableMap(data);
+            }
+
+            do {
+                final Map<String, Object> metadata = fromCursor(cursor);
+                final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN));
+
+                data.put(url, metadata);
+                cache.put(url, metadata);
+            } while(cursor.moveToNext());
+
+        } finally {
+            cursor.close();
+        }
+
+        return Collections.unmodifiableMap(data);
+    }
+
+    /**
+     * Saves a HashMap of metadata into the database. Will iterate through columns
+     * in the Database and only save rows with matching keys in the HashMap.
+     * Must not be called from UI or Gecko threads.
+     */
+    public static void save(final ContentResolver cr, final String url, final Map<String, Object> data) {
+        ThreadUtils.assertNotOnUiThread();
+        ThreadUtils.assertNotOnGeckoThread();
+
+        try {
+            ContentValues values = new ContentValues();
+
+            Set<String> model = getModel();
+            for (String key : model) {
+                if (data.containsKey(key)) {
+                    values.put(key, (String) data.get(key));
+                }
+            }
+
+            if (values.size() == 0) {
+                return;
+            }
+
+            Uri uri = URLMetadataTable.CONTENT_URI.buildUpon()
+                                 .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
+                                 .build();
+            cr.update(uri, values, URLMetadataTable.URL_COLUMN + "=?", new String[] {
+                (String) data.get(URLMetadataTable.URL_COLUMN)
+            });
+        } catch (Exception ex) {
+            Log.e(LOGTAG, "error saving", ex);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/URLMetadataTable.java
@@ -0,0 +1,72 @@
+/* -*- 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.db;
+
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.Telemetry;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.List;
+import java.util.HashMap;
+import java.util.HashSet;
+
+// Holds metadata info about urls. Supports some helper functions for getting back a HashMap of key value data.
+public class URLMetadataTable extends BaseTable {
+    private static final String LOGTAG = "GeckoURLMetadataTable";
+
+    private static final String TABLE = "metadata"; // Name of the table in the db
+    private static final int TABLE_ID_NUMBER = 1200;
+
+    // Uri for querying this table
+    public static final Uri CONTENT_URI = Uri.withAppendedPath(BrowserContract.AUTHORITY_URI, "metadata");
+
+    // Columns in the table
+    public static final String ID_COLUMN = "id";
+    public static final String URL_COLUMN = "url";
+    public static final String TILE_IMAGE_URL_COLUMN = "tileImage";
+    public static final String TILE_COLOR_COLUMN = "tileColor";
+
+    URLMetadataTable() { }
+
+    @Override
+    protected String getTable() {
+        return TABLE;
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        String create = "CREATE TABLE " + TABLE + " (" +
+            ID_COLUMN + " INTEGER PRIMARY KEY, " +
+            URL_COLUMN + " TEXT NON NULL UNIQUE, " +
+            TILE_IMAGE_URL_COLUMN + " STRING, " +
+            TILE_COLOR_COLUMN + " STRING);";
+        db.execSQL(create);
+
+        db.execSQL("CREATE INDEX metadata_url_idx ON " + TABLE + " (" + URL_COLUMN + ")");
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        // This table was added in v19 of the db. Force its creation if we're coming from an earlier version
+        if (newVersion >= 21 && oldVersion < 21) {
+            onCreate(db);
+        }
+    }
+
+    @Override
+    public Table.ContentProviderInfo[] getContentProviderInfo() {
+        return new Table.ContentProviderInfo[] {
+            new Table.ContentProviderInfo(TABLE_ID_NUMBER, TABLE)
+        };
+    }
+}
--- a/mobile/android/base/distribution/Distribution.java
+++ b/mobile/android/base/distribution/Distribution.java
@@ -176,47 +176,49 @@ public class Distribution {
 
             this.localizedAbout = Collections.unmodifiableMap(loc);
             this.valid = (null != this.id) &&
                          (null != this.version) &&
                          (null != this.about);
         }
     }
 
-    private static void init(final Distribution distribution) {
+    private static Distribution init(final Distribution distribution) {
         // Read/write preferences and files on the background thread.
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 boolean distributionSet = distribution.doInit();
                 if (distributionSet) {
                     GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", ""));
                 }
             }
         });
+
+        return distribution;
     }
 
     /**
      * Initializes distribution if it hasn't already been initialized. Sends
      * messages to Gecko as appropriate.
      *
      * @param packagePath where to look for the distribution directory.
      */
     @RobocopTarget
-    public static void init(final Context context, final String packagePath, final String prefsPath) {
-        init(new Distribution(context, packagePath, prefsPath));
+    public static Distribution init(final Context context, final String packagePath, final String prefsPath) {
+        return init(new Distribution(context, packagePath, prefsPath));
     }
 
     /**
      * Use <code>Context.getPackageResourcePath</code> to find an implicit
      * package path. Reuses the existing Distribution if one exists.
      */
     @RobocopTarget
-    public static void init(final Context context) {
-        Distribution.init(Distribution.getInstance(context));
+    public static Distribution init(final Context context) {
+        return init(Distribution.getInstance(context));
     }
 
     /**
      * Returns parsed contents of bookmarks.json.
      * This method should only be called from a background thread.
      */
     public static JSONArray getBookmarks(final Context context) {
         Distribution dist = new Distribution(context);
--- a/mobile/android/base/gfx/BitmapUtils.java
+++ b/mobile/android/base/gfx/BitmapUtils.java
@@ -117,17 +117,17 @@ public final class BitmapUtils {
         if (data.startsWith("-moz-icon://")) {
             final Uri imageUri = Uri.parse(data);
             final String ssp = imageUri.getSchemeSpecificPart();
             final String resource = ssp.substring(ssp.lastIndexOf('/') + 1);
 
             try {
                 final Drawable d = context.getPackageManager().getApplicationIcon(resource);
                 runOnBitmapFoundOnUiThread(loader, d);
-            } catch(Exception ex) { }
+            } catch (Exception ex) { }
 
             return;
         }
 
         if (data.startsWith("drawable://")) {
             final Uri imageUri = Uri.parse(data);
             final int id = getResource(imageUri, R.drawable.ic_status_logo);
             final Drawable d = context.getResources().getDrawable(id);
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/ImageLoader.java
@@ -0,0 +1,154 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.home;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+import com.squareup.picasso.Picasso;
+import com.squareup.picasso.Downloader.Response;
+import com.squareup.picasso.UrlConnectionDownloader;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.mozilla.gecko.distribution.Distribution;
+
+public class ImageLoader {
+    private static final String LOGTAG = "GeckoImageLoader";
+
+    private static final String DISTRIBUTION_SCHEME = "gecko.distribution";
+    private static final String SUGGESTED_SITES_AUTHORITY = "suggestedsites";
+
+    // The order of density factors to try when looking for an image resource
+    // in the distribution directory. It looks for an exact match first (1.0) then
+    // tries to find images with higher density (2.0 and 1.5). If no image is found,
+    // try a lower density (0.5). See loadDistributionImage().
+    private static final float[] densityFactors = new float[] { 1.0f, 2.0f, 1.5f, 0.5f };
+
+    private static enum Density {
+        MDPI,
+        HDPI,
+        XHDPI,
+        XXHDPI;
+
+        @Override
+        public String toString() {
+            return super.toString().toLowerCase();
+        }
+    }
+
+    private static Picasso instance;
+
+    public static synchronized Picasso with(Context context) {
+        if (instance == null) {
+            Picasso.Builder builder = new Picasso.Builder(context);
+
+            final Distribution distribution = Distribution.getInstance(context);
+            builder.downloader(new ImageDownloader(context, distribution));
+            instance = builder.build();
+        }
+
+        return instance;
+    }
+
+    /**
+     * Custom Downloader built on top of Picasso's UrlConnectionDownloader
+     * that supports loading images from custom URIs.
+     */
+    public static class ImageDownloader extends UrlConnectionDownloader {
+        private final Context context;
+        private final Distribution distribution;
+
+        public ImageDownloader(Context context, Distribution distribution) {
+            super(context);
+            this.context = context;
+            this.distribution = distribution;
+        }
+
+        private Density getDensity(float factor) {
+            final DisplayMetrics dm = context.getResources().getDisplayMetrics();
+            final float densityDpi = dm.densityDpi * factor;
+
+            if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) {
+                return Density.XXHDPI;
+            } else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) {
+                return Density.XHDPI;
+            } else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) {
+                return Density.HDPI;
+            }
+
+            // Fallback to mdpi, no need to handle ldpi.
+            return Density.MDPI;
+        }
+
+        @Override
+        public Response load(Uri uri, boolean localCacheOnly) throws IOException {
+            final String scheme = uri.getScheme();
+            if (DISTRIBUTION_SCHEME.equals(scheme)) {
+                return loadDistributionImage(uri);
+            }
+
+            return super.load(uri, localCacheOnly);
+        }
+
+        private static String getPathForDensity(String basePath, Density density,
+                                                String filename) {
+            final File dir = new File(basePath, density.toString());
+            return String.format("%s/%s.png", dir.toString(), filename);
+        }
+
+        /**
+         * Handle distribution URIs in Picasso. The expected format is:
+         *
+         *   gecko.distribution://<basepath>/<imagename>
+         *
+         * Which will look for the following file in the distribution:
+         *
+         *   <distribution-root-dir>/<basepath>/<device-density>/<imagename>.png
+         */
+        private Response loadDistributionImage(Uri uri) throws IOException {
+            // Eliminate the leading '//'
+            final String ssp = uri.getSchemeSpecificPart().substring(2);
+
+            final String filename;
+            final String basePath;
+
+            final int slashIndex = ssp.lastIndexOf('/');
+            if (slashIndex == -1) {
+                filename = ssp;
+                basePath = "";
+            } else {
+                filename = ssp.substring(slashIndex + 1);
+                basePath = ssp.substring(0, slashIndex);
+            }
+
+            Set<Density> triedDensities = EnumSet.noneOf(Density.class);
+
+            for (int i = 0; i < densityFactors.length; i++) {
+                final Density density = getDensity(densityFactors[i]);
+                if (!triedDensities.add(density)) {
+                    continue;
+                }
+
+                final String path = getPathForDensity(basePath, density, filename);
+                Log.d(LOGTAG, "Trying to load image from distribution " + path);
+
+                final File f = distribution.getDistributionFile(path);
+                if (f != null) {
+                    return new Response(new FileInputStream(f), true);
+                }
+            }
+
+            throw new ResponseException("Couldn't find suggested site image in distribution");
+        }
+    }
+}
--- a/mobile/android/base/home/PanelAuthLayout.java
+++ b/mobile/android/base/home/PanelAuthLayout.java
@@ -51,14 +51,14 @@ class PanelAuthLayout extends LinearLayo
 
         final ImageView imageView = (ImageView) findViewById(R.id.image);
         final String imageUrl = authConfig.getImageUrl();
 
         if (TextUtils.isEmpty(imageUrl)) {
             // Use a default image if an image URL isn't specified.
             imageView.setImageResource(R.drawable.icon_home_empty_firefox);
         } else {
-            Picasso.with(getContext())
-                   .load(imageUrl)
-                   .into(imageView);
+            ImageLoader.with(getContext())
+                       .load(imageUrl)
+                       .into(imageView);
         }
     }
 }
--- a/mobile/android/base/home/PanelBackItemView.java
+++ b/mobile/android/base/home/PanelBackItemView.java
@@ -28,20 +28,20 @@ class PanelBackItemView extends LinearLa
 
         title = (TextView) findViewById(R.id.title);
 
         final ImageView image = (ImageView) findViewById(R.id.image);
 
         if (TextUtils.isEmpty(backImageUrl)) {
             image.setImageResource(R.drawable.folder_up);
         } else {
-            Picasso.with(getContext())
-                   .load(backImageUrl)
-                   .placeholder(R.drawable.folder_up)
-                   .into(image);
+            ImageLoader.with(getContext())
+                       .load(backImageUrl)
+                       .placeholder(R.drawable.folder_up)
+                       .into(image);
         }
     }
 
     public void updateFromFilter(FilterDetail filter) {
         final String backText = getResources()
             .getString(R.string.home_move_up_to_filter, filter.title);
         title.setText(backText);
     }
--- a/mobile/android/base/home/PanelItemView.java
+++ b/mobile/android/base/home/PanelItemView.java
@@ -63,19 +63,19 @@ class PanelItemView extends LinearLayout
         int imageIndex = cursor.getColumnIndexOrThrow(HomeItems.IMAGE_URL);
         final String imageUrl = cursor.getString(imageIndex);
 
         // Only try to load the image if the item has define image URL
         final boolean hasImageUrl = !TextUtils.isEmpty(imageUrl);
         image.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE);
 
         if (hasImageUrl) {
-            Picasso.with(getContext())
-                   .load(imageUrl)
-                   .into(image);
+            ImageLoader.with(getContext())
+                       .load(imageUrl)
+                       .into(image);
         }
     }
 
     private static class ArticleItemView extends PanelItemView {
         private ArticleItemView(Context context) {
             super(context, R.layout.panel_article_item);
             setOrientation(LinearLayout.HORIZONTAL);
         }
--- a/mobile/android/base/home/PanelLayout.java
+++ b/mobile/android/base/home/PanelLayout.java
@@ -455,20 +455,20 @@ abstract class PanelLayout extends Frame
             }
 
             final String imageUrl = (emptyViewConfig == null) ? null : emptyViewConfig.getImageUrl();
             final ImageView imageView = (ImageView) view.findViewById(R.id.home_empty_image);
 
             if (TextUtils.isEmpty(imageUrl)) {
                 imageView.setImageResource(R.drawable.icon_home_empty_firefox);
             } else {
-                Picasso.with(getContext())
-                       .load(imageUrl)
-                       .error(R.drawable.icon_home_empty_firefox)
-                       .into(imageView);
+                ImageLoader.with(getContext())
+                           .load(imageUrl)
+                           .error(R.drawable.icon_home_empty_firefox)
+                           .into(imageView);
             }
 
             viewState.setEmptyView(view);
         }
 
         return view;
     }
 
--- a/mobile/android/base/home/TopSitesGridItemView.java
+++ b/mobile/android/base/home/TopSitesGridItemView.java
@@ -1,25 +1,34 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import com.squareup.picasso.Picasso;
+import com.squareup.picasso.Callback;
 
 import org.mozilla.gecko.db.BrowserContract.TopSites;
+import org.mozilla.gecko.db.URLMetadata;
 import org.mozilla.gecko.favicons.Favicons;
+import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UiAsyncTask;
 
 import android.content.Context;
+import android.content.ContentResolver;
 import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.support.v4.content.AsyncTaskLoader;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.widget.ImageView;
 import android.widget.ImageView.ScaleType;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 /**
  * A view that displays the thumbnail and the title/url for a top/pinned site.
@@ -31,16 +40,17 @@ public class TopSitesGridItemView extend
     private static final String LOGTAG = "GeckoTopSitesGridItemView";
 
     // Empty state, to denote there is no valid url.
     private static final int[] STATE_EMPTY = { android.R.attr.state_empty };
 
     private static final ScaleType SCALE_TYPE_FAVICON   = ScaleType.CENTER;
     private static final ScaleType SCALE_TYPE_RESOURCE  = ScaleType.CENTER;
     private static final ScaleType SCALE_TYPE_THUMBNAIL = ScaleType.CENTER_CROP;
+    private static final ScaleType SCALE_TYPE_URL       = ScaleType.CENTER_INSIDE;
 
     // Child views.
     private final TextView mTitleView;
     private final TopSitesThumbnailView mThumbnailView;
 
     // Data backing this view.
     private String mTitle;
     private String mUrl;
@@ -134,56 +144,60 @@ public class TopSitesGridItemView extend
     }
 
     public void blankOut() {
         mUrl = "";
         mTitle = "";
         updateType(TopSites.TYPE_BLANK);
         updateTitleView();
         setLoadId(Favicons.NOT_LOADING);
-        Picasso.with(getContext()).cancelRequest(mThumbnailView);
+        ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
         displayThumbnail(R.drawable.top_site_add);
 
     }
 
     public void markAsDirty() {
         mIsDirty = true;
     }
 
     /**
      * Updates the title, URL, and pinned state of this view.
      *
      * Also resets our loadId to NOT_LOADING.
      *
      * Returns true if any fields changed.
      */
-    public boolean updateState(final String title, final String url, final int type, final Bitmap thumbnail) {
+    public boolean updateState(final String title, final String url, final int type, final TopSitesPanel.ThumbnailInfo thumbnail) {
         boolean changed = false;
         if (mUrl == null || !mUrl.equals(url)) {
             mUrl = url;
             changed = true;
         }
 
         if (mTitle == null || !mTitle.equals(title)) {
             mTitle = title;
             changed = true;
         }
 
         if (thumbnail != null) {
-            displayThumbnail(thumbnail);
+            if (thumbnail.imageUrl != null) {
+                displayThumbnail(thumbnail.imageUrl, thumbnail.bgColor);
+            } else if (thumbnail.bitmap != null) {
+                displayThumbnail(thumbnail.bitmap);
+            }
         } else if (changed) {
             // Because we'll have a new favicon or thumbnail arriving shortly, and
             // we need to not reject it because we already had a thumbnail.
             mThumbnailSet = false;
         }
 
         if (changed) {
             updateTitleView();
             setLoadId(Favicons.NOT_LOADING);
-            Picasso.with(getContext()).cancelRequest(mThumbnailView);
+            ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
         }
 
         if (updateType(type)) {
             changed = true;
         }
 
         // The dirty state forces the state update to return true
         // so that the adapter loads favicons once the thumbnails
@@ -214,39 +228,39 @@ public class TopSitesGridItemView extend
     public void displayThumbnail(Bitmap thumbnail) {
         if (thumbnail == null) {
             // Show a favicon based view instead.
             displayThumbnail(R.drawable.favicon);
             return;
         }
         mThumbnailSet = true;
         Favicons.cancelFaviconLoad(mLoadId);
-        Picasso.with(getContext()).cancelRequest(mThumbnailView);
+        ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
 
         mThumbnailView.setScaleType(SCALE_TYPE_THUMBNAIL);
         mThumbnailView.setImageBitmap(thumbnail);
         mThumbnailView.setBackgroundDrawable(null);
     }
 
     /**
      * Display the thumbnail from a URL.
      *
      * @param imageUrl URL of the image to show.
      * @param bgColor background color to use in the view.
      */
-    public void displayThumbnail(String imageUrl, int bgColor) {
-        mThumbnailView.setScaleType(SCALE_TYPE_RESOURCE);
+    public void displayThumbnail(final String imageUrl, final int bgColor) {
+        mThumbnailView.setScaleType(SCALE_TYPE_URL);
         mThumbnailView.setBackgroundColor(bgColor);
         mThumbnailSet = true;
 
-        Picasso.with(getContext())
-               .load(imageUrl)
-               .noFade()
-               .error(R.drawable.favicon)
-               .into(mThumbnailView);
+        ImageLoader.with(getContext())
+                   .load(imageUrl)
+                   .noFade()
+                   .error(R.drawable.favicon)
+                   .into(mThumbnailView);
     }
 
     public void displayFavicon(Bitmap favicon, String faviconURL, int expectedLoadId) {
         if (mLoadId != Favicons.NOT_LOADING &&
             mLoadId != expectedLoadId) {
             // View recycled.
             return;
         }
--- a/mobile/android/base/home/TopSitesPanel.java
+++ b/mobile/android/base/home/TopSitesPanel.java
@@ -3,41 +3,48 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserContract.TopSites;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.URLMetadata;
+import org.mozilla.gecko.db.URLMetadataTable;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener;
 import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener;
 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
+import static org.mozilla.gecko.db.URLMetadataTable.TILE_IMAGE_URL_COLUMN;
+import static org.mozilla.gecko.db.URLMetadataTable.TILE_COLOR_COLUMN;
+
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.AsyncTaskLoader;
 import android.support.v4.content.Loader;
 import android.support.v4.widget.CursorAdapter;
@@ -416,17 +423,17 @@ public class TopSitesPanel extends HomeF
             });
         }
     }
 
     private void updateUiFromCursor(Cursor c) {
         mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries);
     }
 
-    private void updateUiWithThumbnails(Map<String, Bitmap> thumbnails) {
+    private void updateUiWithThumbnails(Map<String, ThumbnailInfo> thumbnails) {
         if (mGridAdapter != null) {
             mGridAdapter.updateThumbnails(thumbnails);
         }
 
         // Once thumbnails have finished loading, the UI is ready. Reset
         // Gecko to normal priority.
         ThreadUtils.resetGeckoPriority();
     }
@@ -481,17 +488,17 @@ public class TopSitesPanel extends HomeF
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
             return LayoutInflater.from(context).inflate(R.layout.bookmark_item_row, parent, false);
         }
     }
 
     public class TopSitesGridAdapter extends CursorAdapter {
         // Cache to store the thumbnails.
         // Ensure that this is only accessed from the UI thread.
-        private Map<String, Bitmap> mThumbnails;
+        private Map<String, ThumbnailInfo> mThumbnailInfos;
 
         public TopSitesGridAdapter(Context context, Cursor cursor) {
             super(context, cursor, 0);
         }
 
         @Override
         public int getCount() {
             return Math.min(mMaxGridEntries, super.getCount());
@@ -504,18 +511,18 @@ public class TopSitesPanel extends HomeF
             return;
         }
 
         /**
          * Update the thumbnails returned by the db.
          *
          * @param thumbnails A map of urls and their thumbnail bitmaps.
          */
-        public void updateThumbnails(Map<String, Bitmap> thumbnails) {
-            mThumbnails = thumbnails;
+        public void updateThumbnails(Map<String, ThumbnailInfo> thumbnails) {
+            mThumbnailInfos = thumbnails;
 
             final int count = mGrid.getChildCount();
             for (int i = 0; i < count; i++) {
                 TopSitesGridItemView gridItem = (TopSitesGridItemView) mGrid.getChildAt(i);
 
                 // All the views have already got their initial state at this point.
                 // This will force each view to load favicons for the missing
                 // thumbnails if necessary.
@@ -535,17 +542,17 @@ public class TopSitesPanel extends HomeF
 
             // If there is no url, then show "add bookmark".
             if (type == TopSites.TYPE_BLANK) {
                 view.blankOut();
                 return;
             }
 
             // Show the thumbnail, if any.
-            Bitmap thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null);
+            ThumbnailInfo thumbnail = (mThumbnailInfos != null ? mThumbnailInfos.get(url) : null);
 
             // Debounce bindView calls to avoid redundant redraws and favicon
             // fetches.
             final boolean updated = view.updateState(title, url, type, thumbnail);
 
             // Thumbnails are delivered late, so we can't short-circuit any
             // sooner than this. But we can avoid a duplicate favicon
             // fetch...
@@ -563,17 +570,17 @@ public class TopSitesPanel extends HomeF
             if (!TextUtils.isEmpty(imageUrl)) {
                 final int bgColor = BrowserDB.getSuggestedBackgroundColorForUrl(decodedUrl);
                 view.displayThumbnail(imageUrl, bgColor);
                 return;
             }
 
             // If thumbnails are still being loaded, don't try to load favicons
             // just yet. If we sent in a thumbnail, we're done now.
-            if (mThumbnails == null || thumbnail != null) {
+            if (mThumbnailInfos == null || thumbnail != null) {
                 return;
             }
 
             // If we have no thumbnail, attempt to show a Favicon instead.
             LoadIDAwareFaviconLoadedListener listener = new LoadIDAwareFaviconLoadedListener(view);
             final int loadId = Favicons.getSizedFaviconForPageFromLocal(url, listener);
             if (loadId == Favicons.LOADED) {
                 // Great!
@@ -660,17 +667,17 @@ public class TopSitesPanel extends HomeF
                     continue;
                 }
 
                 urls.add(url);
             } while (i++ < mMaxGridEntries && c.moveToNext());
 
             if (urls.isEmpty()) {
                 // Short-circuit empty results to the UI.
-                updateUiWithThumbnails(new HashMap<String, Bitmap>());
+                updateUiWithThumbnails(new HashMap<String, ThumbnailInfo>());
                 return;
             }
 
             Bundle bundle = new Bundle();
             bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls);
             getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks);
         }
 
@@ -681,43 +688,107 @@ public class TopSitesPanel extends HomeF
             }
 
             if (mGridAdapter != null) {
                 mGridAdapter.swapCursor(null);
             }
         }
     }
 
+    static class ThumbnailInfo {
+        public final Bitmap bitmap;
+        public final String imageUrl;
+        public final int bgColor;
+
+        public ThumbnailInfo(final Bitmap bitmap) {
+            this.bitmap = bitmap;
+            this.imageUrl = null;
+            this.bgColor = Color.TRANSPARENT;
+        }
+
+        public ThumbnailInfo(final String imageUrl, final int bgColor) {
+            this.bitmap = null;
+            this.imageUrl = imageUrl;
+            this.bgColor = bgColor;
+        }
+
+        public static ThumbnailInfo fromMetadata(final Map<String, Object> data) {
+            final String imageUrl = (String) data.get(TILE_IMAGE_URL_COLUMN);
+            if (imageUrl == null) {
+                return null;
+            }
+
+            int bgColor = Color.WHITE;
+            final String colorString = (String) data.get(TILE_COLOR_COLUMN);
+            try {
+                bgColor = Color.parseColor(colorString);
+            } catch (Exception ex) {
+            }
+
+            return new ThumbnailInfo(imageUrl, bgColor);
+        }
+    }
+
     /**
      * An AsyncTaskLoader to load the thumbnails from a cursor.
      */
-    private static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, Bitmap>> {
-        private Map<String, Bitmap> mThumbnails;
+    @SuppressWarnings("serial")
+    static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, ThumbnailInfo>> {
+        private Map<String, ThumbnailInfo> mThumbnailInfos;
         private ArrayList<String> mUrls;
 
+        private static final ArrayList<String> COLUMNS = new ArrayList<String>() {{
+            add(TILE_IMAGE_URL_COLUMN);
+            add(TILE_COLOR_COLUMN);
+        }};
+
         public ThumbnailsLoader(Context context, ArrayList<String> urls) {
             super(context);
             mUrls = urls;
         }
 
         @Override
-        public Map<String, Bitmap> loadInBackground() {
+        public Map<String, ThumbnailInfo> loadInBackground() {
+            final Map<String, ThumbnailInfo> thumbnails = new HashMap<String, ThumbnailInfo>();
             if (mUrls == null || mUrls.size() == 0) {
-                return null;
+                return thumbnails;
             }
 
-            // Query the DB for thumbnails.
+            // Query the DB for tile images.
             final ContentResolver cr = getContext().getContentResolver();
-            final Cursor cursor = BrowserDB.getThumbnailsForUrls(cr, mUrls);
+            final Map<String, Map<String, Object>> metadata = URLMetadata.getForUrls(cr, mUrls, COLUMNS);
+
+            // Keep a list of urls that don't have tiles images. We'll use thumbnails for them instead.
+            final List<String> thumbnailUrls;
+            if (metadata != null) {
+                thumbnailUrls = new ArrayList<String>();
 
-            if (cursor == null) {
-                return null;
+                for (String url : metadata.keySet()) {
+                    ThumbnailInfo info = ThumbnailInfo.fromMetadata(metadata.get(url));
+                    if (info == null) {
+                        // If we didn't find metadata, we'll look for a thumbnail for this url.
+                        thumbnailUrls.add(url);
+                        continue;
+                    }
+
+                    thumbnails.put(url, info);
+                }
+            } else {
+                thumbnailUrls = new ArrayList<String>(mUrls);
             }
 
-            final Map<String, Bitmap> thumbnails = new HashMap<String, Bitmap>();
+            if (thumbnailUrls.size() == 0) {
+                return thumbnails;
+            }
+
+            // Query the DB for tile thumbnails.
+            final Cursor cursor = BrowserDB.getThumbnailsForUrls(cr, thumbnailUrls);
+            if (cursor == null) {
+                return thumbnails;
+            }
 
             try {
                 final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL);
                 final int dataIndex = cursor.getColumnIndexOrThrow(Thumbnails.DATA);
 
                 while (cursor.moveToNext()) {
                     String url = cursor.getString(urlIndex);
 
@@ -732,85 +803,85 @@ public class TopSitesPanel extends HomeF
                     // Our thumbnails are never null, so if we get a null decoded
                     // bitmap, it's because we hit an OOM or some other disaster.
                     // Give up immediately rather than hammering on.
                     if (bitmap == null) {
                         Log.w(LOGTAG, "Aborting thumbnail load; decode failed.");
                         break;
                     }
 
-                    thumbnails.put(url, bitmap);
+                    thumbnails.put(url, new ThumbnailInfo(bitmap));
                 }
             } finally {
                 cursor.close();
             }
 
             return thumbnails;
         }
 
         @Override
-        public void deliverResult(Map<String, Bitmap> thumbnails) {
+        public void deliverResult(Map<String, ThumbnailInfo> thumbnails) {
             if (isReset()) {
-                mThumbnails = null;
+                mThumbnailInfos = null;
                 return;
             }
 
-            mThumbnails = thumbnails;
+            mThumbnailInfos = thumbnails;
 
             if (isStarted()) {
                 super.deliverResult(thumbnails);
             }
         }
 
         @Override
         protected void onStartLoading() {
-            if (mThumbnails != null) {
-                deliverResult(mThumbnails);
+            if (mThumbnailInfos != null) {
+                deliverResult(mThumbnailInfos);
             }
 
-            if (takeContentChanged() || mThumbnails == null) {
+            if (takeContentChanged() || mThumbnailInfos == null) {
                 forceLoad();
             }
         }
 
         @Override
         protected void onStopLoading() {
             cancelLoad();
         }
 
         @Override
-        public void onCanceled(Map<String, Bitmap> thumbnails) {
-            mThumbnails = null;
+        public void onCanceled(Map<String, ThumbnailInfo> thumbnails) {
+            mThumbnailInfos = null;
         }
 
         @Override
         protected void onReset() {
             super.onReset();
 
             // Ensure the loader is stopped.
             onStopLoading();
 
-            mThumbnails = null;
+            mThumbnailInfos = null;
         }
     }
 
     /**
      * Loader callbacks for the thumbnails on TopSitesGridView.
      */
-    private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, Bitmap>> {
+    private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, ThumbnailInfo>> {
         @Override
-        public Loader<Map<String, Bitmap>> onCreateLoader(int id, Bundle args) {
+        public Loader<Map<String, ThumbnailInfo>> onCreateLoader(int id, Bundle args) {
             return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY));
         }
 
         @Override
-        public void onLoadFinished(Loader<Map<String, Bitmap>> loader, Map<String, Bitmap> thumbnails) {
+        public void onLoadFinished(Loader<Map<String, ThumbnailInfo>> loader, Map<String, ThumbnailInfo> thumbnails) {
             updateUiWithThumbnails(thumbnails);
         }
 
         @Override
-        public void onLoaderReset(Loader<Map<String, Bitmap>> loader) {
+        public void onLoaderReset(Loader<Map<String, ThumbnailInfo>> loader) {
             if (mGridAdapter != null) {
                 mGridAdapter.updateThumbnails(null);
             }
         }
     }
 }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -133,34 +133,38 @@ gbjar.sources += [
     'BrowserApp.java',
     'BrowserLocaleManager.java',
     'ContactService.java',
     'ContextGetter.java',
     'CustomEditText.java',
     'DataReportingNotification.java',
     'db/AbstractPerProfileDatabaseProvider.java',
     'db/AbstractTransactionalProvider.java',
+    'db/BaseTable.java',
     'db/BrowserContract.java',
     'db/BrowserDatabaseHelper.java',
     'db/BrowserDB.java',
     'db/BrowserProvider.java',
     'db/DBUtils.java',
     'db/FormHistoryProvider.java',
     'db/HomeProvider.java',
     'db/LocalBrowserDB.java',
     'db/PasswordsProvider.java',
     'db/PerProfileDatabaseProvider.java',
     'db/PerProfileDatabases.java',
     'db/ReadingListProvider.java',
     'db/SearchHistoryProvider.java',
     'db/SharedBrowserDatabaseProvider.java',
     'db/SQLiteBridgeContentProvider.java',
     'db/SuggestedSites.java',
+    'db/Table.java',
     'db/TabsProvider.java',
     'db/TopSitesCursorWrapper.java',
+    'db/URLMetadata.java',
+    'db/URLMetadataTable.java',
     'distribution/Distribution.java',
     'distribution/ReferrerDescriptor.java',
     'distribution/ReferrerReceiver.java',
     'DoorHangerPopup.java',
     'DynamicToolbar.java',
     'EditBookmarkDialog.java',
     'EventDispatcher.java',
     'favicons/cache/FaviconCache.java',
@@ -269,16 +273,17 @@ gbjar.sources += [
     'home/HomeConfigPrefsBackend.java',
     'home/HomeContextMenuInfo.java',
     'home/HomeFragment.java',
     'home/HomeListView.java',
     'home/HomePager.java',
     'home/HomePagerTabStrip.java',
     'home/HomePanelPicker.java',
     'home/HomePanelsManager.java',
+    'home/ImageLoader.java',
     'home/MultiTypeCursorAdapter.java',
     'home/PanelAuthCache.java',
     'home/PanelAuthLayout.java',
     'home/PanelBackItemView.java',
     'home/PanelGridView.java',
     'home/PanelInfoManager.java',
     'home/PanelItemView.java',
     'home/PanelLayout.java',
@@ -526,17 +531,17 @@ ANDROID_RES_DIRS += [
 ]
 
 ANDROID_GENERATED_RESFILES += [
     'res/raw/suggestedsites.json',
     'res/values/strings.xml',
 ]
 
 for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_LINKER_EXTRACT', 'MOZILLA_OFFICIAL', 'MOZ_DEBUG',
-            'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES'):
+            'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER'):
     if CONFIG[var]:
         DEFINES[var] = 1
 
 for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL'):
     if CONFIG[var]:
         DEFINES[var] = CONFIG[var]
 
 for var in ('ANDROID_PACKAGE_NAME', 'ANDROID_CPU_ARCH', 'CPU_ARCH',
@@ -640,17 +645,21 @@ omnijar.package_name = 'org.mozilla.fenn
 # triggers a new build (of FennecOmnijar) when something actually changes, so
 # we're not constantly rebuilding the FennecOmnijar (or Fennec) project.
 omnijar.recursive_make_targets += [TOPOBJDIR + '/dist/fennec/assets/omni.ja']
 for d in ['app', 'chrome', 'components', 'locales', 'modules', 'themes']:
     omnijar.add_classpathentry(d, TOPSRCDIR + '/mobile/android/' + d, dstdir=d)
 
 # The omnijar is included in the Fennec APK (although it's empty,
 # having no resources, assets, or Java code).
-main.included_projects += [omnijar.name]
+main.included_projects += ['../' + omnijar.name]
 
 if CONFIG['MOZ_CRASHREPORTER']:
     crashreporter = add_android_eclipse_library_project('FennecResourcesCrashReporter')
     crashreporter.package_name = 'org.mozilla.fennec.resources.crashreporter'
     crashreporter.res = SRCDIR + '/crashreporter/res'
     crashreporter.included_projects += ['../' + resources.name]
 
     main.included_projects += ['../' + crashreporter.name]
+
+if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
+    main.included_projects += ['../FennecStumbler']
+    main.referenced_projects += ['../FennecStumbler']
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -258,16 +258,18 @@ OnSharedPreferenceChangeListener
 
             // Don't finish the activity -- we just reloaded all of the
             // individual parts! -- but when it returns, make sure that the
             // caller knows the locale changed.
             setResult(RESULT_CODE_LOCALE_DID_CHANGE);
             return;
         }
 
+        refreshSuggestedSites();
+
         // Cause the current fragment to redisplay, the hard way.
         // This avoids nonsense with trying to reach inside fragments and force them
         // to redisplay themselves.
         // We also don't need to update the title.
         final Intent intent = (Intent) getIntent().clone();
         intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
         startActivityForResultChoosingTransition(intent, REQUEST_CODE_PREF_SCREEN);
 
@@ -942,16 +944,24 @@ OnSharedPreferenceChangeListener
                     }
                 });
             }
         });
 
         return true;
     }
 
+    private void refreshSuggestedSites() {
+        final ContentResolver cr = getApplicationContext().getContentResolver();
+
+        // This will force all active suggested sites cursors
+        // to request a refresh (e.g. cursor loaders).
+        cr.notifyChange(SuggestedSites.CONTENT_URI, null);
+    }
+
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
 
         Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
 
         if (lastLocale.equals(newConfig.locale)) {
             Log.d(LOGTAG, "Old locale same as new locale. Short-circuiting.");
@@ -977,21 +987,17 @@ OnSharedPreferenceChangeListener
      * changing multiple times.
      */
     @Override
     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
         if (PREFS_BROWSER_LOCALE.equals(key)) {
             onLocaleSelected(BrowserLocaleManager.getLanguageTag(lastLocale),
                              sharedPreferences.getString(key, null));
         } else if (PREFS_SUGGESTED_SITES.equals(key)) {
-            final ContentResolver cr = getApplicationContext().getContentResolver();
-
-            // This will force all active suggested sites cursors
-            // to request a refresh (e.g. cursor loaders).
-            cr.notifyChange(SuggestedSites.CONTENT_URI, null);
+            refreshSuggestedSites();
         }
     }
 
     @Override
     public boolean onPreferenceChange(Preference preference, Object newValue) {
         final String prefName = preference.getKey();
         if (PREFS_MP_ENABLED.equals(prefName)) {
             showDialog((Boolean) newValue ? DIALOG_CREATE_MASTER_PASSWORD : DIALOG_REMOVE_MASTER_PASSWORD);
--- a/mobile/android/base/tabspanel/TabsPanel.java
+++ b/mobile/android/base/tabspanel/TabsPanel.java
@@ -441,16 +441,17 @@ public class TabsPanel extends LinearLay
                 mFooter.setVisibility(View.VISIBLE);
 
             mAddTab.setVisibility(View.VISIBLE);
             mAddTab.setImageLevel(index);
 
 
             if (!HardwareUtils.hasMenuButton()) {
                 mMenuButton.setVisibility(View.VISIBLE);
+                mMenuButton.setEnabled(true);
                 mPopupMenu.setAnchor(mMenuButton);
             } else {
                 mPopupMenu.setAnchor(mAddTab);
             }
         }
 
         if (isSideBar()) {
             if (showAnimation)
--- a/mobile/android/base/tests/testAddSearchEngine.java
+++ b/mobile/android/base/tests/testAddSearchEngine.java
@@ -16,17 +16,17 @@ import android.widget.ListView;
  * Test adding a search engine from an input field context menu.
  * 1. Get the number of existing search engines from the SearchEngine:Data event and as displayed in about:home.
  * 2. Load a page with a text field, open the context menu and add a search engine from the page.
  * 3. Get the number of search engines after adding the new one and verify it has increased by 1.
  */
 public class testAddSearchEngine extends AboutHomeTest {
     private final int MAX_WAIT_TEST_MS = 5000;
     private final String SEARCH_TEXT = "Firefox for Android";
-    private final String ADD_SEARCHENGINE_OPTION_TEXT = "Add Search Engine";
+    private final String ADD_SEARCHENGINE_OPTION_TEXT = "Add as Search Engine";
 
     public void testAddSearchEngine() {
         String blankPageURL = getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL);
         String searchEngineURL = getAbsoluteUrl(StringHelper.ROBOCOP_SEARCH_URL);
 
         blockForGeckoReady();
         int height = mDriver.getGeckoTop() + 150;
         int width = mDriver.getGeckoLeft() + 150;
--- a/mobile/android/base/util/ThreadUtils.java
+++ b/mobile/android/base/util/ThreadUtils.java
@@ -124,16 +124,20 @@ public final class ThreadUtils {
         assertNotOnThread(getUiThread(), AssertBehavior.THROW);
     }
 
     @RobocopTarget
     public static void assertOnGeckoThread() {
         assertOnThread(sGeckoThread, AssertBehavior.THROW);
     }
 
+    public static void assertNotOnGeckoThread() {
+        assertNotOnThread(sGeckoThread, AssertBehavior.THROW);
+    }
+
     public static void assertOnBackgroundThread() {
         assertOnThread(getBackgroundThread(), AssertBehavior.THROW);
     }
 
     public static void assertOnThread(final Thread expectedThread) {
         assertOnThread(expectedThread, AssertBehavior.THROW);
     }
 
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -382,16 +382,18 @@ var SelectionHandler = {
     return true;
   },
 
   /*
    * Called to perform a selection operation, given a target element, selection method, starting point etc.
    */
   _performSelection: function sh_performSelection(aOptions) {
     if (aOptions.mode == this.SELECT_AT_POINT) {
+      // Clear any ranges selected outside SelectionHandler, by code such as Find-In-Page.
+      this._contentWindow.getSelection().removeAllRanges();
       return this._domWinUtils.selectAtPoint(aOptions.x, aOptions.y, Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE);
     }
 
     if (aOptions.mode != this.SELECT_ALL) {
       Cu.reportError("SelectionHandler.js: _performSelection() Invalid selection mode " + aOptions.mode);
       return false;
     }
 
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -112,16 +112,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 #endif
   ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
   ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
   ["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
   ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
   ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
   ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
   ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
+  ["Notifications", ["Notification:Event"], "chrome://browser/content/Notifications.jsm"],
 ].forEach(function (aScript) {
   let [name, notifications, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
   let observer = (s, t, d) => {
@@ -3161,16 +3162,17 @@ Tab.prototype = {
                 Ci.nsIWebProgress.NOTIFY_SECURITY;
     this.browser.addProgressListener(this, flags);
     this.browser.sessionHistory.addSHistoryListener(this);
 
     this.browser.addEventListener("DOMContentLoaded", this, true);
     this.browser.addEventListener("DOMFormHasPassword", this, true);
     this.browser.addEventListener("DOMLinkAdded", this, true);
     this.browser.addEventListener("DOMLinkChanged", this, true);
+    this.browser.addEventListener("DOMMetaAdded", this, false);
     this.browser.addEventListener("DOMTitleChanged", this, true);
     this.browser.addEventListener("DOMWindowClose", this, true);
     this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
     this.browser.addEventListener("DOMAutoComplete", this, true);
     this.browser.addEventListener("blur", this, true);
     this.browser.addEventListener("scroll", this, true);
     this.browser.addEventListener("MozScrolledAreaChanged", this, true);
     this.browser.addEventListener("pageshow", this, true);
@@ -3334,16 +3336,17 @@ Tab.prototype = {
 
     this.browser.removeProgressListener(this);
     this.browser.sessionHistory.removeSHistoryListener(this);
 
     this.browser.removeEventListener("DOMContentLoaded", this, true);
     this.browser.removeEventListener("DOMFormHasPassword", this, true);
     this.browser.removeEventListener("DOMLinkAdded", this, true);
     this.browser.removeEventListener("DOMLinkChanged", this, true);
+    this.browser.removeEventListener("DOMMetaAdded", this, false);
     this.browser.removeEventListener("DOMTitleChanged", this, true);
     this.browser.removeEventListener("DOMWindowClose", this, true);
     this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
     this.browser.removeEventListener("DOMAutoComplete", this, true);
     this.browser.removeEventListener("blur", this, true);
     this.browser.removeEventListener("scroll", this, true);
     this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
     this.browser.removeEventListener("pageshow", this, true);
@@ -3677,16 +3680,35 @@ Tab.prototype = {
       // If the page changed size twice since we last measured the viewport and
       // the latest size change reveals we don't need to remeasure, cancel any
       // pending remeasure.
       clearTimeout(this.viewportMeasureCallback);
       this.viewportMeasureCallback = null;
     }
   },
 
+  // These constants are used to prioritize high quality metadata over low quality data, so that
+  // we can collect data as we find meta tags, and replace low quality metadata with higher quality
+  // matches. For instance a msApplicationTile icon is a better tile image than an og:image tag.
+  METADATA_GOOD_MATCH: 10,
+  METADATA_NORMAL_MATCH: 1,
+
+  addMetadata: function(type, value, quality = 1) {
+    if (!this.metatags) {
+      this.metatags = {
+        url: this.browser.currentURI.spec
+      };
+    }
+
+    if (!this.metatags[type] || this.metatags[type + "_quality"] < quality) {
+      this.metatags[type] = value;
+      this.metatags[type + "_quality"] = quality;
+    }
+  },
+
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "DOMContentLoaded": {
         let target = aEvent.originalTarget;
 
         // ignore on frames and other documents
         if (target != this.browser.contentDocument)
           return;
@@ -3711,19 +3733,22 @@ Tab.prototype = {
           errorType = "blocked"
         else if (docURI.startsWith("about:neterror"))
           errorType = "neterror";
 
         sendMessageToJava({
           type: "DOMContentLoaded",
           tabID: this.id,
           bgColor: backgroundColor,
-          errorType: errorType
+          errorType: errorType,
+          metadata: this.metatags
         });
 
+        this.metatags = null;
+
         // Attach a listener to watch for "click" events bubbling up from error
         // pages and other similar page. This lets us fix bugs like 401575 which
         // require error page UI to do privileged things, without letting error
         // pages have any privilege themselves.
         if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) {
           this.browser.addEventListener("click", ErrorPageEventHandler, true);
           let listener = function() {
             this.browser.removeEventListener("click", ErrorPageEventHandler, true);
@@ -3747,16 +3772,31 @@ Tab.prototype = {
         break;
       }
 
       case "DOMFormHasPassword": {
         LoginManagerContent.onFormPassword(aEvent);
         break;
       }
 
+      case "DOMMetaAdded":
+        let target = aEvent.originalTarget;
+        let browser = BrowserApp.getBrowserForDocument(target.ownerDocument);
+
+        switch (target.name) {
+          case "msapplication-TileImage":
+            this.addMetadata("tileImage", browser.currentURI.resolve(target.content), this.METADATA_GOOD_MATCH);
+            break;
+          case "msapplication-TileColor":
+            this.addMetadata("tileColor", target.content, this.METADATA_GOOD_MATCH);
+            break;
+        }
+
+        break;
+
       case "DOMLinkAdded":
       case "DOMLinkChanged": {
         let target = aEvent.originalTarget;
         if (!target.href || target.disabled)
           return;
 
         // Ignore on frames and other documents
         if (target.ownerDocument != this.browser.contentDocument)
@@ -6830,17 +6870,17 @@ var SearchEngines = {
         // POST      multipart/form-data                 NO
         // POST      everything else                     YES
         return (method == "GET" || method == "") ||
                (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
       }
     };
     SelectionHandler.addAction({
       id: "search_add_action",
-      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine"),
+      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine2"),
       icon: "drawable://ab_add_search_engine",
       selector: filter,
       action: function(aElement) {
         UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
         SearchEngines.addEngine(aElement);
       }
     });
   },
@@ -7035,17 +7075,17 @@ var SearchEngines = {
               formData.push({ name: escapedName, value: escapedValue });
               break;
             }
           }
       }
     }
 
     // prompt user for name of search engine
-    let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine");
+    let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine2");
     let title = { value: (aElement.ownerDocument.title || docURI.host) };
     if (!Services.prompt.prompt(null, promptTitle, null, title, null, {}))
       return;
 
     // fetch the favicon for this page
     let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
     let mDBConn = Services.storage.openDatabase(dbFile);
     let stmts = [];
--- a/mobile/android/chrome/content/downloads.js
+++ b/mobile/android/chrome/content/downloads.js
@@ -21,16 +21,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 var Downloads = {
   _initialized: false,
   _dlmgr: null,
   _progressAlert: null,
   _privateDownloads: [],
   _showingPrompt: false,
   _downloadsIdMap: {},
+  _notificationKey: "downloads",
 
   _getLocalFile: function dl__getLocalFile(aFileURI) {
     // if this is a URL, get the file from that
     // XXX it's possible that using a null char-set here is bad
     const fileUrl = Services.io.newURI(aFileURI, null, null).QueryInterface(Ci.nsIFileURL);
     return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
   },
 
@@ -39,16 +40,40 @@ var Downloads = {
       return;
     this._initialized = true;
 
     // Monitor downloads and display alerts
     this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
     this._progressAlert = new AlertDownloadProgressListener();
     this._dlmgr.addPrivacyAwareListener(this._progressAlert);
     Services.obs.addObserver(this, "last-pb-context-exited", true);
+
+    // All click, cancel, and button presses will be handled by this handler as part of the Notifications callback API.
+    Notifications.registerHandler(this._notificationKey, this);
+  },
+
+  onClick: function(aCookie) {
+    Services.console.logStringMessage("Onclick " + aCookie);
+    Downloads.clickCallback(aCookie);
+  },
+
+  onCancel: function(aCookie) {
+    Services.console.logStringMessage("onCancel " + aCookie);
+    Downloads.notificationCanceledCallback(aCookie);
+  },
+
+  onButtonClick: function(aButtonId, aCookie) {
+    Services.console.logStringMessage("onButtonClick " + aCookie);
+    if (aButtonId === PAUSE_BUTTON.buttonId) {
+      Downloads.pauseClickCallback(aCookie);
+    } else if (aButtonId === RESUME_BUTTON.buttonId) {
+      Downloads.resumeClickCallback(aCookie);
+    } else if (aButtonId === CANCEL_BUTTON.buttonId) {
+      Downloads.cancelClickCallback(aCookie);
+    }
   },
 
   openDownload: function dl_openDownload(aDownload) {
     let fileUri = aDownload.target.spec;
     let guid = aDownload.guid;
     let f = this._getLocalFile(fileUri);
     try {
       f.launch();
@@ -115,20 +140,18 @@ var Downloads = {
 
   cancelClickCallback: function dl_buttonPauseCallback(aDownloadId) {
     this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
           if (Components.isSuccessCode(status))
             this.cancelDownload(download);
         }).bind(this));
   },
 
-  notificationCanceledCallback: function dl_notifCancelCallback(aId, aDownloadId) {
-    let notificationId = this._downloadsIdMap[aDownloadId];
-    if (notificationId && notificationId == aId)
-      delete this._downloadsIdMap[aDownloadId];
+  notificationCanceledCallback: function dl_notifCancelCallback(aDownloadId) {
+    delete this._downloadsIdMap[aDownloadId];
   },
 
   createNotification: function dl_createNotif(aDownload, aOptions) {
     let notificationId = Notifications.create(aOptions);
     this._downloadsIdMap[aDownload.guid] = notificationId;
   },
 
   updateNotification: function dl_updateNotif(aDownload, aOptions) {
@@ -164,52 +187,38 @@ var Downloads = {
     return this;
   }
 };
 
 const PAUSE_BUTTON = {
   buttonId: "pause",
   title : Strings.browser.GetStringFromName("alertDownloadsPause"),
   icon : URI_PAUSE_ICON,
-  onClicked: function (aId, aCookie) {
-    Downloads.pauseClickCallback(aCookie);
-  }
 };
 
 const CANCEL_BUTTON = {
   buttonId: "cancel",
   title : Strings.browser.GetStringFromName("alertDownloadsCancel"),
   icon : URI_CANCEL_ICON,
-  onClicked: function (aId, aCookie) {
-    Downloads.cancelClickCallback(aCookie);
-  }
 };
 
 const RESUME_BUTTON = {
   buttonId: "resume",
   title : Strings.browser.GetStringFromName("alertDownloadsResume"),
   icon: URI_RESUME_ICON,
-  onClicked: function (aId, aCookie) {
-    Downloads.resumeClickCallback(aCookie);
-  }
 };
 
 function DownloadNotifOptions (aDownload, aTitle, aMessage) {
   this.icon = URI_GENERIC_ICON_DOWNLOAD;
-  this.onCancel = function (aId, aCookie) {
-    Downloads.notificationCanceledCallback(aId, aCookie);
-  }
-  this.onClick = function (aId, aCookie) {
-    Downloads.clickCallback(aCookie);
-  }
   this.title = aTitle;
   this.message = aMessage;
   this.buttons = null;
   this.cookie = aDownload.guid;
   this.persistent = true;
+  this.handlerKey = Downloads._notificationKey;
 }
 
 function DownloadProgressNotifOptions (aDownload, aButtons) {
   DownloadNotifOptions.apply(this, [aDownload, aDownload.displayName, aDownload.percentComplete + "%"]);
   this.ongoing = true;
   this.progress = aDownload.percentComplete;
   this.buttons = aButtons;
 }
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -72,8 +72,11 @@ MOZ_DEVICES=1
 # Enable second screen using native Android libraries
 MOZ_NATIVE_DEVICES=
 
 # Mark as WebGL conformant
 MOZ_WEBGL_CONFORMANT=1
 
 # Don't enable the Search Activity.
 # MOZ_ANDROID_SEARCH_ACTIVITY=1
+
+# Don't enable the Mozilla Location Service stumbler.
+# MOZ_ANDROID_MLS_STUMBLER=1
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -194,17 +194,17 @@ contextmenu.fullScreen=Full Screen
 contextmenu.copyImageLocation=Copy Image Location
 contextmenu.shareImage=Share Image
 # LOCALIZATION NOTE (contextmenu.search):
 # The label of the contextmenu item which allows you to search with your default search engine for
 # the text you have selected. %S is the name of the search engine. For example, "Google".
 contextmenu.search=%S Search
 contextmenu.saveImage=Save Image
 contextmenu.setImageAs=Set Image As
-contextmenu.addSearchEngine=Add Search Engine
+contextmenu.addSearchEngine2=Add as Search Engine
 contextmenu.playMedia=Play
 contextmenu.pauseMedia=Pause
 contextmenu.shareMedia=Share Video
 contextmenu.showControls2=Show Controls
 contextmenu.mute=Mute
 contextmenu.unmute=Unmute
 contextmenu.saveVideo=Save Video
 contextmenu.saveAudio=Save Audio
--- a/mobile/android/modules/Notifications.jsm
+++ b/mobile/android/modules/Notifications.jsm
@@ -9,17 +9,18 @@ let Ci = Components.interfaces;
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 this.EXPORTED_SYMBOLS = ["Notifications"];
 
 function log(msg) {
   // Services.console.logStringMessage(msg);
 }
 
-var _notificationsMap = {};
+let _notificationsMap = {};
+let _handlersMap = {};
 
 function Notification(aId, aOptions) {
   this._id = aId;
   this._when = (new Date).getTime();
   this.fillWithOptions(aOptions);
 }
 
 Notification.prototype = {
@@ -75,46 +76,52 @@ Notification.prototype = {
     else
       this._onClick = null;
 
     if ("cookie" in aOptions && aOptions.cookie != null)
       this._cookie = aOptions.cookie;
     else
       this._cookie = null;
 
+    if ("handlerKey" in aOptions && aOptions.handlerKey != null)
+      this._handlerKey = aOptions.handlerKey;
+
     if ("persistent" in aOptions && aOptions.persistent != null)
       this._persistent = aOptions.persistent;
     else
       this._persistent = false;
   },
 
   show: function() {
     let msg = {
         type: "Notification:Show",
         id: this._id,
         title: this._title,
         smallIcon: this._icon,
         ongoing: this._ongoing,
         when: this._when,
-        persistent: this._persistent
+        persistent: this._persistent,
     };
 
     if (this._message)
       msg.text = this._message;
 
     if (this._progress) {
       msg.progress_value = this._progress;
       msg.progress_max = 100;
       msg.progress_indeterminate = false;
     } else if (Number.isNaN(this._progress)) {
       msg.progress_value = 0;
       msg.progress_max = 0;
       msg.progress_indeterminate = true;
     }
 
+    if (this._cookie)
+      msg.cookie = JSON.stringify(this._cookie);
+
     if (this._priority)
       msg.priority = this._priority;
 
     if (this._buttons) {
       msg.actions = [];
       let buttonName;
       for (buttonName in this._buttons) {
         let button = this._buttons[buttonName];
@@ -125,48 +132,61 @@ Notification.prototype = {
         };
         msg.actions.push(obj);
       }
     }
 
     if (this._light)
       msg.light = this._light;
 
+    if (this._handlerKey)
+      msg.handlerKey = this._handlerKey;
+
     Services.androidBridge.handleGeckoMessage(msg);
     return this;
   },
 
   cancel: function() {
     let msg = {
-        type: "Notification:Hide",
-        id: this._id
+      type: "Notification:Hide",
+      id: this._id,
+      handlerKey: this._handlerKey,
+      cookie: JSON.stringify(this._cookie),
     };
     Services.androidBridge.handleGeckoMessage(msg);
   }
 }
 
-var Notifications = {
-  _initObserver: function() {
-    if (!this._observerAdded) {
-      Services.obs.addObserver(this, "Notification:Event", true);
-      this._observerAdded = true;
-    }
-  },
-
+let Notifications = {
   get idService() {
     delete this.idService;
     return this.idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
   },
 
+  registerHandler: function(key, handler) {
+    if (!_handlersMap[key]) {
+      _handlersMap[key] = [];
+    }
+    _handlersMap[key].push(handler);
+  },
+
+  unregisterHandler: function(key, handler) {
+    let i = _handlersMap[key].indexOf(handler);
+    if (i > -1) {
+      _handlersMap.splice(i, 1);
+    }
+  },
+
   create: function notif_notify(aOptions) {
-    this._initObserver();
     let id = this.idService.generateUUID().toString();
+
     let notification = new Notification(id, aOptions);
     _notificationsMap[id] = notification;
     notification.show();
+
     return id;
   },
 
   update: function notif_update(aId, aOptions) {
     let notification = _notificationsMap[aId];
     if (!notification)
       throw "Unknown notification id";
     notification.fillWithOptions(aOptions);
@@ -175,43 +195,61 @@ var Notifications = {
 
   cancel: function notif_cancel(aId) {
     let notification = _notificationsMap[aId];
     if (notification)
       notification.cancel();
   },
 
   observe: function notif_observe(aSubject, aTopic, aData) {
+    Services.console.logStringMessage(aTopic + " " + aData);
+
     let data = JSON.parse(aData);
     let id = data.id;
+    let handlerKey = data.handlerKey;
+    let cookie = data.cookie ? JSON.parse(data.cookie) : undefined;
     let notification = _notificationsMap[id];
-    if (!notification) {
-      Services.console.logStringMessage("Notifications.jsm observe: received unknown event id " + id);
-      return;
-    }
 
     switch (data.eventType) {
       case "notification-clicked":
-        if (notification._onClick)
+        if (notification && notification._onClick)
           notification._onClick(id, notification._cookie);
+
+        if (handlerKey) {
+          _handlersMap[handlerKey].forEach(function(handler) {
+            handler.onClick(cookie);
+          });
+        }
+
         break;
-      case "notification-button-clicked": {
-        if (!notification._buttons) {
-          Services.console.logStringMessage("Notifications.jsm: received button clicked event but no buttons are available");
+      case "notification-button-clicked":
+        if (handlerKey) {
+          _handlersMap[handlerKey].forEach(function(handler) {
+            handler.onButtonClick(data.buttonId, cookie);
+          });
+        }
+
+        if (notification && !notification._buttons) {
           break;
         }
 
         let button = notification._buttons[data.buttonId];
-        if (button)
+        if (button) {
           button.onClicked(id, notification._cookie);
         }
         break;
       case "notification-cleared":
       case "notification-closed":
-        if (notification._onCancel)
+        if (handlerKey) {
+          _handlersMap[handlerKey].forEach(function(handler) {
+            handler.onCancel(cookie);
+          });
+        }
+
+        if (notification && notification._onCancel)
           notification._onCancel(id, notification._cookie);
         delete _notificationsMap[id]; // since the notification was dismissed, we no longer need to hold a reference.
         break;
     }
   },
 
   QueryInterface: function (aIID) {
     if (!aIID.equals(Ci.nsISupports) &&
--- a/mobile/android/moz.build
+++ b/mobile/android/moz.build
@@ -4,25 +4,29 @@
 # 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/.
 
 CONFIGURE_SUBST_FILES += ['installer/Makefile']
 
 DIRS += [
     '../locales',
     'locales',
+    'stumbler',
     'base',
     'chrome',
     'components',
     'modules',
     'themes/core',
     'app',
     'fonts',
     'geckoview_library',
     'extensions',
 ]
 
+if not CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
+    DIRS.remove('stumbler')
+
 if not CONFIG['LIBXUL_SDK']:
     PARALLEL_DIRS += ['../../xulrunner/tools/redit']
 
 TEST_DIRS += [
     'tests',
 ]
--- a/mobile/android/search/java/org/mozilla/search/MainActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/MainActivity.java
@@ -1,15 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.search;
 
-import android.net.Uri;
 import android.os.Bundle;
 import android.support.v4.app.FragmentActivity;
 import android.view.View;
 
 import org.mozilla.search.autocomplete.AcceptsSearchQuery;
 
 /**
  * The main entrance for the Android search intent.
@@ -28,26 +27,32 @@ public class MainActivity extends Fragme
     }
 
     private State state = State.START;
 
     @Override
     protected void onCreate(Bundle stateBundle) {
         super.onCreate(stateBundle);
         setContentView(R.layout.search_activity_main);
-        startPresearch();
     }
 
     @Override
     public void onSearch(String s) {
         startPostsearch();
         ((PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.postsearch))
                 .startSearch(s);
     }
 
+    @Override
+    protected void onResume() {
+        super.onResume();
+        // When the app launches, make sure we're in presearch *always*
+        startPresearch();
+    }
+
     private void startPresearch() {
         if (state != State.PRESEARCH) {
             state = State.PRESEARCH;
             findViewById(R.id.postsearch).setVisibility(View.INVISIBLE);
             findViewById(R.id.presearch).setVisibility(View.VISIBLE);
         }
     }
 
--- a/mobile/android/search/java/org/mozilla/search/autocomplete/SearchFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/autocomplete/SearchFragment.java
@@ -14,28 +14,28 @@ import android.text.Editable;
 import android.text.TextWatcher;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.AdapterView;
+import android.widget.Button;
 import android.widget.EditText;
 import android.widget.FrameLayout;
 import android.widget.ListView;
 import android.widget.TextView;
 
 import org.mozilla.search.R;
 
 /**
  * A fragment to handle autocomplete. Its interface with the outside
  * world should be very very limited.
  * <p/>
- * TODO: Add clear button to search input
  * TODO: Add more search providers (other than the dictionary)
  */
 public class SearchFragment extends Fragment implements AdapterView.OnItemClickListener,
         TextView.OnEditorActionListener, AcceptsJumpTaps {
 
     private View mainView;
     private FrameLayout backdropFrame;
     private EditText searchBar;
@@ -90,37 +90,41 @@ public class SearchFragment extends Frag
             public void onClick(View v) {
                 if (v.hasFocus()) {
                     return;
                 }
                 transitionToRunning();
             }
         });
 
+        final Button clearButton = (Button) mainView.findViewById(R.id.clear_button);
+        clearButton.setOnClickListener(new View.OnClickListener(){
+            @Override
+            public void onClick(View v) {
+                searchBar.setText("");
+            }
+        });
+
         backdropFrame.setOnClickListener(new BackdropClickListener());
 
         autoCompleteAdapter = new AutoCompleteAdapter(getActivity(), this);
 
         // Disable notifying on change. We're going to be changing the entire dataset, so
         // we don't want multiple re-draws.
         autoCompleteAdapter.setNotifyOnChange(false);
 
         suggestionDropdown.setAdapter(autoCompleteAdapter);
 
         initRows();
 
         autoCompleteAgentManager =
                 new AutoCompleteAgentManager(getActivity(), new MainUiHandler(autoCompleteAdapter));
 
         // This will hide the autocomplete box and background frame.
-        // Is there a case where we *shouldn't* hide this upfront?
-
-        // Uncomment show card stream first.
-        // transitionToWaiting();
-        transitionToRunning();
+        transitionToWaiting();
 
         // Attach listener for tapping on a suggestion.
         suggestionDropdown.setOnItemClickListener(new AdapterView.OnItemClickListener() {
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                 String query = ((AutoCompleteModel) suggestionDropdown.getItemAtPosition(position))
                         .getMainText();
                 startSearch(query);
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..efb5ab8e7474a84d0cbf37fa22702768b2d0ffa6
GIT binary patch
literal 550
zc%17D@N?(olHy`uVBq!ia0vp^Y9P$P3?%12mYf5mSkfJR9T^xl_H+M9WCijSl0AZa
z85pX73L9D&7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10!Y05c#1%-#CuhVZrANdh
zre_x>q+|lQ#GwEG|Eo4Wj05TyDhcunW&i^@G0lw&75**xE5CHjHs8k;Kq1B?Z+91A
zp4~CifSfy?E{-7<r;kp#%hasE(b_)sS$x|2{~I<hb6M8Ay4U(8M?A-#xc^2LZ)V3c
zcwC*P$*J(cBs=-N6+`xyp1KLQEc4G-tG0hVm)U8QdS}~@f?jrkKbtHUKj4`sbGquH
z>eX3u{<So&xgC3<bmD5o&z=6qv>X{7_r3Dq_WZ#S9=lX1;s2DKPP?C+xc=mnnSwwO
z?_4&ENk?xy-1SNIb%9{)oz#zkN2hF@)b&f;#oP2Ax6w9Zqf@7=be>)QEY(?%8rj@`
z^U0lwiF^r?&%MI>jq@#i&aX~W`Z!zGwnXe!{~O+{2_oIc>}F2_`ct*UHKHUXu_V<h
zxhNG#F&G&bn&=vs=o*@b7#dp{8(W#0=^B_@85p!XIz^*s$jwj5OsmAL;hW5_=b)hW
MboFyt=akR{0E6?)RR910
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..eda09a3f6b27fb6772bab05f1541b4a2f79d3b34
GIT binary patch
literal 473
zc%17D@N?(olHy`uVBq!ia0vp^QXtI13?%1G+4BcTv7|ftIx;Y9?C1WI$O_~uBzpw;
zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpoC?BPlzj!j!({rj!#a>
z$OSTpMF0Q)zn~Z62h=QB666;Qq#1aG+LkE)S*^5Aan0{-9*fql0g5pudAqv^Rqx3G
zDVXf(;uxZFerwQMzQYO}I%~u1*4+ADe{<u`&|5x})uiiNWMtSaeCrJ=ZeHg&@KZVH
z_-?kHrVpeqvmbuNcY;0Xr^6wU8fBMdJFcoltGHd}RtXX@=o4Ie(sQ!m)lX{m*S<`N
zY$?7p>!*=)k)G;oySCs{`@|co1KI`789itzIl}KIr<=9mUe%PRFZHJ{=JC7vpTT}B
zyZ;Qf(^)#296;x(mbgZgq$HN4S|t~y0x1R~149#C0~1|C^AJO0D`R6TQ!`xyb1MUb
hc1NdZ6b-rgDVb@NxHWu}`SlzW0-mmZF6*2UngIQ-pEdvh
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8fd7a3d1d00f3a61c1bf29396fc19be4759ab8e8
GIT binary patch
literal 679
zc%17D@N?(olHy`uVBq!ia0vp^#vshW3=*k7AOWOU(j9#r85lP9bN@+X1@aY=J%W50
z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10A~wJ$#1%-#C8ftFXZQz&
z`38hk)V3s~WY#uyDk-amMa9J?riO&a0A<NQ|NsA=`|#&yparreL4LsuNI)}2P-WY@
z2EM;vzX<TxD}FeV_937VD9)JV?d~F1#HFAOWE*<AIEHu}e>?3qQ<DNuTc%prJk7Jh
z@Bh!zbYx<BweGg+=lkCt-cfmye@Smc!~@2M`y<pN<9>v0UVg&(GJimrX*HXPe^~g^
zl_yt;=kHnMdg(*>RLNO;p3T|*=B)_ho=EH8ukTzCDW2nch0$f264Q&8C5fyNC!-Z_
zO?&=T^Z2xvryp^#uS%PJ(&GNK3u}GCD=T6nt*(FF@rti7%17(jtlQt8C1n}D)G_(C
zt>YMrwT|C1TQS?oU2C86czS$Fe$oE#__PTdG?x`heUHpht7~yReWl;Y=v(`yGiA&Y
zY71wwHQZL@RpM`Y{%3FEJ^n@f0)NZ)tv5GTwBSB-$ggI3M`c0XyM$-bNqtGrw(b*u
zH|^QG+uSq$Hs35Rnjhrf9dO*P_Qc+~RVFL%+~zmFF!4XL*FUwl%$;j2PwbpL2^hx=
zswJ)wB`Jv|saDBFsX&Us$iUD<*T6*A&^*M@*vi=0%G6BPz}(8fpxx0a8bw2HeoAIq
WC2kGhWPUvdMWUyxpUXO@geCyZ*$1Tn
--- a/mobile/android/search/res/layout/search_auto_complete.xml
+++ b/mobile/android/search/res/layout/search_auto_complete.xml
@@ -20,19 +20,33 @@
     <LinearLayout
 
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:background="@drawable/search_card_background"
         android:orientation="vertical"
         >
 
-        <EditText
-            android:id="@+id/auto_complete_search_bar"
-            style="@style/AutoCompleteEditText"/>
+        <FrameLayout
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content">
+
+            <EditText
+                android:id="@+id/auto_complete_search_bar"
+                style="@style/AutoCompleteEditText"/>
+
+            <Button
+                android:id="@+id/clear_button"
+                android:layout_width="26dp"
+                android:layout_height="26dp"
+                android:layout_gravity="right|center_vertical"
+                android:layout_marginRight="3dp"
+                android:background="@drawable/search_clear" />
+
+        </FrameLayout>
 
         <ListView
             android:id="@+id/auto_complete_dropdown"
             style="@style/AutoCompleteDropdown"/>
     </LinearLayout>
 
 
 </FrameLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/stumbler/Makefile.in
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/rules.mk
+
+JAVA_CLASSPATH := $(ANDROID_SDK)/android.jar
+include $(topsrcdir)/config/android-common.mk
+
+libs:: stumbler.jar
new file mode 100644
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/PlaceHolder.java
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler;
+
+/**
+ * Bug 1024708: this class is a place-holder for landing the build integration
+ * of the background stumbler into Fennec.
+ */
+public class PlaceHolder {
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/stumbler/manifests/StumblerManifest_services.xml.in
@@ -0,0 +1,2 @@
+<!-- Bug 1024708: this fragment is a place-holder for landing the
+     build integration of the background stumbler into Fennec. -->
new file mode 100644
--- /dev/null
+++ b/mobile/android/stumbler/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include('stumbler_sources.mozbuild')
+
+stumbler_jar = add_java_jar('stumbler')
+stumbler_jar.sources += stumbler_sources
+stumbler_jar.extra_jars += [CONFIG['ANDROID_COMPAT_LIB']]
+stumbler_jar.javac_flags += ['-Xlint:all']
+
+stumbler_eclipse = add_android_eclipse_library_project('FennecStumbler')
+stumbler_eclipse.package_name = 'org.mozilla.fennec.stumbler'
+stumbler_eclipse.res = None
+stumbler_eclipse.extra_jars += [CONFIG['ANDROID_COMPAT_LIB']]
+stumbler_eclipse.add_classpathentry('java', SRCDIR + '/java', dstdir='java')
new file mode 100644
--- /dev/null
+++ b/mobile/android/stumbler/stumbler_sources.mozbuild
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+stumbler_sources = [
+    'java/org/mozilla/mozstumbler/PlaceHolder.java',
+]
--- a/mobile/android/tests/browser/junit3/moz.build
+++ b/mobile/android/tests/browser/junit3/moz.build
@@ -8,16 +8,17 @@ DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG
 
 jar = add_java_jar('browser-junit3')
 jar.sources += [
     'src/harness/BrowserInstrumentationTestRunner.java',
     'src/harness/BrowserTestListener.java',
     'src/tests/BrowserTestCase.java',
     'src/tests/TestDistribution.java',
     'src/tests/TestGeckoSharedPrefs.java',
+    'src/tests/TestImageDownloader.java',
     'src/tests/TestJarReader.java',
     'src/tests/TestRawResource.java',
     'src/tests/TestSuggestedSites.java',
     'src/tests/TestTopSitesCursorWrapper.java',
 ]
 jar.generated_sources = [] # None yet -- try to keep it this way.
 jar.javac_flags += ['-Xlint:all,-unchecked']
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/tests/TestImageDownloader.java
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.browser.tests;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.test.mock.MockResources;
+import android.test.RenamingDelegatingContext;
+import android.util.DisplayMetrics;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.home.ImageLoader.ImageDownloader;
+
+public class TestImageDownloader extends BrowserTestCase {
+    private static class TestContext extends RenamingDelegatingContext {
+        private static final String PREFIX = "TestImageDownloader-";
+
+        private final Resources resources;
+        private final Set<String> usedPrefs;
+
+        public TestContext(Context context) {
+            super(context, PREFIX);
+            resources = new TestResources();
+            usedPrefs = Collections.synchronizedSet(new HashSet<String>());
+        }
+
+        @Override
+        public Resources getResources() {
+            return resources;
+        }
+
+        @Override
+        public SharedPreferences getSharedPreferences(String name, int mode) {
+            usedPrefs.add(name);
+            return super.getSharedPreferences(PREFIX + name, mode);
+        }
+
+        public void clearUsedPrefs() {
+            for (String prefsName : usedPrefs) {
+                getSharedPreferences(prefsName, 0).edit().clear().commit();
+            }
+
+            usedPrefs.clear();
+        }
+    }
+
+    private static class TestResources extends MockResources {
+        private final DisplayMetrics metrics;
+
+        public TestResources() {
+            metrics = new DisplayMetrics();
+        }
+
+        @Override
+        public DisplayMetrics getDisplayMetrics() {
+            return metrics;
+        }
+
+        public void setDensityDpi(int densityDpi) {
+            metrics.densityDpi = densityDpi;
+        }
+    }
+
+    private static class TestDistribution extends Distribution {
+        final List<String> accessedFiles;
+
+        public TestDistribution(Context context) {
+            super(context);
+            accessedFiles = new ArrayList<String>();
+        }
+
+        @Override
+        public File getDistributionFile(String name) {
+            accessedFiles.add(name);
+
+            // Return null to ensure the ImageDownloader will go
+            // through a complete density lookup for each filename.
+            return null;
+        }
+
+        public List<String> getAccessedFiles() {
+            return Collections.unmodifiableList(accessedFiles);
+        }
+
+        public void resetAccessedFiles() {
+            accessedFiles.clear();
+        }
+    }
+
+    private TestContext context;
+    private TestResources resources;
+    private TestDistribution distribution;
+    private ImageDownloader downloader;
+
+    protected void setUp() {
+        context = new TestContext(getApplicationContext());
+        resources = (TestResources) context.getResources();
+        distribution = new TestDistribution(context);
+        downloader = new ImageDownloader(context, distribution);
+    }
+
+    protected void tearDown() {
+        context.clearUsedPrefs();
+    }
+
+    private void triggerLoad(Uri uri) {
+        try {
+            downloader.load(uri, false);
+        } catch (IOException e) {
+            // Ignore any IO exceptions.
+        }
+    }
+
+    private void checkAccessedFiles(String[] filenames) {
+        List<String> accessedFiles = distribution.getAccessedFiles();
+
+        for (int i = 0; i < filenames.length; i++) {
+            assertEquals(filenames[i], accessedFiles.get(i));
+        }
+    }
+
+    private void checkAccessedFilesForUri(Uri uri, int densityDpi, String[] filenames) {
+        resources.setDensityDpi(densityDpi);
+        triggerLoad(uri);
+        checkAccessedFiles(filenames);
+        distribution.resetAccessedFiles();
+    }
+
+    public void testAccessedFiles() {
+        // Filename only.
+        checkAccessedFilesForUri(Uri.parse("gecko.distribution://file"),
+                                 DisplayMetrics.DENSITY_MEDIUM,
+                                 new String[] {
+                                    "mdpi/file.png",
+                                    "xhdpi/file.png",
+                                    "hdpi/file.png"
+                                 });
+
+        // Directory and filename.
+        checkAccessedFilesForUri(Uri.parse("gecko.distribution://dir/file"),
+                                 DisplayMetrics.DENSITY_MEDIUM,
+                                 new String[] {
+                                    "dir/mdpi/file.png",
+                                    "dir/xhdpi/file.png",
+                                    "dir/hdpi/file.png"
+                                 });
+
+        // Sub-directories and filename.
+        checkAccessedFilesForUri(Uri.parse("gecko.distribution://dir/subdir/file"),
+                                 DisplayMetrics.DENSITY_MEDIUM,
+                                 new String[] {
+                                    "dir/subdir/mdpi/file.png",
+                                    "dir/subdir/xhdpi/file.png",
+                                    "dir/subdir/hdpi/file.png"
+                                 });
+    }
+
+    public void testDensityLookup() {
+        Uri uri = Uri.parse("gecko.distribution://file");
+
+        // Medium density
+        checkAccessedFilesForUri(uri,
+                                 DisplayMetrics.DENSITY_MEDIUM,
+                                 new String[] {
+                                    "mdpi/file.png",
+                                    "xhdpi/file.png",
+                                    "hdpi/file.png"
+                                 });
+
+        checkAccessedFilesForUri(uri,
+                                 DisplayMetrics.DENSITY_HIGH,
+                                 new String[] {
+                                    "hdpi/file.png",
+                                    "xxhdpi/file.png",
+                                    "xhdpi/file.png"
+                                 });
+
+        checkAccessedFilesForUri(uri,
+                                 DisplayMetrics.DENSITY_XHIGH,
+                                 new String[] {
+                                    "xhdpi/file.png",
+                                    "xxhdpi/file.png",
+                                    "mdpi/file.png"
+                                 });
+
+
+        checkAccessedFilesForUri(uri,
+                                 DisplayMetrics.DENSITY_XXHIGH,
+                                 new String[] {
+                                    "xxhdpi/file.png",
+                                    "hdpi/file.png"
+                                 });
+    }
+}
--- a/mobile/android/tests/browser/junit3/src/tests/TestSuggestedSites.java
+++ b/mobile/android/tests/browser/junit3/src/tests/TestSuggestedSites.java
@@ -1,36 +1,50 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.browser.tests;
 
 import android.content.Context;
+import android.content.ContentResolver;
 import android.content.res.Resources;
 import android.content.SharedPreferences;
 import android.database.Cursor;
+import android.database.ContentObserver;
 import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
 import android.test.mock.MockResources;
 import android.test.RenamingDelegatingContext;
 
 import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
 import java.io.InputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.jar.JarInputStream;
+import java.util.Map;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 
 import org.json.JSONArray;
 import org.json.JSONObject;
 
+import org.mozilla.gecko.BrowserLocaleManager;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.SuggestedSites;
+import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 
 public class TestSuggestedSites extends BrowserTestCase {
     private static class TestContext extends RenamingDelegatingContext {
         private static final String PREFIX = "TestSuggestedSites-";
 
         private final Resources resources;
@@ -74,59 +88,147 @@ public class TestSuggestedSites extends 
             return null;
         }
 
         public void setSuggestedSitesResource(String suggestedSites) {
             this.suggestedSites = suggestedSites;
         }
     }
 
+    private static class TestDistribution extends Distribution {
+        private final Context context;
+        private final Map<Locale, File> filesPerLocale;
+
+        public TestDistribution(Context context) {
+            super(context);
+            this.context = context;
+            this.filesPerLocale = new HashMap<Locale, File>();
+        }
+
+        @Override
+        public File getDistributionFile(String name) {
+            for (Locale locale : filesPerLocale.keySet()) {
+                if (name.startsWith("suggestedsites/locales/" + BrowserLocaleManager.getLanguageTag(locale))) {
+                    return filesPerLocale.get(locale);
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public boolean exists() {
+            return true;
+        }
+
+        public void setFileForLocale(Locale locale, File file) {
+            filesPerLocale.put(locale, file);
+        }
+
+        public void start() {
+            doInit();
+        }
+    }
+
+    class TestObserver extends ContentObserver {
+        private final Object changeLock;
+
+        public TestObserver(Object changeLock) {
+            super(null);
+            this.changeLock = changeLock;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            synchronized(changeLock) {
+                changeLock.notifyAll();
+            }
+        }
+    }
+
     private static final int DEFAULT_LIMIT = 6;
 
+    private static final String DIST_PREFIX = "dist";
+
     private TestContext context;
     private TestResources resources;
+    private List<File> tempFiles;
 
     private String generateSites(int n) {
+        return generateSites(n, "");
+    }
+
+    private String generateSites(int n, String prefix) {
         JSONArray sites = new JSONArray();
 
         try {
             for (int i = 0; i < n; i++) {
                 JSONObject site = new JSONObject();
-                site.put("url", "url" + i);
-                site.put("title", "title" + i);
-                site.put("imageurl", "imageUrl" + i);
-                site.put("bgcolor", "bgColor" + i);
+                site.put("url", prefix + "url" + i);
+                site.put("title", prefix + "title" + i);
+                site.put("imageurl", prefix + "imageUrl" + i);
+                site.put("bgcolor", prefix + "bgColor" + i);
 
                 sites.put(site);
             }
         } catch (Exception e) {
             return "";
         }
 
         return sites.toString();
     }
 
+    private File createDistSuggestedSitesFile(int n) {
+        FileOutputStream fos = null;
+
+        try {
+            File distFile = File.createTempFile("distrosites", ".json",
+                                                context.getCacheDir());
+
+            fos = new FileOutputStream(distFile);
+            fos.write(generateSites(n, DIST_PREFIX).getBytes());
+
+            return distFile;
+        } catch (IOException e) {
+            fail("Failed to create temp suggested sites file");
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException e) {
+                    // Ignore.
+                }
+            }
+        }
+
+        return null;
+    }
+
     private void checkCursorCount(String content, int expectedCount) {
         checkCursorCount(content, expectedCount, DEFAULT_LIMIT);
     }
 
     private void checkCursorCount(String content, int expectedCount, int limit) {
         resources.setSuggestedSitesResource(content);
         Cursor c = new SuggestedSites(context).get(limit);
         assertEquals(expectedCount, c.getCount());
         c.close();
     }
 
     protected void setUp() {
         context = new TestContext(getApplicationContext());
         resources = (TestResources) context.getResources();
+        tempFiles = new ArrayList<File>();
     }
 
     protected void tearDown() {
         context.clearUsedPrefs();
+        for (File f : tempFiles) {
+            f.delete();
+        }
     }
 
     public void testCount() {
         // Empty array = empty cursor
         checkCursorCount(generateSites(0), 0);
 
         // 2 items = cursor with 2 rows
         checkCursorCount(generateSites(2), 2);
@@ -299,9 +401,100 @@ public class TestSuggestedSites extends 
         assertEquals(3, c.getCount());
         c.close();
 
         // Changing the locale forces the cached list to be refreshed.
         c = suggestedSites.get(DEFAULT_LIMIT, Locale.US);
         assertEquals(5, c.getCount());
         c.close();
     }
+
+    public void testDistribution() {
+        final int DIST_COUNT = 2;
+        final int DEFAULT_COUNT = 3;
+
+        File sitesFile = new File(context.getCacheDir(),
+                                  "suggestedsites-" + SystemClock.uptimeMillis() + ".json");
+        tempFiles.add(sitesFile);
+        assertFalse(sitesFile.exists());
+
+        File distFile = createDistSuggestedSitesFile(DIST_COUNT);
+        tempFiles.add(distFile);
+        assertTrue(distFile.exists());
+
+        // Init distribution with the mock file.
+        TestDistribution distribution = new TestDistribution(context);
+        distribution.setFileForLocale(Locale.getDefault(), distFile);
+        distribution.start();
+
+        // Init suggested sites with default values.
+        resources.setSuggestedSitesResource(generateSites(DEFAULT_COUNT));
+        SuggestedSites suggestedSites =
+                new SuggestedSites(context, distribution, sitesFile);
+
+        Object changeLock = new Object();
+
+        // Watch for change notifications on suggested sites.
+        ContentResolver cr = context.getContentResolver();
+        ContentObserver observer = new TestObserver(changeLock);
+        cr.registerContentObserver(BrowserContract.SuggestedSites.CONTENT_URI,
+                                   false, observer);
+
+        // The initial query will not contain the distribution sites
+        // yet. This will happen asynchronously once the distribution
+        // is installed.
+        Cursor c1 = null;
+        try {
+            c1 = suggestedSites.get(DEFAULT_LIMIT);
+            assertEquals(DEFAULT_COUNT, c1.getCount());
+        } finally {
+            if (c1 != null) {
+                c1.close();
+            }
+        }
+
+        synchronized(changeLock) {
+            try {
+                changeLock.wait(5000);
+            } catch (InterruptedException ie) {
+                fail("No change notification after fetching distribution file");
+            }
+        }
+
+        // Target file should exist after distribution is deployed.
+        assertTrue(sitesFile.exists());
+        cr.unregisterContentObserver(observer);
+
+        Cursor c2 = null;
+        try {
+            c2 = suggestedSites.get(DEFAULT_LIMIT);
+
+            // The next query should contain the distribution contents.
+            assertEquals(DIST_COUNT + DEFAULT_COUNT, c2.getCount());
+
+            // The first items should be from the distribution
+            for (int i = 0; i < DIST_COUNT; i++) {
+                c2.moveToPosition(i);
+
+                String url = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
+                assertEquals(DIST_PREFIX +  "url" + i, url);
+
+                String title = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.TITLE));
+                assertEquals(DIST_PREFIX +  "title" + i, title);
+            }
+
+            // The remaining items should be the default ones
+            for (int i = 0; i < c2.getCount() - DIST_COUNT; i++) {
+                c2.moveToPosition(i + DIST_COUNT);
+
+                String url = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
+                assertEquals("url" + i, url);
+
+                String title = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.TITLE));
+                assertEquals("title" + i, title);
+            }
+        } finally {
+            if (c2 != null) {
+                c2.close();
+            }
+        }
+    }
 }
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
@@ -192,20 +192,23 @@ class DeviceManagerSUT(DeviceManager):
             cmdline = '%s\r\n' % cmd['cmd']
 
             try:
                 sent = self._sock.send(cmdline)
                 if sent != len(cmdline):
                     raise DMError("Remote Device Error: our cmd was %s bytes and we "
                                   "only sent %s" % (len(cmdline), sent))
                 if cmd.get('data'):
-                    sent = self._sock.send(cmd['data'])
-                    if sent != len(cmd['data']):
-                        raise DMError("Remote Device Error: we had %s bytes of data to send, but "
-                                      "only sent %s" % (len(cmd['data']), sent))
+                    totalsent = 0
+                    while totalsent < len(cmd['data']):
+                        sent = self._sock.send(cmd['data'][totalsent:])
+                        self._logger.debug("sent %s bytes of data payload" % sent)
+                        if sent == 0:
+                            raise DMError("Socket connection broken when sending data")
+                        totalsent += sent
 
                 self._logger.debug("sent cmd: %s" % cmd['cmd'])
             except socket.error, msg:
                 self._sock.close()
                 self._sock = None
                 self._logger.error("Remote Device Error: Error sending data"\
                         " to socket. cmd=%s; err=%s" % (cmd['cmd'], msg))
                 return False
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6529,16 +6529,23 @@
     "n_values": 30,
     "description": "Algorithms used with WebCrypto (see table in WebCryptoTask.cpp)"
   },
   "MASTER_PASSWORD_ENABLED": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "If a master-password is enabled for this profile"
   },
+  "FENNEC_TILES_CACHE_HIT": {
+    "expires_in_version": "never",
+    "kind": "linear",
+    "high": "13",
+    "n_buckets": 12,
+    "description": "Cache hits on the tile-info metadata database"
+  },
   "DISPLAY_SCALING_OSX" : {
     "expires_in_version": "never",
     "kind": "linear",
     "high": "500",
     "n_buckets": "100",
     "description": "Scaling percentage for the display where the first window is opened (OS X only)",
     "cpp_guard": "XP_MACOSX"
   },
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2046,24 +2046,24 @@ ThreadActor.prototype = {
   },
 
   /**
    * Return a protocol completion value representing the given
    * Debugger-provided completion value.
    */
   createProtocolCompletionValue: function (aCompletion) {
     let protoValue = {};
-    if ("return" in aCompletion) {
+    if (aCompletion == null) {
+      protoValue.terminated = true;
+    } else if ("return" in aCompletion) {
       protoValue.return = this.createValueGrip(aCompletion.return);
-    } else if ("yield" in aCompletion) {
-      protoValue.return = this.createValueGrip(aCompletion.yield);
     } else if ("throw" in aCompletion) {
       protoValue.throw = this.createValueGrip(aCompletion.throw);
     } else {
-      protoValue.terminated = true;
+      protoValue.return = this.createValueGrip(aCompletion.yield);
     }
     return protoValue;
   },
 
   /**
    * Create a grip for the given debuggee object.
    *
    * @param aValue Debugger.Object