Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 07 Aug 2015 13:18:35 +0200
changeset 288478 a2cfbfac8ae898ff1e16ec893dfbbc247c8f8ee2
parent 288477 7e77838e7e2eec51cbfd1ec31402a368a3e5669d (current diff)
parent 288423 3e51753a099f8014b3dc37ed3fa27b887842735b (diff)
child 288479 b9c50a97ee3ed413476928b269741e97206a6248
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound
mobile/android/base/home/RecyclerViewItemClickListener.java
--- a/b2g/config/aries/sources.xml
+++ b/b2g/config/aries/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="735805df70bc64af1e5b709133afb76499a92ee1"/>
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="735805df70bc64af1e5b709133afb76499a92ee1"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="1b0db93fb6b870b03467aff50d6419771ba0d88c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="2d58f4b9206b50b8fda0d5036da6f0c62608db7c"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="d70e4bfdcb65e7514de0f9315b74aea1c811678d"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="660169a3d7e034a892359e39135e8c2785a6ad6f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="735805df70bc64af1e5b709133afb76499a92ee1"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="735805df70bc64af1e5b709133afb76499a92ee1"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="05a36844c1046a1eb07d5b1325f85ed741f961ea">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="735805df70bc64af1e5b709133afb76499a92ee1"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="1b0db93fb6b870b03467aff50d6419771ba0d88c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="2d58f4b9206b50b8fda0d5036da6f0c62608db7c"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="d70e4bfdcb65e7514de0f9315b74aea1c811678d"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="735805df70bc64af1e5b709133afb76499a92ee1"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "91068221506ff05692aa187ac314e15443db68fd", 
+        "git_revision": "59ce66c60e71b434061aeaf11e945814b234c355", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "ab36b7edaadc88ece94cc213db139d1615dadf64", 
+    "revision": "8ab354e8ef85b6ee872eb7eb1710a8e094c93b18", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="660169a3d7e034a892359e39135e8c2785a6ad6f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="735805df70bc64af1e5b709133afb76499a92ee1"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="05a36844c1046a1eb07d5b1325f85ed741f961ea">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="91068221506ff05692aa187ac314e15443db68fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="59ce66c60e71b434061aeaf11e945814b234c355"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4d9fbc08e87731447c19e96e13d8c7444baafcca"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="735805df70bc64af1e5b709133afb76499a92ee1"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -305,17 +305,18 @@ pref("browser.urlbar.restrict.searches",
 pref("browser.urlbar.match.title", "#");
 pref("browser.urlbar.match.url", "@");
 
 // The default behavior for the urlbar can be configured to use any combination
 // of the match filters with each additional filter adding more results (union).
 pref("browser.urlbar.suggest.history",              true);
 pref("browser.urlbar.suggest.bookmark",             true);
 pref("browser.urlbar.suggest.openpage",             true);
-pref("browser.urlbar.suggest.searches",             true);
+pref("browser.urlbar.suggest.searches",             false);
+pref("browser.urlbar.userMadeSearchSuggestionsChoice", false);
 
 // Limit the number of characters sent to the current search engine to fetch
 // suggestions.
 pref("browser.urlbar.maxCharsForSearchSuggestions", 20);
 
 // Restrictions to current suggestions can also be applied (intersection).
 // Typed suggestion works only if history is set to true.
 pref("browser.urlbar.suggest.history.onlyTyped",    false);
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -472,16 +472,37 @@ panel[noactions] > richlistbox > richlis
 searchbar[oneoffui] {
   -moz-binding: url("chrome://browser/content/search/search.xml#searchbar-flare") !important;
 }
 
 #PopupAutoCompleteRichResult {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup");
 }
 
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification {
+  transition: height 100ms;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] {
+  visibility: collapse;
+  transition: margin-top 100ms;
+}
+
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > hbox[anonid="search-suggestions-notification"] {
+  visibility: visible;
+}
+
+#PopupAutoCompleteRichResult > richlistbox {
+  transition: height 100ms;
+}
+
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > richlistbox {
+  transition: none;
+}
+
 #urlbar[pageproxystate="invalid"] > #urlbar-icons > .urlbar-icon,
 #urlbar[pageproxystate="invalid"][focused="true"] > #urlbar-go-button ~ toolbarbutton,
 #urlbar[pageproxystate="valid"] > #urlbar-go-button,
 #urlbar:not([focused="true"]) > #urlbar-go-button {
   visibility: collapse;
 }
 
 #urlbar[pageproxystate="invalid"] > #identity-box > #identity-icon-labels {
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -453,16 +453,17 @@ skip-if = e10s # Bug 1100700 - test reli
 [browser_urlbarAutoFillTrimURLs.js]
 [browser_urlbarCopying.js]
 [browser_urlbarDelete.js]
 [browser_urlbarEnter.js]
 [browser_urlbarEnterAfterMouseOver.js]
 skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
 [browser_urlbarRevert.js]
 [browser_urlbarSearchSingleWordNotification.js]
+[browser_urlbarSearchSuggestionsNotification.js]
 [browser_urlbarStop.js]
 [browser_urlbarTrimURLs.js]
 [browser_urlbar_autoFill_backspaced.js]
 [browser_urlbar_search_healthreport.js]
 [browser_urlbar_searchsettings.js]
 [browser_utilityOverlay.js]
 [browser_visibleFindSelection.js]
 [browser_visibleLabel.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_urlbarSearchSuggestionsNotification.js
@@ -0,0 +1,192 @@
+const SUGGEST_ALL_PREF = "browser.search.suggest.enabled";
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const CHOICE_PREF = "browser.urlbar.userMadeSearchSuggestionsChoice";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// Must run first.
+add_task(function* prepare() {
+  let engine = yield promiseNewEngine(TEST_ENGINE_BASENAME);
+  let oldCurrentEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engine;
+  registerCleanupFunction(function () {
+    Services.search.currentEngine = oldCurrentEngine;
+    Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
+    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+
+    // Disable the notification for future tests so it doesn't interfere with
+    // them.  clearUserPref() won't work because by default the pref is false.
+    Services.prefs.setBoolPref(CHOICE_PREF, true);
+
+    // Make sure the popup is closed for the next test.
+    gURLBar.blur();
+    Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+  });
+});
+
+add_task(function* focus_allSuggestionsDisabled() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, false);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+  yield promiseAutocompleteResultPopup("foo");
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(false);
+});
+
+add_task(function* focus_noChoiceMade() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(true);
+  gURLBar.blur();
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open again");
+  assertVisible(true);
+});
+
+add_task(function* dismissWithoutResults() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(true);
+  Assert.equal(gURLBar.popup._matchCount, 0, "popup should have no results");
+  let disableButton = document.getAnonymousElementByAttribute(
+    gURLBar.popup, "anonid", "search-suggestions-notification-disable"
+  );
+  let transitionPromise = promiseTransition();
+  disableButton.click();
+  yield transitionPromise;
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
+  yield promiseAutocompleteResultPopup("foo");
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(false);
+});
+
+add_task(function* dismissWithResults() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(true);
+  yield promiseAutocompleteResultPopup("foo");
+  Assert.ok(gURLBar.popup._matchCount > 0, "popup should have results");
+  let disableButton = document.getAnonymousElementByAttribute(
+    gURLBar.popup, "anonid", "search-suggestions-notification-disable"
+  );
+  let transitionPromise = promiseTransition();
+  disableButton.click();
+  yield transitionPromise;
+  Assert.ok(gURLBar.popup.popupOpen, "popup should remain open");
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
+  yield promiseAutocompleteResultPopup("foo");
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(false);
+});
+
+add_task(function* disable() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(true);
+  let disableButton = document.getAnonymousElementByAttribute(
+    gURLBar.popup, "anonid", "search-suggestions-notification-disable"
+  );
+  let transitionPromise = promiseTransition();
+  disableButton.click();
+  yield transitionPromise;
+  gURLBar.blur();
+  yield promiseAutocompleteResultPopup("foo");
+  assertSuggestionsPresent(false);
+});
+
+add_task(function* enable() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  yield promiseAutocompleteResultPopup("foo");
+  assertVisible(true);
+  assertSuggestionsPresent(false);
+  let enableButton = document.getAnonymousElementByAttribute(
+    gURLBar.popup, "anonid", "search-suggestions-notification-enable"
+  );
+  let searchPromise = promiseSearchComplete();
+  enableButton.click();
+  yield searchPromise;
+  // Clicking Yes should trigger a new search so that suggestions appear
+  // immediately.
+  assertSuggestionsPresent(true);
+  gURLBar.blur();
+  gURLBar.focus();
+  // Suggestions should still be present in a new search of course.
+  yield promiseAutocompleteResultPopup("bar");
+  assertSuggestionsPresent(true);
+});
+
+function assertSuggestionsPresent(expectedPresent) {
+  let controller = gURLBar.popup.input.controller;
+  let matchCount = controller.matchCount;
+  let actualPresent = false;
+  for (let i = 0; i < matchCount; i++) {
+    let url = controller.getValueAt(i);
+    let [, type, paramStr] = url.match(/^moz-action:([^,]+),(.*)$/);
+    let params = {};
+    try {
+      params = JSON.parse(paramStr);
+    } catch (err) {}
+    let isSuggestion = type == "searchengine" && "searchSuggestion" in params;
+    actualPresent = actualPresent || isSuggestion;
+  }
+  Assert.equal(actualPresent, expectedPresent);
+}
+
+function assertVisible(visible) {
+  let style =
+    window.getComputedStyle(gURLBar.popup.searchSuggestionsNotification);
+  Assert.equal(style.visibility, visible ? "visible" : "collapse");
+}
+
+function promiseNewEngine(basename) {
+  return new Promise((resolve, reject) => {
+    info("Waiting for engine to be added: " + basename);
+    let url = getRootDirectory(gTestPath) + basename;
+    Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "",
+                              false, {
+      onSuccess: function (engine) {
+        info("Search engine added: " + basename);
+        registerCleanupFunction(() => Services.search.removeEngine(engine));
+        resolve(engine);
+      },
+      onError: function (errCode) {
+        Assert.ok(false, "addEngine failed with error code " + errCode);
+        reject();
+      },
+    });
+  });
+}
+
+function promiseTransition() {
+  return new Promise(resolve => {
+    gURLBar.popup.addEventListener("transitionend", function onEnd() {
+      gURLBar.popup.removeEventListener("transitionend", onEnd, true);
+      // The urlbar needs to handle the transitionend first, but that happens
+      // naturally since promises are resolved at the end of the current tick.
+      resolve();
+    }, true);
+  });
+}
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -61,16 +61,18 @@ file, You can obtain one at http://mozil
 
         this._prefs.addObserver("", this, false);
         this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll");
         this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll");
         this.completeDefaultIndex = this._prefs.getBoolPref("autoFill");
         this.timeout = this._prefs.getIntPref("delay");
         this._formattingEnabled = this._prefs.getBoolPref("formatting.enabled");
         this._mayTrimURLs = this._prefs.getBoolPref("trimURLs");
+        this._userMadeSearchSuggestionsChoice =
+          this._prefs.getBoolPref("userMadeSearchSuggestionsChoice");
         this._ignoreNextSelect = false;
 
         this.inputField.controllers.insertControllerAt(0, this._copyCutController);
         this.inputField.addEventListener("paste", this, false);
         this.inputField.addEventListener("mousedown", this, false);
         this.inputField.addEventListener("mousemove", this, false);
         this.inputField.addEventListener("mouseout", this, false);
         this.inputField.addEventListener("overflow", this, false);
@@ -655,16 +657,20 @@ file, You can obtain one at http://mozil
                 this.completeDefaultIndex = this._prefs.getBoolPref(aData);
                 break;
               case "delay":
                 this.timeout = this._prefs.getIntPref(aData);
                 break;
               case "formatting.enabled":
                 this._formattingEnabled = this._prefs.getBoolPref(aData);
                 break;
+              case "userMadeSearchSuggestionsChoice":
+                this._userMadeSearchSuggestionsChoice =
+                  this._prefs.getBoolPref(aData);
+                break;
               case "trimURLs":
                 this._mayTrimURLs = this._prefs.getBoolPref(aData);
                 break;
               case "unifiedcomplete":
                 let useUnifiedComplete = false;
                 try {
                   useUnifiedComplete = this._prefs.getBoolPref(aData);
                 } catch (ex) {}
@@ -908,16 +914,35 @@ file, You can obtain one at http://mozil
           if (Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete") &&
               this.popup.selectedIndex == 0) {
             return this.mController.handleText();
           }
           return this.mController.handleDelete();
         ]]></body>
       </method>
 
+      <field name="_userMadeSearchSuggestionsChoice"><![CDATA[
+        false
+      ]]></field>
+
+      <method name="_maybeShowSearchSuggestionsNotification">
+        <body><![CDATA[
+          let showNotification =
+            !this._userMadeSearchSuggestionsChoice &&
+            // When _urlbarFocused is true, tabbrowser would close the popup if
+            // it's opened here, so don't show the notification.
+            !gBrowser.selectedBrowser._urlbarFocused &&
+            Services.prefs.getBoolPref("browser.search.suggest.enabled") &&
+            this._prefs.getBoolPref("unifiedcomplete");
+          if (showNotification) {
+            this.popup.showSearchSuggestionsNotification(this, this);
+          }
+        ]]></body>
+      </method>
+
     </implementation>
 
     <handlers>
       <handler event="keydown"><![CDATA[
         if ((event.keyCode === KeyEvent.DOM_VK_ALT ||
              event.keyCode === KeyEvent.DOM_VK_SHIFT) &&
             this.popup.selectedIndex >= 0 &&
             !this._noActionsKeys.has(event.keyCode)) {
@@ -938,16 +963,17 @@ file, You can obtain one at http://mozil
             this._clearNoActions();
         }
       ]]></handler>
 
       <handler event="focus"><![CDATA[
         if (event.originalTarget == this.inputField) {
           this._hideURLTooltip();
           this.formatValue();
+          this._maybeShowSearchSuggestionsNotification();
         }
       ]]></handler>
 
       <handler event="blur"><![CDATA[
         if (event.originalTarget == this.inputField) {
           this._clearNoActions();
           this.formatValue();
         }
@@ -1469,25 +1495,81 @@ file, You can obtain one at http://mozil
   <binding id="addengine-icon" extends="xul:box">
     <content>
       <xul:image class="addengine-icon" xbl:inherits="src"/>
       <xul:image class="addengine-badge"/>
     </content>
   </binding>
 
   <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
+
+    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
+      <xul:hbox anonid="search-suggestions-notification" align="center">
+        <xul:description flex="1">
+          &urlbar.searchSuggestionsNotification.question;
+          <xul:label anonid="search-suggestions-notification-learn-more"
+                     class="text-link"
+                     value="&urlbar.searchSuggestionsNotification.learnMore;"/>
+        </xul:description>
+        <xul:button anonid="search-suggestions-notification-disable"
+                    label="&urlbar.searchSuggestionsNotification.disable;"
+                    onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/>
+        <xul:button anonid="search-suggestions-notification-enable"
+                    label="&urlbar.searchSuggestionsNotification.enable;"
+                    onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/>
+      </xul:hbox>
+      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
+                       flex="1"/>
+      <xul:hbox anonid="footer">
+        <children/>
+      </xul:hbox>
+    </content>
+
     <implementation>
       <field name="_maxResults">0</field>
 
       <field name="_bundle" readonly="true">
         Cc["@mozilla.org/intl/stringbundle;1"].
           getService(Ci.nsIStringBundleService).
           createBundle("chrome://browser/locale/places/places.properties");
       </field>
 
+      <field name="searchSuggestionsNotification" readonly="true">
+        document.getAnonymousElementByAttribute(
+          this, "anonid", "search-suggestions-notification"
+        );
+      </field>
+
+      <field name="searchSuggestionsNotificationLearnMoreLink" readonly="true">
+        document.getAnonymousElementByAttribute(
+          this, "anonid", "search-suggestions-notification-learn-more"
+        );
+      </field>
+
+      <field name="footer" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "footer");
+      </field>
+
+      <method name="dismissSearchSuggestionsNotification">
+        <parameter name="enableSuggestions"/>
+        <body><![CDATA[
+          Services.prefs.setBoolPref(
+            "browser.urlbar.userMadeSearchSuggestionsChoice", true
+          );
+          Services.prefs.setBoolPref(
+            "browser.urlbar.suggest.searches", enableSuggestions
+          );
+          this._hideSearchSuggestionsNotification(true);
+          if (enableSuggestions && this.input.textValue) {
+            // Start a new search so that suggestions appear immediately.
+            this.input.controller.startSearch(this.input.textValue);
+          }
+        ]]></body>
+      </method>
+
       <!-- Override this so that when UnifiedComplete is enabled, navigating
            between items results in an item always being selected. This is
            contrary to the old behaviour (UnifiedComplete disabled) where
            if you navigate beyond either end of the list, no item will be
            selected. -->
       <method name="getNextIndex">
         <parameter name="reverse"/>
         <parameter name="amount"/>
@@ -1544,19 +1626,112 @@ file, You can obtain one at http://mozil
         <parameter name="aInput"/>
         <parameter name="aElement"/>
         <body>
           <![CDATA[
           // initially the panel is hidden
           // to avoid impacting startup / new window performance
           aInput.popup.hidden = false;
 
-          // this method is defined on the base binding
+          // The popup may already be open if it's showing the search
+          // suggestions notification.  In that case, its footer visibility
+          // needs to be updated.
+          if (this.popupOpen) {
+            this._updateFooterVisibility();
+          }
+
           this._openAutocompletePopup(aInput, aElement);
-        ]]></body>
+          ]]>
+        </body>
+      </method>
+
+      <method name="_updateFooterVisibility">
+        <body>
+          <![CDATA[
+          this.footer.collapsed = this._matchCount == 0;
+          ]]>
+        </body>
+      </method>
+
+      <method name="showSearchSuggestionsNotification">
+        <parameter name="aInput"/>
+        <parameter name="aElement"/>
+        <body>
+          <![CDATA[
+          // Set the learn-more link href.
+          let link = this.searchSuggestionsNotificationLearnMoreLink;
+          if (!link.hasAttribute("href")) {
+            let url = Services.urlFormatter.formatURL(
+              Services.prefs.getCharPref("app.support.baseURL") + "suggestions"
+            );
+            link.setAttribute("href", url);
+          }
+
+          // With the notification shown, the listbox's height can sometimes be
+          // too small when it's flexed, as it normally is.  Also, it can start
+          // out slightly scrolled down.  Both problems appear together, most
+          // often when the popup is very narrow and the notification's text
+          // must wrap.  Work around them by removing the flex.
+          //
+          // But without flexing the listbox, the listbox's height animation
+          // sometimes fails to complete, leaving the popup too tall.  Work
+          // around that problem by disabling the listbox animation.
+          this.richlistbox.flex = 0;
+          this.setAttribute("dontanimate", "true");
+
+          this.classList.add("showSearchSuggestionsNotification");
+          this._updateFooterVisibility();
+          this.openAutocompletePopup(aInput, aElement);
+          ]]>
+        </body>
+      </method>
+
+      <method name="_hideSearchSuggestionsNotification">
+        <parameter name="animate"/>
+        <body>
+          <![CDATA[
+          if (animate) {
+            this._hideSearchSuggestionsNotificationWithAnimation();
+            return;
+          }
+          this.classList.remove("showSearchSuggestionsNotification");
+          this.richlistbox.flex = 1;
+          this.removeAttribute("dontanimate");
+          if (this._matchCount) {
+            // Update popup height.
+            this._invalidate();
+          } else {
+            this.closePopup();
+          }
+          ]]>
+        </body>
+      </method>
+
+      <method name="_hideSearchSuggestionsNotificationWithAnimation">
+        <body>
+          <![CDATA[
+          let notificationHeight = this.searchSuggestionsNotification
+                                       .getBoundingClientRect()
+                                       .height;
+          this.searchSuggestionsNotification.style.marginTop =
+            "-" + notificationHeight + "px";
+
+          let popupHeightPx =
+            (this.getBoundingClientRect().height - notificationHeight) + "px";
+          this.style.height = popupHeightPx;
+
+          let onTransitionEnd = () => {
+            this.removeEventListener("transitionend", onTransitionEnd, true);
+            this.searchSuggestionsNotification.style.marginTop = "0px";
+            this.style.removeProperty("height");
+            this._hideSearchSuggestionsNotification(false);
+          };
+          this.addEventListener("transitionend", onTransitionEnd, true);
+          ]]>
+        </body>
       </method>
 
       <method name="onPopupClick">
         <parameter name="aEvent"/>
         <body>
           <![CDATA[
           // Ignore right-clicks
           if (aEvent.button == 2)
@@ -1664,16 +1839,25 @@ file, You can obtain one at http://mozil
         // When the user selects one of matches, stop the search to avoid
         // changing the underlying result unexpectedly.
         if (!this._ignoreNextSelect && this.selectedIndex >= 0) {
           let controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
           controller.stopSearch();
         }
       ]]></handler>
 
+      <handler event="mousedown"><![CDATA[
+        // Required to make the xul:label.text-link elements in the search
+        // suggestions notification work correctly when clicked on Linux.
+        // This is copied from the mousedown handler in
+        // browser-search-autocomplete-result-popup, which apparently had a
+        // similar problem.
+        event.preventDefault();
+      ]]></handler>
+
     </handlers>
   </binding>
 
   <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
     <implementation>
       <constructor><![CDATA[
         if (!this.notification)
           return;
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -20,17 +20,17 @@
                        oncommand="gMenuButtonUpdateBadge.onMenuPanelCommand(event);"
                        wrap="true"
                        hidden="true"/>
         <hbox id="PanelUI-footer-fxa">
           <hbox id="PanelUI-fxa-status"
                 defaultlabel="&fxaSignIn.label;"
                 signedinTooltiptext="&fxaSignedIn.tooltip;"
                 errorlabel="&fxaSignInError.label;"
-                onclick="gFxAccounts.onMenuPanelCommand();">
+                onclick="if (event.which == 1) gFxAccounts.onMenuPanelCommand();">
             <image id="PanelUI-fxa-avatar"/>
             <toolbarbutton id="PanelUI-fxa-label"
                            fxabrandname="&syncBrand.fxAccount.label;"/>
           </hbox>
           <toolbarseparator/>
           <toolbarbutton id="PanelUI-fxa-icon"
                          oncommand="gSyncUI.doSync();"
                          closemenu="none"/>
--- a/browser/components/loop/content/css/contacts.css
+++ b/browser/components/loop/content/css/contacts.css
@@ -13,53 +13,55 @@
 }
 
 .content-area input.contact-filter {
   margin-top: 14px;
   border-radius: 10000px;
 }
 
 .contact-list {
-  border-top: 1px solid #ccc;
   overflow-x: hidden;
   overflow-y: auto;
   /* Space for six contacts, not affected by filtering.  This is enough space
      to show the dropdown menu when there is only one contact. */
   height: 306px;
 }
 
+.contact-list-title {
+  padding: 0 1rem;
+  color: #666;
+  font-weight: 500;
+  font-size: .9em;
+}
+
 .contact,
 .contact-separator {
-  padding: .5rem 1rem;
+  padding: .5rem 15px;
   font-size: 13px;
 }
 
 .contact {
   position: relative;
   display: flex;
   flex-direction: row;
   align-items: center;
   color: #666;
 }
 
 .contact-separator {
   background-color: #eee;
   color: #888;
 }
 
-.contact:not(:first-child) {
-  border-top: 1px solid #ddd;
-}
-
 .contact-separator:not(:first-child) {
   border-top: 1px solid #ccc;
 }
 
 .contact:hover {
-  background-color: #eee;
+  background-color: #E3F7FE;
 }
 
 .contact:hover > .icons {
   display: block;
   z-index: 1;
 }
 
 .contact > .details {
@@ -86,24 +88,111 @@
   box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
   background-image: url("../shared/img/audio-call-avatar.svg");
   background-repeat: no-repeat;
   background-color: #4ba6e7;
   background-size: contain;
   -moz-user-select: none;
 }
 
+/*
+ * Loop through all 12 default avatars.
+ */
+.contact:nth-child(12n + 1) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#blue-avatar");
+  background-color: #4A90E2;
+}
+
+.contact:nth-child(12n + 2) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#orange-avatar");
+  background-color: #F3A35C;
+}
+
+.contact:nth-child(12n + 3) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#mintgreen-avatar");
+  background-color: #50E2C2;
+}
+
+.contact:nth-child(12n + 4) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#lightpink-avatar");
+  background-color: #E364A1;
+}
+
+.contact:nth-child(12n + 5) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#grey-avatar");
+  background-color: #9B9B9B;
+}
+
+.contact:nth-child(12n + 6) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#yellow-avatar");
+  background-color: #F3E968;
+}
+
+.contact:nth-child(12n + 7) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#purple-avatar");
+  background-color: #9C61AF;
+}
+
+.contact:nth-child(12n + 8) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#lightgreen-avatar");
+  background-color: #9AC967;
+}
+
+.contact:nth-child(12n + 9) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#darkblue-avatar");
+  background-color: #607CAE;
+}
+
+.contact:nth-child(12n + 10) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#darkpink-avatar");
+  background-color: #CE4D6E;
+}
+
+.contact:nth-child(12n + 11) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#brown-avatar");
+  background-color: #8A572A;
+}
+
+.contact:nth-child(12n + 12) > .avatar.defaultAvatar {
+  background-image: url("../shared/img/avatars.svg#green-avatar");
+  background-color: #56B397;
+}
+
 .contact > .avatar > img {
   width: 100%;
 }
 
+.contact-list-empty {
+  background-image: url("../shared/img/empty_contacts.svg");
+  background-repeat: no-repeat;
+  background-position: top center;
+  padding-top: 28%;
+  padding-bottom: 5%;
+  text-align: center;
+  color: #4a4a4a;
+  font-weight: lighter;
+}
+
+.panel-text-medium,
+.panel-text-large {
+  margin: 3px;
+}
+
+.panel-text-medium {
+  font-size: 1.6rem;
+}
+
+.panel-text-large {
+  font-size: 2.2rem;
+}
+
 .contact > .details > .username {
-  font-size: 12px;
+  font-size: 1.3rem;
   line-height: 20px;
-  color: #222;
+  color: #000;
 }
 
 .contact.blocked > .details > .username {
   color: #d74345;
 }
 
 .contact > .details > .username > strong {
   font-weight: bold;
@@ -115,135 +204,88 @@
   height: 20px;
   -moz-margin-start: 3px;
   background-image: url("../shared/img/icons-16x16.svg#block-red");
   background-position: center;
   background-size: 10px 10px;
   background-repeat: no-repeat;
 }
 
-.contact > .details > .username > i.icon-google {
-  position: absolute;
-  right: 1rem;
-  top: 35%;
-  width: 14px;
-  height: 14px;
-  border-radius: 50%;
-  background-image: url("../shared/img/icons-16x16.svg#google");
-  background-position: center;
-  background-size: 16px 16px;
-  background-repeat: no-repeat;
-  background-color: #fff;
-}
-
-html[dir="rtl"] .contact > .details > .username > i.icon-google {
-  left: 1rem;
-  right: auto;
-}
-
 .contact > .details > .email {
-  color: #999;
+  color: #4a4a4a;
   font-size: 11px;
-  line-height: 16px;
+  line-height: 14px;
 }
 
 .icons {
   cursor: pointer;
   display: none;
   -moz-margin-start: auto;
-  padding: 10px;
-  border-radius: 2px;
-  background-color: #5bc0a4;
   color: #fff;
   -moz-user-select: none;
 }
 
 .icons:hover {
-  background-color: #47b396;
-}
-
-.icons:hover:active {
-  background-color: #3aa689;
+  display: block;
 }
 
 .icons i {
-  margin: 0 5px;
   display: inline-block;
   background-position: center;
   background-repeat: no-repeat;
 }
 
-.icons i.icon-video {
-  background-image: url("../shared/img/icons-14x14.svg#video-white");
-  background-size: 14px 14px;
+.icon-contact-video-call {
+  padding: 15px;
   width: 16px;
   height: 16px;
+  border-radius: 50%;
+  background-color: #5bc0a4;
+  background-image: url("../shared/img/icons-14x14.svg#video-white");
+  background-size: 16px 16px;
+}
+
+.icon-contact-video-call:hover {
+  background-color: #47b396;
 }
 
-.icons i.icon-caret-down {
-  background-image: url("../shared/img/icons-10x10.svg#dropdown-white");
-  background-size: 10px 10px;
-  width: 10px;
-  height: 16px;
+.icon-contact-video-call:active {
+  background-color: #3aa689;
+}
+
+.icon-vertical-ellipsis {
+  /* Added padding for a larger click area. */
+  padding: 0 10px;
+  margin: 6px 0;
+  -moz-margin-start: 5px;
+  -moz-margin-end: -8px;
+  width: 4px;
+  height: 20px;
+  background-image: url("../shared/img/ellipsis-v.svg");
+  background-size: contain;
 }
 
 .contact > .dropdown-menu {
   z-index: 2;
-  top: 10px;
+  top: 37px;
+  right: 22px;
   bottom: auto;
-  right: 3em;
   left: auto;
 }
 
 html[dir="rtl"] .contact > .dropdown-menu {
   right: auto;
-  left: 3em;
+  left: 22px;
 }
 
 .contact > .dropdown-menu-up {
-  bottom: 10px;
+  bottom: 25px;
   top: auto;
 }
 
-.contact > .dropdown-menu > .dropdown-menu-item > .icon {
-  width: 20px;
-  height: 10px;
-  background-position: center left;
-  background-size: 10px 10px;
-  margin-top: 3px;
-}
-
-html[dir="rtl"] .contact > .dropdown-menu > .dropdown-menu-item > .icon {
-  background-position: center right;
-}
-
-.contact > .dropdown-menu > .dropdown-menu-item > .icon-audio-call {
-  background-image: url("../shared/img/icons-16x16.svg#audio");
-}
-
-.contact > .dropdown-menu > .dropdown-menu-item > .icon-video-call {
-  background-image: url("../shared/img/icons-16x16.svg#video");
-}
-
-.contact > .dropdown-menu > .dropdown-menu-item > .icon-edit {
-  background-image: url("../shared/img/icons-16x16.svg#contacts");
-}
-
-.contact > .dropdown-menu > .dropdown-menu-item > .icon-block {
-  background-image: url("../shared/img/icons-16x16.svg#block");
-}
-
-.contact > .dropdown-menu > .dropdown-menu-item > .icon-unblock {
-  background-image: url("../shared/img/icons-16x16.svg#unblock");
-}
-
-.contact > .dropdown-menu > .dropdown-menu-item > .icon-remove {
-  background-image: url("../shared/img/icons-16x16.svg#delete");
-}
-
 .contact-form > .button-group {
   margin-top: 1rem;
 }
 
 .contacts-gravatar-promo {
   position: relative;
   border: 1px dashed #c1c1c1;
   border-radius: 2px;
--- a/browser/components/loop/content/css/panel.css
+++ b/browser/components/loop/content/css/panel.css
@@ -61,16 +61,17 @@ body {
   padding: .5rem 1rem;
   border-radius: 3px;
 }
 
 /* Tabs and tab selection buttons */
 
 .tab-view-container {
   background-image: url("../shared/img/beta-ribbon.svg#beta-ribbon");
+  background-color: #fbfbfb;
   background-size: 36px 36px;
   background-repeat: no-repeat;
 }
 
 .tab-view {
   position: relative;
   width: 100%;
   height: 4rem;
@@ -717,16 +718,21 @@ html[dir="rtl"] .generate-url-spinner {
   height: 16px;
   vertical-align: bottom;
   background-repeat: no-repeat;
   background-size: cover;
   -moz-margin-end: .2rem;
   margin-bottom: -2px;
 }
 
+.dropdown-menu-item.status-available:before,
+.dropdown-menu-item.status-unavailable:before {
+  margin-bottom: 2px;
+}
+
 html[dir="rtl"] .dropdown-menu-item.status-available:before,
 html[dir="rtl"] .dropdown-menu-item.status-unavailable:before {
   margin-right: -3px;
 }
 
 .status-available:before {
   background-image: url("../shared/img/icons-16x16.svg#status-available");
 }
@@ -837,17 +843,17 @@ html[dir="rtl"] .settings-menu .dropdown
 .footer {
   display: flex;
   flex-direction: row;
   flex-wrap: nowrap;
   justify-content: space-between;
   align-content: stretch;
   align-items: center;
   font-size: 1rem;
-  background-color: #fff;
+  background-color: #fbfbfb;
   color: #666666;
   padding: .5rem 15px;
 }
 
 .footer .signin-details {
   align-items: center;
   display: flex;
 }
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -180,57 +180,39 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     onItemClick: function(event) {
       this.props.handleAction(event.currentTarget.dataset.action);
     },
 
     render: function() {
       var cx = React.addons.classSet;
-
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
         React.createElement("ul", {className: cx({ "dropdown-menu": true,
                             "dropdown-menu-up": this.state.openDirUp })}, 
           React.createElement("li", {className: cx({ "dropdown-menu-item": true,
-                              "disabled": this.props.blocked }), 
-              "data-action": "video-call", 
-              onClick: this.onItemClick}, 
-            React.createElement("i", {className: "icon icon-video-call"}), 
-            mozL10n.get("video_call_menu_button")
-          ), 
-          React.createElement("li", {className: cx({ "dropdown-menu-item": true,
-                              "disabled": this.props.blocked }), 
-              "data-action": "audio-call", 
-              onClick: this.onItemClick}, 
-            React.createElement("i", {className: "icon icon-audio-call"}), 
-            mozL10n.get("audio_call_menu_button")
-          ), 
-          React.createElement("li", {className: cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit }), 
               "data-action": "edit", 
               onClick: this.onItemClick}, 
-            React.createElement("i", {className: "icon icon-edit"}), 
-            mozL10n.get("edit_contact_menu_button")
+            mozL10n.get("edit_contact_title")
           ), 
           React.createElement("li", {className: "dropdown-menu-item", 
               "data-action": blockAction, 
               onClick: this.onItemClick}, 
-            React.createElement("i", {className: "icon icon-" + blockAction}), 
             mozL10n.get(blockLabel)
           ), 
           React.createElement("li", {className: cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit }), 
-               "data-action": "remove", 
-               onClick: this.onItemClick}, 
-            React.createElement("i", {className: "icon icon-remove"}), 
-            mozL10n.get("remove_contact_menu_button2")
+              "data-action": "remove", 
+              onClick: this.onItemClick}, 
+            mozL10n.get("confirm_delete_contact_remove_button")
           )
         )
       );
     }
   });
 
   const ContactDetail = React.createClass({displayName: "ContactDetail",
     getInitialState: function() {
@@ -290,38 +272,42 @@ loop.contacts = (function(_, mozL10n) {
       // We cannot modify imported contacts.  For the moment, the check for
       // determining whether the contact is imported is based on its category.
       return this.props.contact.category[0] != "google";
     },
 
     render: function() {
       let names = getContactNames(this.props.contact);
       let email = getPreferred(this.props.contact, "email");
+      let avatarSrc = navigator.mozLoop.getUserAvatar(email.value);
       let cx = React.addons.classSet;
       let contactCSSClass = cx({
         contact: true,
         blocked: this.props.contact.blocked
       });
+      let avatarCSSClass = cx({
+        avatar: true,
+        defaultAvatar: !avatarSrc
+      });
 
       return (
         React.createElement("li", {className: contactCSSClass, onMouseLeave: this.hideDropdownMenu}, 
-          React.createElement("div", {className: "avatar"}, 
-            React.createElement("img", {src: navigator.mozLoop.getUserAvatar(email.value)})
+          React.createElement("div", {className: avatarCSSClass}, 
+            avatarSrc ? React.createElement("img", {src: avatarSrc}) : null
           ), 
           React.createElement("div", {className: "details"}, 
             React.createElement("div", {className: "username"}, React.createElement("strong", null, names.firstName), " ", names.lastName, 
-              React.createElement("i", {className: cx({"icon icon-google": this.props.contact.category[0] == "google"})}), 
               React.createElement("i", {className: cx({"icon icon-blocked": this.props.contact.blocked})})
             ), 
             React.createElement("div", {className: "email"}, email.value)
           ), 
           React.createElement("div", {className: "icons"}, 
-            React.createElement("i", {className: "icon icon-video", 
+            React.createElement("i", {className: "icon icon-contact-video-call", 
                onClick: this.handleAction.bind(null, "video-call")}), 
-            React.createElement("i", {className: "icon icon-caret-down", 
+            React.createElement("i", {className: "icon icon-vertical-ellipsis", 
                onClick: this.showDropdownMenu})
           ), 
           this.state.showMenu
             ? React.createElement(ContactDropdown, {blocked: this.props.contact.blocked, 
                                canEdit: this.canEdit(), 
                                handleAction: this.handleAction})
             : null
           
@@ -332,20 +318,21 @@ loop.contacts = (function(_, mozL10n) {
 
   const ContactsList = React.createClass({displayName: "ContactsList",
     mixins: [
       React.addons.LinkedStateMixin,
       loop.shared.mixins.WindowCloseMixin
     ],
 
     propTypes: {
+      mozLoop: React.PropTypes.object.isRequired,
       notifications: React.PropTypes.instanceOf(
-        loop.shared.models.NotificationCollection).isRequired,
-        // Callback to handle entry to the add/edit contact form.
-        startForm: React.PropTypes.func.isRequired
+                     loop.shared.models.NotificationCollection).isRequired,
+      // Callback to handle entry to the add/edit contact form.
+      startForm: React.PropTypes.func.isRequired
     },
 
     /**
      * Contacts collection object
      */
     contacts: null,
 
     /**
@@ -356,17 +343,17 @@ loop.contacts = (function(_, mozL10n) {
     getInitialState: function() {
       return {
         importBusy: false,
         filter: ""
       };
     },
 
     refresh: function(callback = function() {}) {
-      let contactsAPI = navigator.mozLoop.contacts;
+      let contactsAPI = this.props.mozLoop.contacts;
 
       this.handleContactRemoveAll();
 
       contactsAPI.getAll((err, contacts) => {
         if (err) {
           callback(err);
           return;
         }
@@ -388,28 +375,28 @@ loop.contacts = (function(_, mozL10n) {
         addContactsInChunks(contacts);
       });
     },
 
     componentWillMount: function() {
       // Take the time to initialize class variables that are used outside
       // `this.state`.
       this.contacts = {};
-      this._userProfile = navigator.mozLoop.userProfile;
+      this._userProfile = this.props.mozLoop.userProfile;
     },
 
     componentDidMount: function() {
       window.addEventListener("LoopStatusChanged", this._onStatusChanged);
 
       this.refresh(err => {
         if (err) {
           throw err;
         }
 
-        let contactsAPI = navigator.mozLoop.contacts;
+        let contactsAPI = this.props.mozLoop.contacts;
 
         // Listen for contact changes/ updates.
         contactsAPI.on("add", (eventName, contact) => {
           this.handleContactAddOrUpdate(contact);
         });
         contactsAPI.on("remove", (eventName, contact) => {
           this.handleContactRemove(contact);
         });
@@ -422,17 +409,17 @@ loop.contacts = (function(_, mozL10n) {
       });
     },
 
     componentWillUnmount: function() {
       window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
     },
 
     _onStatusChanged: function() {
-      let profile = navigator.mozLoop.userProfile;
+      let profile = this.props.mozLoop.userProfile;
       let currUid = this._userProfile ? this._userProfile.uid : null;
       let newUid = profile ? profile.uid : null;
       if (currUid != newUid) {
         // On profile change (login, logout), reload all contacts.
         this._userProfile = profile;
         // The following will do a forceUpdate() for us.
         this.refresh();
       }
@@ -460,17 +447,17 @@ loop.contacts = (function(_, mozL10n) {
     handleContactRemoveAll: function() {
       // Do not allow any race conditions when removing all contacts.
       this.contacts = {};
       this.forceUpdate();
     },
 
     handleImportButtonClick: function() {
       this.setState({ importBusy: true });
-      navigator.mozLoop.startImport({
+      this.props.mozLoop.startImport({
         service: "google"
       }, (err, stats) => {
         this.setState({ importBusy: false });
         if (err) {
           console.error("Contact import error", err);
           this.props.notifications.errorL10n("import_contacts_failure_message");
           return;
         }
@@ -486,54 +473,54 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleContactAction: function(contact, actionName) {
       switch (actionName) {
         case "edit":
           this.props.startForm("contacts_edit", contact);
           break;
         case "remove":
-          navigator.mozLoop.confirm({
+          this.props.mozLoop.confirm({
             message: mozL10n.get("confirm_delete_contact_alert"),
             okButton: mozL10n.get("confirm_delete_contact_remove_button"),
             cancelButton: mozL10n.get("confirm_delete_contact_cancel_button")
           }, (error, result) => {
             if (error) {
               throw error;
             }
 
             if (!result) {
               return;
             }
 
-            navigator.mozLoop.contacts.remove(contact._guid, err => {
+            this.props.mozLoop.contacts.remove(contact._guid, err => {
               if (err) {
                 throw err;
               }
             });
           });
           break;
         case "block":
         case "unblock":
           // Invoke the API named like the action.
-          navigator.mozLoop.contacts[actionName](contact._guid, err => {
+          this.props.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
         case "video-call":
           if (!contact.blocked) {
-            navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+            this.props.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
             this.closeWindow();
           }
           break;
         case "audio-call":
           if (!contact.blocked) {
-            navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+            this.props.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
             this.closeWindow();
           }
           break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
@@ -549,17 +536,17 @@ loop.contacts = (function(_, mozL10n) {
       if (comp !== 0) {
         return comp;
       }
       // If names are equal, compare against unique ids to make sure we have
       // consistent ordering.
       return contact1._guid - contact2._guid;
     },
 
-    render: function() {
+    _renderContactsList: function() {
       let cx = React.addons.classSet;
 
       let viewForItem = item => {
         return (
           React.createElement(ContactDetail, {contact: item, 
                          handleContactAction: this.handleContactAction, 
                          key: item._guid})
         );
@@ -582,51 +569,79 @@ loop.contacts = (function(_, mozL10n) {
             shownContacts.available = shownContacts.available.filter(filterFn);
           }
           if (shownContacts.blocked) {
             shownContacts.blocked = shownContacts.blocked.filter(filterFn);
           }
         }
       }
 
+      if (shownContacts.available || shownContacts.blocked) {
+        return (
+          React.createElement("div", null, 
+            React.createElement("div", {className: "contact-list-title"}, 
+              mozL10n.get("contact_list_title")
+            ), 
+            React.createElement("ul", {className: "contact-list"}, 
+              shownContacts.available ?
+                shownContacts.available.sort(this.sortContacts).map(viewForItem) :
+                null, 
+              shownContacts.blocked && shownContacts.blocked.length > 0 ?
+                React.createElement("div", {className: "contact-separator"}, mozL10n.get("contacts_blocked_contacts")) :
+                null, 
+              shownContacts.blocked ?
+                shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
+                null
+            )
+          )
+        );
+      }
+
+      return (
+        React.createElement("div", {className: "contact-list-empty"}, 
+          React.createElement("p", {className: "panel-text-large"}, 
+            mozL10n.get("no_contacts_message_heading")
+          ), 
+          React.createElement("p", {className: "panel-text-medium"}, 
+            mozL10n.get("no_contacts_import_or_add")
+          )
+        )
+      );
+    },
+
+    render: function() {
+      let cx = React.addons.classSet;
+      let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
+                       MIN_CONTACTS_FOR_FILTERING;
+
       return (
         React.createElement("div", null, 
           React.createElement("div", {className: "content-area"}, 
             showFilter ?
             React.createElement("input", {className: "contact-filter", 
                    placeholder: mozL10n.get("contacts_search_placesholder"), 
                    valueLink: this.linkState("filter")})
             : null, 
             React.createElement(GravatarPromo, {handleUse: this.handleUseGravatar})
           ), 
-          React.createElement("ul", {className: "contact-list"}, 
-            shownContacts.available ?
-              shownContacts.available.sort(this.sortContacts).map(viewForItem) :
-              null, 
-            shownContacts.blocked && shownContacts.blocked.length > 0 ?
-              React.createElement("div", {className: "contact-separator"}, mozL10n.get("contacts_blocked_contacts")) :
-              null, 
-            shownContacts.blocked ?
-              shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
-              null
-          ), 
+          this._renderContactsList(), 
           React.createElement(ButtonGroup, {additionalClass: "contact-controls"}, 
             React.createElement(Button, {additionalClass: "secondary", 
-                    caption: this.state.importBusy
-                             ? mozL10n.get("importing_contacts_progress_button")
-                             : mozL10n.get("import_contacts_button3"), 
-                    disabled: this.state.importBusy, 
-                    onClick: this.handleImportButtonClick}, 
-              React.createElement("div", {className: cx({"contact-import-spinner": true,
-                                  spinner: true,
-                                  busy: this.state.importBusy})})
+              caption: this.state.importBusy
+                ? mozL10n.get("importing_contacts_progress_button")
+                : mozL10n.get("import_contacts_button3"), 
+                disabled: this.state.importBusy, 
+                onClick: this.handleImportButtonClick}, 
+                React.createElement("div", {className: cx({"contact-import-spinner": true,
+                                   spinner: true,
+                busy: this.state.importBusy})})
             ), 
             React.createElement(Button, {additionalClass: "primary", 
-                    caption: mozL10n.get("new_contact_button"), 
-                    onClick: this.handleAddContactButtonClick})
+              caption: mozL10n.get("new_contact_button"), 
+              onClick: this.handleAddContactButtonClick})
           )
         )
       );
     }
   });
 
   const ContactDetailsForm = React.createClass({displayName: "ContactDetailsForm",
     mixins: [React.addons.LinkedStateMixin],
@@ -761,14 +776,16 @@ loop.contacts = (function(_, mozL10n) {
                     onClick: this.handleAcceptButtonClick})
           )
         )
       );
     }
   });
 
   return {
+    ContactDropdown: ContactDropdown,
     ContactsList: ContactsList,
+    ContactDetail: ContactDetail,
     ContactDetailsForm: ContactDetailsForm,
     _getPreferred: getPreferred,
     _setPreferred: setPreferred
   };
 })(_, document.mozL10n);
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -180,57 +180,39 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     onItemClick: function(event) {
       this.props.handleAction(event.currentTarget.dataset.action);
     },
 
     render: function() {
       var cx = React.addons.classSet;
-
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
         <ul className={cx({ "dropdown-menu": true,
                             "dropdown-menu-up": this.state.openDirUp })}>
           <li className={cx({ "dropdown-menu-item": true,
-                              "disabled": this.props.blocked })}
-              data-action="video-call"
-              onClick={this.onItemClick}>
-            <i className="icon icon-video-call" />
-            {mozL10n.get("video_call_menu_button")}
-          </li>
-          <li className={cx({ "dropdown-menu-item": true,
-                              "disabled": this.props.blocked })}
-              data-action="audio-call"
-              onClick={this.onItemClick}>
-            <i className="icon icon-audio-call" />
-            {mozL10n.get("audio_call_menu_button")}
-          </li>
-          <li className={cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit })}
               data-action="edit"
               onClick={this.onItemClick}>
-            <i className="icon icon-edit" />
-            {mozL10n.get("edit_contact_menu_button")}
+            {mozL10n.get("edit_contact_title")}
           </li>
           <li className="dropdown-menu-item"
               data-action={blockAction}
               onClick={this.onItemClick}>
-            <i className={"icon icon-" + blockAction} />
             {mozL10n.get(blockLabel)}
           </li>
           <li className={cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit })}
-               data-action="remove"
-               onClick={this.onItemClick}>
-            <i className="icon icon-remove" />
-            {mozL10n.get("remove_contact_menu_button2")}
+              data-action="remove"
+              onClick={this.onItemClick}>
+            {mozL10n.get("confirm_delete_contact_remove_button")}
           </li>
         </ul>
       );
     }
   });
 
   const ContactDetail = React.createClass({
     getInitialState: function() {
@@ -290,38 +272,42 @@ loop.contacts = (function(_, mozL10n) {
       // We cannot modify imported contacts.  For the moment, the check for
       // determining whether the contact is imported is based on its category.
       return this.props.contact.category[0] != "google";
     },
 
     render: function() {
       let names = getContactNames(this.props.contact);
       let email = getPreferred(this.props.contact, "email");
+      let avatarSrc = navigator.mozLoop.getUserAvatar(email.value);
       let cx = React.addons.classSet;
       let contactCSSClass = cx({
         contact: true,
         blocked: this.props.contact.blocked
       });
+      let avatarCSSClass = cx({
+        avatar: true,
+        defaultAvatar: !avatarSrc
+      });
 
       return (
         <li className={contactCSSClass} onMouseLeave={this.hideDropdownMenu}>
-          <div className="avatar">
-            <img src={navigator.mozLoop.getUserAvatar(email.value)} />
+          <div className={avatarCSSClass}>
+            {avatarSrc ? <img src={avatarSrc} /> : null}
           </div>
           <div className="details">
             <div className="username"><strong>{names.firstName}</strong> {names.lastName}
-              <i className={cx({"icon icon-google": this.props.contact.category[0] == "google"})} />
               <i className={cx({"icon icon-blocked": this.props.contact.blocked})} />
             </div>
             <div className="email">{email.value}</div>
           </div>
           <div className="icons">
-            <i className="icon icon-video"
+            <i className="icon icon-contact-video-call"
                onClick={this.handleAction.bind(null, "video-call")} />
-            <i className="icon icon-caret-down"
+            <i className="icon icon-vertical-ellipsis"
                onClick={this.showDropdownMenu} />
           </div>
           {this.state.showMenu
             ? <ContactDropdown blocked={this.props.contact.blocked}
                                canEdit={this.canEdit()}
                                handleAction={this.handleAction} />
             : null
           }
@@ -332,20 +318,21 @@ loop.contacts = (function(_, mozL10n) {
 
   const ContactsList = React.createClass({
     mixins: [
       React.addons.LinkedStateMixin,
       loop.shared.mixins.WindowCloseMixin
     ],
 
     propTypes: {
+      mozLoop: React.PropTypes.object.isRequired,
       notifications: React.PropTypes.instanceOf(
-        loop.shared.models.NotificationCollection).isRequired,
-        // Callback to handle entry to the add/edit contact form.
-        startForm: React.PropTypes.func.isRequired
+                     loop.shared.models.NotificationCollection).isRequired,
+      // Callback to handle entry to the add/edit contact form.
+      startForm: React.PropTypes.func.isRequired
     },
 
     /**
      * Contacts collection object
      */
     contacts: null,
 
     /**
@@ -356,17 +343,17 @@ loop.contacts = (function(_, mozL10n) {
     getInitialState: function() {
       return {
         importBusy: false,
         filter: ""
       };
     },
 
     refresh: function(callback = function() {}) {
-      let contactsAPI = navigator.mozLoop.contacts;
+      let contactsAPI = this.props.mozLoop.contacts;
 
       this.handleContactRemoveAll();
 
       contactsAPI.getAll((err, contacts) => {
         if (err) {
           callback(err);
           return;
         }
@@ -388,28 +375,28 @@ loop.contacts = (function(_, mozL10n) {
         addContactsInChunks(contacts);
       });
     },
 
     componentWillMount: function() {
       // Take the time to initialize class variables that are used outside
       // `this.state`.
       this.contacts = {};
-      this._userProfile = navigator.mozLoop.userProfile;
+      this._userProfile = this.props.mozLoop.userProfile;
     },
 
     componentDidMount: function() {
       window.addEventListener("LoopStatusChanged", this._onStatusChanged);
 
       this.refresh(err => {
         if (err) {
           throw err;
         }
 
-        let contactsAPI = navigator.mozLoop.contacts;
+        let contactsAPI = this.props.mozLoop.contacts;
 
         // Listen for contact changes/ updates.
         contactsAPI.on("add", (eventName, contact) => {
           this.handleContactAddOrUpdate(contact);
         });
         contactsAPI.on("remove", (eventName, contact) => {
           this.handleContactRemove(contact);
         });
@@ -422,17 +409,17 @@ loop.contacts = (function(_, mozL10n) {
       });
     },
 
     componentWillUnmount: function() {
       window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
     },
 
     _onStatusChanged: function() {
-      let profile = navigator.mozLoop.userProfile;
+      let profile = this.props.mozLoop.userProfile;
       let currUid = this._userProfile ? this._userProfile.uid : null;
       let newUid = profile ? profile.uid : null;
       if (currUid != newUid) {
         // On profile change (login, logout), reload all contacts.
         this._userProfile = profile;
         // The following will do a forceUpdate() for us.
         this.refresh();
       }
@@ -460,17 +447,17 @@ loop.contacts = (function(_, mozL10n) {
     handleContactRemoveAll: function() {
       // Do not allow any race conditions when removing all contacts.
       this.contacts = {};
       this.forceUpdate();
     },
 
     handleImportButtonClick: function() {
       this.setState({ importBusy: true });
-      navigator.mozLoop.startImport({
+      this.props.mozLoop.startImport({
         service: "google"
       }, (err, stats) => {
         this.setState({ importBusy: false });
         if (err) {
           console.error("Contact import error", err);
           this.props.notifications.errorL10n("import_contacts_failure_message");
           return;
         }
@@ -486,54 +473,54 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleContactAction: function(contact, actionName) {
       switch (actionName) {
         case "edit":
           this.props.startForm("contacts_edit", contact);
           break;
         case "remove":
-          navigator.mozLoop.confirm({
+          this.props.mozLoop.confirm({
             message: mozL10n.get("confirm_delete_contact_alert"),
             okButton: mozL10n.get("confirm_delete_contact_remove_button"),
             cancelButton: mozL10n.get("confirm_delete_contact_cancel_button")
           }, (error, result) => {
             if (error) {
               throw error;
             }
 
             if (!result) {
               return;
             }
 
-            navigator.mozLoop.contacts.remove(contact._guid, err => {
+            this.props.mozLoop.contacts.remove(contact._guid, err => {
               if (err) {
                 throw err;
               }
             });
           });
           break;
         case "block":
         case "unblock":
           // Invoke the API named like the action.
-          navigator.mozLoop.contacts[actionName](contact._guid, err => {
+          this.props.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
         case "video-call":
           if (!contact.blocked) {
-            navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+            this.props.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
             this.closeWindow();
           }
           break;
         case "audio-call":
           if (!contact.blocked) {
-            navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+            this.props.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
             this.closeWindow();
           }
           break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
@@ -549,17 +536,17 @@ loop.contacts = (function(_, mozL10n) {
       if (comp !== 0) {
         return comp;
       }
       // If names are equal, compare against unique ids to make sure we have
       // consistent ordering.
       return contact1._guid - contact2._guid;
     },
 
-    render: function() {
+    _renderContactsList: function() {
       let cx = React.addons.classSet;
 
       let viewForItem = item => {
         return (
           <ContactDetail contact={item}
                          handleContactAction={this.handleContactAction}
                          key={item._guid} />
         );
@@ -582,51 +569,79 @@ loop.contacts = (function(_, mozL10n) {
             shownContacts.available = shownContacts.available.filter(filterFn);
           }
           if (shownContacts.blocked) {
             shownContacts.blocked = shownContacts.blocked.filter(filterFn);
           }
         }
       }
 
+      if (shownContacts.available || shownContacts.blocked) {
+        return (
+          <div>
+            <div className="contact-list-title">
+              {mozL10n.get("contact_list_title")}
+            </div>
+            <ul className="contact-list">
+              {shownContacts.available ?
+                shownContacts.available.sort(this.sortContacts).map(viewForItem) :
+                null}
+              {shownContacts.blocked && shownContacts.blocked.length > 0 ?
+                <div className="contact-separator">{mozL10n.get("contacts_blocked_contacts")}</div> :
+                null}
+              {shownContacts.blocked ?
+                shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
+                null}
+            </ul>
+          </div>
+        );
+      }
+
+      return (
+        <div className="contact-list-empty">
+          <p className="panel-text-large">
+            {mozL10n.get("no_contacts_message_heading")}
+          </p>
+          <p className="panel-text-medium">
+            {mozL10n.get("no_contacts_import_or_add")}
+          </p>
+        </div>
+      );
+    },
+
+    render: function() {
+      let cx = React.addons.classSet;
+      let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
+                       MIN_CONTACTS_FOR_FILTERING;
+
       return (
         <div>
           <div className="content-area">
             {showFilter ?
             <input className="contact-filter"
                    placeholder={mozL10n.get("contacts_search_placesholder")}
                    valueLink={this.linkState("filter")} />
             : null }
             <GravatarPromo handleUse={this.handleUseGravatar}/>
           </div>
-          <ul className="contact-list">
-            {shownContacts.available ?
-              shownContacts.available.sort(this.sortContacts).map(viewForItem) :
-              null}
-            {shownContacts.blocked && shownContacts.blocked.length > 0 ?
-              <div className="contact-separator">{mozL10n.get("contacts_blocked_contacts")}</div> :
-              null}
-            {shownContacts.blocked ?
-              shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
-              null}
-          </ul>
+          {this._renderContactsList()}
           <ButtonGroup additionalClass="contact-controls">
             <Button additionalClass="secondary"
-                    caption={this.state.importBusy
-                             ? mozL10n.get("importing_contacts_progress_button")
-                             : mozL10n.get("import_contacts_button3")}
-                    disabled={this.state.importBusy}
-                    onClick={this.handleImportButtonClick} >
-              <div className={cx({"contact-import-spinner": true,
-                                  spinner: true,
-                                  busy: this.state.importBusy})} />
+              caption={this.state.importBusy
+                ? mozL10n.get("importing_contacts_progress_button")
+                : mozL10n.get("import_contacts_button3")}
+                disabled={this.state.importBusy}
+                onClick={this.handleImportButtonClick} >
+                <div className={cx({"contact-import-spinner": true,
+                                   spinner: true,
+                busy: this.state.importBusy})} />
             </Button>
             <Button additionalClass="primary"
-                    caption={mozL10n.get("new_contact_button")}
-                    onClick={this.handleAddContactButtonClick} />
+              caption={mozL10n.get("new_contact_button")}
+              onClick={this.handleAddContactButtonClick} />
           </ButtonGroup>
         </div>
       );
     }
   });
 
   const ContactDetailsForm = React.createClass({
     mixins: [React.addons.LinkedStateMixin],
@@ -761,14 +776,16 @@ loop.contacts = (function(_, mozL10n) {
                     onClick={this.handleAcceptButtonClick} />
           </ButtonGroup>
         </div>
       );
     }
   });
 
   return {
+    ContactDropdown: ContactDropdown,
     ContactsList: ContactsList,
+    ContactDetail: ContactDetail,
     ContactDetailsForm: ContactDetailsForm,
     _getPreferred: getPreferred,
     _setPreferred: setPreferred
   };
 })(_, document.mozL10n);
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -311,35 +311,42 @@ loop.panel = (function(_, mozL10n) {
   });
 
   /**
    * Panel settings (gear) menu entry.
    */
   var SettingsDropdownEntry = React.createClass({displayName: "SettingsDropdownEntry",
     propTypes: {
       displayed: React.PropTypes.bool,
-      icon: React.PropTypes.string,
+      extraCSSClass: React.PropTypes.string,
       label: React.PropTypes.string.isRequired,
       onClick: React.PropTypes.func.isRequired
     },
 
     getDefaultProps: function() {
       return {displayed: true};
     },
 
     render: function() {
+      var cx = React.addons.classSet;
+
       if (!this.props.displayed) {
         return null;
       }
+
+      var extraCSSClass = {
+        "dropdown-menu-item": true
+      };
+      if (this.props.extraCSSClass) {
+        extraCSSClass[this.props.extraCSSClass] = true;
+      }
+
       return (
-        React.createElement("li", {className: "dropdown-menu-item", onClick: this.props.onClick}, 
-          this.props.icon ?
-            React.createElement("i", {className: "icon icon-" + this.props.icon}) :
-            null, 
-          React.createElement("span", null, this.props.label)
+        React.createElement("li", {className: cx(extraCSSClass), onClick: this.props.onClick}, 
+          this.props.label
         )
       );
     }
   });
 
   /**
    * Panel settings (gear) menu.
    */
@@ -380,42 +387,43 @@ loop.panel = (function(_, mozL10n) {
 
     openGettingStartedTour: function() {
       this.props.mozLoop.openGettingStartedTour("settings-menu");
       this.closeWindow();
     },
 
     render: function() {
       var cx = React.addons.classSet;
+      var accountEntryCSSClass = this._isSignedIn() ? "entry-settings-signout" :
+                                                      "entry-settings-signin";
 
       return (
         React.createElement("div", {className: "settings-menu dropdown"}, 
           React.createElement("button", {className: "button-settings", 
              onClick: this.toggleDropdownMenu, 
              ref: "menu-button", 
              title: mozL10n.get("settings_menu_button_tooltip")}), 
           React.createElement("ul", {className: cx({"dropdown-menu": true, hide: !this.state.showMenu})}, 
+            React.createElement(SettingsDropdownEntry, {
+                displayed: this._isSignedIn() && this.props.mozLoop.fxAEnabled, 
+                extraCSSClass: "entry-settings-account", 
+                label: mozL10n.get("settings_menu_item_account"), 
+                onClick: this.handleClickAccountEntry}), 
             React.createElement(SettingsDropdownEntry, {displayed: false, 
-                                   icon: "settings", 
                                    label: mozL10n.get("settings_menu_item_settings"), 
                                    onClick: this.handleClickSettingsEntry}), 
-            React.createElement(SettingsDropdownEntry, {displayed: this._isSignedIn() && this.props.mozLoop.fxAEnabled, 
-                                   icon: "account", 
-                                   label: mozL10n.get("settings_menu_item_account"), 
-                                   onClick: this.handleClickAccountEntry}), 
-            React.createElement(SettingsDropdownEntry, {icon: "tour", 
-                                   label: mozL10n.get("tour_label"), 
+            React.createElement(SettingsDropdownEntry, {label: mozL10n.get("tour_label"), 
                                    onClick: this.openGettingStartedTour}), 
             React.createElement(SettingsDropdownEntry, {displayed: this.props.mozLoop.fxAEnabled, 
-                                   icon: this._isSignedIn() ? "signout" : "signin", 
+                                   extraCSSClass: accountEntryCSSClass, 
                                    label: this._isSignedIn() ?
                                           mozL10n.get("settings_menu_item_signout") :
                                           mozL10n.get("settings_menu_item_signin"), 
                                    onClick: this.handleClickAuthEntry}), 
-            React.createElement(SettingsDropdownEntry, {icon: "help", 
+            React.createElement(SettingsDropdownEntry, {extraCSSClass: "entry-settings-help", 
                                    label: mozL10n.get("help_label"), 
                                    onClick: this.handleHelpEntry})
           )
         )
       );
     }
   });
 
@@ -947,20 +955,20 @@ loop.panel = (function(_, mozL10n) {
             selectedTab: this.props.selectedTab}, 
             React.createElement(Tab, {name: "rooms"}, 
               React.createElement(RoomList, {dispatcher: this.props.dispatcher, 
                         mozLoop: this.props.mozLoop, 
                         store: this.props.roomStore, 
                         userProfile: this.state.userProfile})
             ), 
             React.createElement(Tab, {name: "contacts"}, 
-              React.createElement(ContactsList, {
-                notifications: this.props.notifications, 
-                selectTab: this.selectTab, 
-                startForm: this.startForm})
+              React.createElement(ContactsList, {mozLoop: this.props.mozLoop, 
+                            notifications: this.props.notifications, 
+                            selectTab: this.selectTab, 
+                            startForm: this.startForm})
             ), 
             React.createElement(Tab, {hidden: true, name: "contacts_add"}, 
               React.createElement(ContactDetailsForm, {
                 mode: "add", 
                 ref: "contacts_add", 
                 selectTab: this.selectTab})
             ), 
             React.createElement(Tab, {hidden: true, name: "contacts_edit"}, 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -311,35 +311,42 @@ loop.panel = (function(_, mozL10n) {
   });
 
   /**
    * Panel settings (gear) menu entry.
    */
   var SettingsDropdownEntry = React.createClass({
     propTypes: {
       displayed: React.PropTypes.bool,
-      icon: React.PropTypes.string,
+      extraCSSClass: React.PropTypes.string,
       label: React.PropTypes.string.isRequired,
       onClick: React.PropTypes.func.isRequired
     },
 
     getDefaultProps: function() {
       return {displayed: true};
     },
 
     render: function() {
+      var cx = React.addons.classSet;
+
       if (!this.props.displayed) {
         return null;
       }
+
+      var extraCSSClass = {
+        "dropdown-menu-item": true
+      };
+      if (this.props.extraCSSClass) {
+        extraCSSClass[this.props.extraCSSClass] = true;
+      }
+
       return (
-        <li className="dropdown-menu-item" onClick={this.props.onClick}>
-          {this.props.icon ?
-            <i className={"icon icon-" + this.props.icon}></i> :
-            null}
-          <span>{this.props.label}</span>
+        <li className={cx(extraCSSClass)} onClick={this.props.onClick}>
+          {this.props.label}
         </li>
       );
     }
   });
 
   /**
    * Panel settings (gear) menu.
    */
@@ -380,42 +387,43 @@ loop.panel = (function(_, mozL10n) {
 
     openGettingStartedTour: function() {
       this.props.mozLoop.openGettingStartedTour("settings-menu");
       this.closeWindow();
     },
 
     render: function() {
       var cx = React.addons.classSet;
+      var accountEntryCSSClass = this._isSignedIn() ? "entry-settings-signout" :
+                                                      "entry-settings-signin";
 
       return (
         <div className="settings-menu dropdown">
           <button className="button-settings"
              onClick={this.toggleDropdownMenu}
              ref="menu-button"
              title={mozL10n.get("settings_menu_button_tooltip")} />
           <ul className={cx({"dropdown-menu": true, hide: !this.state.showMenu})}>
+            <SettingsDropdownEntry
+                displayed={this._isSignedIn() && this.props.mozLoop.fxAEnabled}
+                extraCSSClass="entry-settings-account"
+                label={mozL10n.get("settings_menu_item_account")}
+                onClick={this.handleClickAccountEntry} />
             <SettingsDropdownEntry displayed={false}
-                                   icon="settings"
                                    label={mozL10n.get("settings_menu_item_settings")}
                                    onClick={this.handleClickSettingsEntry} />
-            <SettingsDropdownEntry displayed={this._isSignedIn() && this.props.mozLoop.fxAEnabled}
-                                   icon="account"
-                                   label={mozL10n.get("settings_menu_item_account")}
-                                   onClick={this.handleClickAccountEntry} />
-            <SettingsDropdownEntry icon="tour"
-                                   label={mozL10n.get("tour_label")}
+            <SettingsDropdownEntry label={mozL10n.get("tour_label")}
                                    onClick={this.openGettingStartedTour} />
             <SettingsDropdownEntry displayed={this.props.mozLoop.fxAEnabled}
-                                   icon={this._isSignedIn() ? "signout" : "signin"}
+                                   extraCSSClass={accountEntryCSSClass}
                                    label={this._isSignedIn() ?
                                           mozL10n.get("settings_menu_item_signout") :
                                           mozL10n.get("settings_menu_item_signin")}
                                    onClick={this.handleClickAuthEntry} />
-            <SettingsDropdownEntry icon="help"
+            <SettingsDropdownEntry extraCSSClass="entry-settings-help"
                                    label={mozL10n.get("help_label")}
                                    onClick={this.handleHelpEntry} />
           </ul>
         </div>
       );
     }
   });
 
@@ -947,20 +955,20 @@ loop.panel = (function(_, mozL10n) {
             selectedTab={this.props.selectedTab}>
             <Tab name="rooms">
               <RoomList dispatcher={this.props.dispatcher}
                         mozLoop={this.props.mozLoop}
                         store={this.props.roomStore}
                         userProfile={this.state.userProfile} />
             </Tab>
             <Tab name="contacts">
-              <ContactsList
-                notifications={this.props.notifications}
-                selectTab={this.selectTab}
-                startForm={this.startForm} />
+              <ContactsList mozLoop={this.props.mozLoop}
+                            notifications={this.props.notifications}
+                            selectTab={this.selectTab}
+                            startForm={this.startForm} />
             </Tab>
             <Tab hidden={true} name="contacts_add">
               <ContactDetailsForm
                 mode="add"
                 ref="contacts_add"
                 selectTab={this.selectTab} />
             </Tab>
             <Tab hidden={true} name="contacts_edit">
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -331,16 +331,26 @@ p {
 .icon-audio,
 .icon-video {
   background-size: 20px;
   background-repeat: no-repeat;
   vertical-align: top;
   background-position: 80% center;
 }
 
+.pseudo-icon:before {
+  content: "";
+  display: inline-block;
+  background-repeat: no-repeat;
+  width: 14px;
+  height: 14px;
+  vertical-align: top;
+  margin: 0 .7rem;
+}
+
 .icon-small {
   background-size: 10px;
 }
 
 .icon-video {
   background-image: url("../img/video-inverse-14x14.png");
 }
 
@@ -414,46 +424,61 @@ p {
 .dropdown {
   position: relative;
 }
 
 .dropdown-menu {
   position: absolute;
   bottom: 0;
   left: 0;
-  background-color: #fdfdfd;
+  background-color: #fbfbfb;
   box-shadow: 0 1px 3px rgba(0,0,0,.3);
   list-style: none;
   border-radius: 2px;
 }
 
 html[dir="rtl"] .dropdown-menu {
   left: auto;
   right: 0;
 }
 
 .dropdown-menu-item {
   width: 100%;
   text-align: start;
-  padding: .5em 15px;
+  padding: .3rem .8rem;
   cursor: pointer;
   border: 1px solid transparent;
-  font-size: 1em;
+  font-size: 1.2rem;
+  line-height: 22px;
   white-space: nowrap;
+  color: #4a4a4a;
+}
+
+.dropdown-menu-item:first-child {
+  padding-top: .8rem;
+}
+
+.dropdown-menu-item:last-child {
+  padding-bottom: .8rem;
+}
+
+.dropdown-menu-item:first-child:hover {
+  border-top-right-radius: 2px;
+  border-top-left-radius: 2px;
+}
+
+.dropdown-menu-item:last-child {
+  border-bottom-right-radius: 2px;
+  border-bottom-left-radius: 2px;
 }
 
 .dropdown-menu-item:hover {
   background-color: #dbf7ff;
 }
 
-.dropdown-menu-item > .icon {
-  background-repeat: no-repeat;
-  display: inline-block;
-}
-
 .dropdown-menu-separator {
   height: 1px;
   margin: 2px -2px 1px -2px;
   border-top: 1px solid #dedede;
   background-color: #fff;
 }
 
 /* Custom checkbox */
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/avatars.svg
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 36 36">
+  <style>
+    use:not(:target) {
+      display: none;
+    }
+    use {
+      fill: #ccc;
+    }
+    use[id$="-hover"] {
+      fill: #444;
+    }
+    use[id$="-active"] {
+      fill: #0095dd;
+    }
+    use[id$="-white"] {
+      fill: #fff;
+    }
+  </style>
+  <defs>
+    <g id="blue" transform="translate(-2588 -413) translate(2588 413)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#4A90E2"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#4A90E2"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="orange" transform="translate(-2638 -317) translate(2638 317)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#F3A35C"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#F3A35C"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="mintgreen" transform="translate(-2588 -317) translate(2588 317)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#50E2C2"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#50E2C2"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="lightpink" transform="translate(-2687 -366) translate(2687 366)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#E364A1"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#E364A1"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="grey" transform="translate(-2736 -366) translate(2736 366)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#9B9B9B"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#9B9B9B"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="yellow" transform="translate(-2732 -317) translate(2732 317)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#F3E968"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#F3E968"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="purple" transform="translate(-2588 -366) translate(2588 366)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#9C61AF"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#9C61AF"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="lightgreen" transform="translate(-2686 -317) translate(2686 317)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#9AC967"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#9AC967"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="darkblue" transform="translate(-2686 -413) translate(2686 413)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#607CAE"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#607CAE"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="darkpink" transform="translate(-2638 -413) translate(2638 413)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#CE4D6E"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#CE4D6E"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="brown" transform="translate(-2736 -413) translate(2736 413)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#8A572A"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#8A572A"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+    <g id="green" transform="translate(-2638 -366) translate(2637.857 366)" fill="none">
+      <path d="M18 36c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18zm-12.021-5.979c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#56B397"/><path d="M5.979 30.021c3.076 3.076 7.326 4.979 12.021 4.979 9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17c0 4.694 1.903 8.944 4.979 12.021z" fill="#56B397"/><path d="M30.779 29.212c-3.116 3.548-7.686 5.788-12.779 5.788-4.694 0-8.944-1.903-12.021-4.979l-.181-.183c2.676-4.187 7.364-6.962 12.701-6.962 5.066 0 9.549 2.501 12.279 6.337z" fill-opacity=".7" fill="#fff"/><path d="M18 35c9.389 0 17-7.611 17-17s-7.611-17-17-17v34z" fill-opacity=".1" fill="#000"/><ellipse fill-opacity=".7" fill="#fff" cx="18.5" cy="12.932" rx="8.002" ry="8.002"/>
+    </g>
+  </defs>
+  <use id="blue-avatar" xlink:href="#blue"/>
+  <use id="orange-avatar" xlink:href="#orange"/>
+  <use id="mintgreen-avatar" xlink:href="#mintgreen"/>
+  <use id="lightpink-avatar" xlink:href="#lightpink"/>
+  <use id="grey-avatar" xlink:href="#grey"/>
+  <use id="yellow-avatar" xlink:href="#yellow"/>
+  <use id="purple-avatar" xlink:href="#purple"/>
+  <use id="lightgreen-avatar" xlink:href="#lightgreen"/>
+  <use id="darkblue-avatar" xlink:href="#darkblue"/>
+  <use id="darkpink-avatar" xlink:href="#darkpink"/>
+  <use id="brown-avatar" xlink:href="#brown"/>
+  <use id="green-avatar" xlink:href="#green"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/ellipsis-v.svg
@@ -0,0 +1,1 @@
+<svg width="4" height="20" viewBox="0 0 4 20" xmlns="http://www.w3.org/2000/svg"><g fill="#3A99DA"><ellipse cx="2" cy="2" rx="2" ry="2"/><ellipse cx="2" cy="10" rx="2" ry="2"/><ellipse cx="2" cy="18" rx="2" ry="2"/></g></svg>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/empty_contacts.svg
@@ -0,0 +1,1 @@
+<svg width="117" height="91" viewBox="0 0 117 91" xmlns="http://www.w3.org/2000/svg"><g fill="#D8D8D8"><path d="M116.431 59.357c-4.793 5.459-11.824 8.905-19.66 8.905-7.222 0-13.761-2.927-18.494-7.66l-.278-.282c4.116-6.441 11.33-10.712 19.541-10.712 7.795 0 14.69 3.848 18.891 9.749zm-18.891-12.736c6.799 0 12.311-5.512 12.311-12.311s-5.512-12.311-12.311-12.311-12.311 5.512-12.311 12.311 5.512 12.311 12.311 12.311zM38.431 59.357c-4.793 5.459-11.824 8.905-19.66 8.905-7.222 0-13.761-2.927-18.494-7.66l-.278-.282c4.116-6.441 11.33-10.712 19.541-10.712 7.795 0 14.69 3.848 18.891 9.749zm-18.891-12.736c6.799 0 12.311-5.512 12.311-12.311s-5.512-12.311-12.311-12.311-12.311 5.512-12.311 12.311 5.512 12.311 12.311 12.311z" id="Mask-Copy-5" fill-opacity=".8"/><path d="M91.495 70.608c-8.418 9.588-20.766 15.639-34.528 15.639-12.684 0-24.167-5.141-32.479-13.453l-.488-.495c7.229-11.312 19.898-18.812 34.318-18.812 13.689 0 25.8 6.759 33.177 17.121zm-33.177-22.367c11.941 0 21.621-9.68 21.621-21.621 0-11.941-9.68-21.621-21.621-21.621-11.941 0-21.621 9.68-21.621 21.621 0 11.941 9.68 21.621 21.621 21.621z" stroke="#FBFBFB" stroke-width="4"/></g></svg>
\ No newline at end of file
--- a/browser/components/loop/content/shared/img/icons-14x14.svg
+++ b/browser/components/loop/content/shared/img/icons-14x14.svg
@@ -30,17 +30,17 @@
     <path id="incoming-shape" fill-rule="evenodd" d="M2.745,7.558l0.637,0.669c0.04,0.041,0.085,0.073,0.134,0.1 l3.249,3.313c0.38,0.393,0.915,0.478,1.197,0.186l0.638-0.676c0.281-0.292,0.2-0.848-0.18-1.244L7.097,8.558h3.566 c0.419,0,0.759-0.34,0.759-0.759V6.28c0-0.419-0.34-0.759-0.759-0.759H7.059l1.42-1.443c0.381-0.392,0.461-0.945,0.18-1.234 l-0.637-0.67C7.74,1.883,7.204,1.966,6.824,2.359L3.55,5.688C3.487,5.717,3.43,5.755,3.381,5.806L2.745,6.482 c-0.131,0.137-0.183,0.332-0.162,0.54C2.562,7.229,2.613,7.423,2.745,7.558z"/>
     <path id="link-shape" fill-rule="evenodd" d="M7.359,6.107c0.757-0.757,0.757-1.995,0-2.752 L5.573,1.568c-0.757-0.757-1.995-0.757-2.752,0L1.568,2.82c-0.757,0.757-0.757,1.995,0,2.752l1.787,1.787 c0.757,0.757,1.995,0.757,2.752,0L6.266,7.2L6.8,7.734L6.641,7.893c-0.757,0.757-0.757,1.995,0,2.752l1.787,1.787 c0.757,0.757,1.995,0.757,2.752,0l1.253-1.253c0.757-0.757,0.757-1.995,0-2.752l-1.787-1.787c-0.757-0.757-1.995-0.757-2.752,0 L7.734,6.8L7.2,6.266L7.359,6.107z M9.87,7.868l1.335,1.335c0.294,0.294,0.294,0.774,0,1.068l-0.934,0.934 c-0.294,0.294-0.774,0.294-1.068,0L7.868,9.87c-0.294-0.294-0.294-0.774,0-1.068L8.13,9.064c0.294,0.294,0.744,0.324,1.001,0.067 C9.388,8.874,9.358,8.424,9.064,8.13L8.802,7.868C9.096,7.574,9.577,7.574,9.87,7.868z M4.13,6.132L2.795,4.797 c-0.294-0.294-0.294-0.774,0-1.068l0.934-0.934c0.294-0.294,0.774-0.294,1.068,0L6.132,4.13c0.294,0.294,0.294,0.774,0,1.068 L5.86,4.926C5.567,4.632,5.116,4.602,4.859,4.859C4.602,5.116,4.632,5.567,4.926,5.86l0.272,0.272 C4.904,6.426,4.423,6.426,4.13,6.132z"/>
     <g id="mute-shape">
       <path fill-rule="evenodd" d="M5.186,9.492L5.49,9.188l3.822-3.822l2.354-2.354l-0.848-0.848 L9.312,3.669V3.142C9.312,1.959,8.352,1,7.169,1C5.986,1,5.026,1.959,5.026,3.142v4.715c0,0.032,0.001,0.064,0.002,0.096 L4.643,8.338c-0.03-0.156-0.046-0.317-0.046-0.481V6.142H3.741v1.715c0,0.414,0.073,0.81,0.208,1.176l-1.615,1.615l0.848,0.848 l1.398-1.398v0L5.186,9.492z"/>
       <path fill-rule="evenodd" d="M9.312,7.857V6.045L5.829,9.528C6.196,9.824,6.662,10,7.169,10 C8.352,10,9.312,9.04,9.312,7.857z"/>
       <path fill-rule="evenodd" d="M9.741,7.857c0,1.42-1.151,2.572-2.572,2.572 c-0.625,0-1.199-0.223-1.645-0.595l-0.605,0.605c0.395,0.344,0.87,0.599,1.393,0.734v0.97H5.884c-0.56,0-1.034,0.359-1.212,0.858 h4.994c-0.178-0.499-0.652-0.858-1.212-0.858H8.026v-0.97c1.478-0.38,2.572-1.718,2.572-3.316V6.142H9.741V7.857z"/>
     </g>
     <path id="pause-shape" fill-rule="evenodd" d="M4.75,1h-1.5C2.836,1,2.5,1.336,2.5,1.75v10.5 C2.5,12.664,2.836,13,3.25,13h1.5c0.414,0,0.75-0.336,0.75-0.75V1.75C5.5,1.336,5.164,1,4.75,1z M10.75,1h-1.5 C8.836,1,8.5,1.336,8.5,1.75v10.5C8.5,12.664,8.836,13,9.25,13h1.5c0.414,0,0.75-0.336,0.75-0.75V1.75C11.5,1.336,11.164,1,10.75,1 z"/>
-    <path id="video-shape" fill-rule="evenodd" d="M12.175,3.347L9.568,5.651V3.905c0-0.657-0.497-1.19-1.111-1.19 H2.111C1.498,2.714,1,3.247,1,3.905v6.191c0,0.658,0.498,1.19,1.111,1.19h6.345c0.614,0,1.111-0.533,1.111-1.19V8.322l2.607,2.305 C12.4,10.867,12.71,10.938,13,10.874V3.099C12.71,3.035,12.4,3.106,12.175,3.347z"/>
+    <path id="video-shape" d="M1.59247473,11.4075253 C1.9956945,11.7983901 2.46237364,12 3.02957694,12 L7.98333957,12 C8.53762636,12 9.01666043,11.7983901 9.40752527,11.4075253 C9.81130663,11.0043055 10,10.5376264 10,9.97042306 L10,8.81074504 L12.8360165,11.6467615 C12.9247473,11.7354923 13.0252714,11.7731187 13.1516286,11.7731187 C13.2145264,11.7731187 13.2650693,11.7607638 13.3279671,11.7354923 C13.517222,11.659678 13.6053912,11.5209659 13.6053912,11.319356 L13.6053912,3.66772744 C13.6053912,3.47903407 13.517222,3.34032198 13.3279671,3.25215275 C13.2650693,3.23923624 13.2145264,3.22688132 13.1516286,3.22688132 C13.0252714,3.22688132 12.9247473,3.26450768 12.8360165,3.3526769 L10,6.17633845 L10,5.01666043 C10,4.46181206 9.81130663,3.98333957 9.40752527,3.59247473 C9.01666043,3.18869337 8.53762636,3 7.98333957,3 L3.02957694,3 C2.46237364,3 1.9956945,3.18869337 1.59247473,3.59247473 C1.20160988,3.98333957 1,4.46181206 1,5.01666043 L1,9.97042306 C1,10.5376264 1.20160988,11.0043055 1.59247473,11.4075253" fill="#fff" fill-rule="evenodd"/>
     <g id="volume-shape">
       <path fill-rule="evenodd" d="M3.513,4.404H1.896c-0.417,0-0.756,0.338-0.756,0.755v3.679 c0,0.417,0.338,0.755,0.756,0.755H3.51l2.575,2.575c0.261,0.261,0.596,0.4,0.938,0.422V1.409C6.682,1.431,6.346,1.57,6.085,1.831 L3.513,4.404z M8.555,5.995C8.619,6.32,8.653,6.656,8.653,7c0,0.344-0.034,0.679-0.098,1.004l0.218,0.142 C8.852,7.777,8.895,7.393,8.895,7c0-0.394-0.043-0.777-0.123-1.147L8.555,5.995z M12.224,3.6l-0.475,0.31 c0.359,0.962,0.557,2.003,0.557,3.09c0,1.087-0.198,2.128-0.557,3.09l0.475,0.31c0.41-1.054,0.635-2.201,0.635-3.4 C12.859,5.8,12.634,4.654,12.224,3.6z M10.061,5.012C10.25,5.642,10.353,6.308,10.353,7c0,0.691-0.103,1.358-0.293,1.987 l0.351,0.229C10.634,8.517,10.756,7.772,10.756,7c0-0.773-0.121-1.517-0.345-2.216L10.061,5.012z"/>
       <path d="M7.164,12.74l-0.15-0.009c-0.389-0.024-0.754-0.189-1.028-0.463L3.452,9.735H1.896 C1.402,9.735,1,9.333,1,8.838V5.16c0-0.494,0.402-0.896,0.896-0.896h1.558l2.531-2.531C6.26,1.458,6.625,1.293,7.014,1.269 l0.15-0.009V12.74z M1.896,4.545c-0.339,0-0.615,0.276-0.615,0.615v3.679c0,0.339,0.276,0.615,0.615,0.615h1.672l2.616,2.616 c0.19,0.19,0.434,0.316,0.697,0.363V1.568C6.619,1.615,6.375,1.741,6.185,1.931L3.571,4.545H1.896z M12.292,10.612l-0.714-0.467 l0.039-0.105C11.981,9.067,12.165,8.044,12.165,7c0-1.044-0.184-2.067-0.548-3.041l-0.039-0.105l0.714-0.467l0.063,0.162 C12.783,4.649,13,5.81,13,7s-0.217,2.351-0.645,3.451L12.292,10.612z M11.92,10.033l0.234,0.153 c0.374-1.019,0.564-2.09,0.564-3.186s-0.19-2.167-0.564-3.186L11.92,3.966C12.27,4.94,12.447,5.96,12.447,7 C12.447,8.04,12.27,9.059,11.92,10.033z M10.489,9.435L9.895,9.047l0.031-0.101C10.116,8.315,10.212,7.66,10.212,7 c0-0.661-0.096-1.316-0.287-1.947L9.895,4.952l0.594-0.388l0.056,0.176C10.779,5.471,10.897,6.231,10.897,7 c0,0.769-0.118,1.529-0.351,2.259L10.489,9.435z M10.225,8.926l0.106,0.069C10.52,8.348,10.615,7.677,10.615,7 c0-0.677-0.095-1.348-0.284-1.996l-0.106,0.07C10.403,5.699,10.494,6.347,10.494,7C10.494,7.652,10.403,8.3,10.225,8.926z M8.867,8.376L8.398,8.07l0.018-0.093C8.48,7.654,8.512,7.325,8.512,7S8.48,6.345,8.417,6.022L8.398,5.929l0.469-0.306l0.043,0.2 C8.994,6.211,9.036,6.607,9.036,7c0,0.393-0.042,0.789-0.126,1.176L8.867,8.376z"/>
     </g>
     <path id="contacts-shape" fill-rule="evenodd" transform="translate(-79.000000, -59.000000)" d="M91.5000066,69.9765672 C91.5000066,68.2109401 91.0859436,65.4999994 88.7968783,65.4999994 C88.5546906,65.4999994 87.5312518,66.5859382 86,66.5859382 C84.4687482,66.5859382 83.4453095,65.4999994 83.2031217,65.4999994 C80.9140564,65.4999994 80.4999935,68.2109401 80.4999935,69.9765672 C80.4999935,71.2421938 81.3437445,72.0000072 82.5859334,72.0000072 L89.4140666,72.0000072 C90.6562555,72.0000072 91.5000066,71.2421938 91.5000066,69.9765672 L91.5000066,69.9765672 L91.5000066,69.9765672 Z M89.0000036,62.9999964 C89.0000036,61.3437444 87.656252,59.9999928 86,59.9999928 C84.343748,59.9999928 82.9999964,61.3437444 82.9999964,62.9999964 C82.9999964,64.6562484 84.343748,66 86,66 C87.656252,66 89.0000036,64.6562484 89.0000036,62.9999964 L89.0000036,62.9999964 L89.0000036,62.9999964 Z" />
     <path id="hello-shape" fill-rule="evenodd" transform="translate(-261.000000, -59.000000)" d="M268.273778,60 C264.809073,60 262,62.4730749 262,65.523237 C262,67.0417726 262.697086,68.4174001 263.822897,69.4155754 C263.627626,70.1061164 263.240356,71.0442922 262.474542,71.959559 C262.605451,72.1919211 264.761073,71.3737446 266.2807,70.7617485 C266.907968,70.946111 267.577782,71.046474 268.274868,71.046474 C271.740664,71.046474 274.549737,68.5733991 274.549737,65.523237 C274.549737,62.4730749 271.739573,60 268.274868,60 L268.273778,60 Z M270.15122,63.3119786 C270.609399,63.3119786 270.980306,63.6850671 270.980306,64.1432459 C270.980306,64.6036066 270.609399,64.9756042 270.15122,64.9756042 C269.693041,64.9756042 269.321044,64.6036066 269.321044,64.1432459 C269.321044,63.6850671 269.693041,63.3119786 270.15122,63.3119786 L270.15122,63.3119786 Z M266.36579,63.3119786 C266.823969,63.3119786 267.195966,63.6850671 267.195966,64.1432459 C267.195966,64.6036066 266.823969,64.9756042 266.36579,64.9756042 C265.907611,64.9756042 265.535613,64.6036066 265.535613,64.1432459 C265.535613,63.6850671 265.907611,63.3119786 266.36579,63.3119786 L266.36579,63.3119786 Z M268.283596,69.3675757 L268.258505,69.3664848 L268.233414,69.3675757 C266.557789,69.3675757 264.685801,68.2777646 264.254894,66.4428674 C265.38616,66.9675913 266.967968,67.1966807 268.258505,67.1966807 C269.549042,67.1966807 271.13085,66.9675913 272.262115,66.4428674 C271.8323,68.2777646 269.959221,69.3675757 268.283596,69.3675757 L268.283596,69.3675757 Z" />
   </defs>
   <use id="audio" xlink:href="#audio-shape"/>
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -67,16 +67,19 @@ browser.jar:
   content/browser/loop/shared/img/movistar.png                  (content/shared/img/movistar.png)
   content/browser/loop/shared/img/movistar@2x.png               (content/shared/img/movistar@2x.png)
   content/browser/loop/shared/img/vivo.png                      (content/shared/img/vivo.png)
   content/browser/loop/shared/img/vivo@2x.png                   (content/shared/img/vivo@2x.png)
   content/browser/loop/shared/img/02.png                        (content/shared/img/02.png)
   content/browser/loop/shared/img/02@2x.png                     (content/shared/img/02@2x.png)
   content/browser/loop/shared/img/telefonica.png                (content/shared/img/telefonica.png)
   content/browser/loop/shared/img/telefonica@2x.png             (content/shared/img/telefonica@2x.png)
+  content/browser/loop/shared/img/ellipsis-v.svg                (content/shared/img/ellipsis-v.svg)
+  content/browser/loop/shared/img/empty_contacts.svg            (content/shared/img/empty_contacts.svg)
+  content/browser/loop/shared/img/avatars.svg                   (content/shared/img/avatars.svg)
 
   # Shared scripts
   content/browser/loop/shared/js/actions.js             (content/shared/js/actions.js)
   content/browser/loop/shared/js/conversationStore.js   (content/shared/js/conversationStore.js)
   content/browser/loop/shared/js/store.js               (content/shared/js/store.js)
   content/browser/loop/shared/js/roomStates.js          (content/shared/js/roomStates.js)
   content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js)
   content/browser/loop/shared/js/activeRoomStore.js     (content/shared/js/activeRoomStore.js)
--- a/browser/components/loop/modules/MozLoopAPI.jsm
+++ b/browser/components/loop/modules/MozLoopAPI.jsm
@@ -893,31 +893,30 @@ function injectLoopAPI(targetWindow) {
 
         request.send();
       }
     },
 
     /**
      * Compose a URL pointing to the location of an avatar by email address.
      * At the moment we use the Gravatar service to match email addresses with
-     * avatars. This might change in the future as avatars might come from another
-     * source.
+     * avatars. If no email address is found we return null.
      *
      * @param {String} emailAddress Users' email address
      * @param {Number} size         Size of the avatar image to return in pixels.
      *                              Optional. Default value: 40.
-     * @return the URL pointing to an avatar matching the provided email address.
+     * @return the URL pointing to an avatar matching the provided email address
+     *         or null if this is not available.
      */
     getUserAvatar: {
       enumerable: true,
       writable: true,
       value: function(emailAddress, size = 40) {
-        const kEmptyGif = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
         if (!emailAddress || !MozLoopService.getLoopPref("contacts.gravatars.show")) {
-          return kEmptyGif;
+          return null;
         }
 
         // Do the MD5 dance.
         let hasher = Cc["@mozilla.org/security/hash;1"]
                        .createInstance(Ci.nsICryptoHash);
         hasher.init(Ci.nsICryptoHash.MD5);
         let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
                              .createInstance(Ci.nsIStringInputStream);
--- a/browser/components/loop/test/desktop-local/contacts_test.js
+++ b/browser/components/loop/test/desktop-local/contacts_test.js
@@ -71,19 +71,23 @@ describe("loop.contacts", function() {
     published: 1406798311748,
     updated: 1406798311748
   }];
   var sandbox;
   var fakeWindow;
   var notifications;
   var listView;
   var oldMozLoop = navigator.mozLoop;
+  var mozL10nGetSpy;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
+
+    mozL10nGetSpy = sandbox.spy(document.mozL10n, "get");
+
     navigator.mozLoop = {
       getStrings: function(entityName) {
         var textContentValue = "fakeText";
         if (entityName == "add_contact_button") {
           textContentValue = fakeAddContactButtonText;
         } else if (entityName == "edit_contact_title") {
           textContentValue = fakeEditContactButtonText;
         } else if (entityName == "edit_contact_done_button") {
@@ -106,16 +110,20 @@ describe("loop.contacts", function() {
         }
         return "gravatarsDisabled";
       },
       contacts: {
         getAll: function(callback) {
           callback(null, [].concat(fakeContacts));
         },
         on: sandbox.stub()
+      },
+      calls: {
+        startDirectCall: function() {},
+        clearCallInProgress: function() {}
       }
     };
 
     fakeWindow = {
       close: sandbox.stub()
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
@@ -141,140 +149,216 @@ describe("loop.contacts", function() {
       // Sanity check the reverse:
       gravatars = node.querySelectorAll(".contact img[src=gravatarsDisabled]");
       expect(gravatars.length).to.equal(enabled ? 0 : fakeContacts.length);
     }
 
     it("should show the gravatars promo box", function() {
       listView = TestUtils.renderIntoDocument(
         React.createElement(loop.contacts.ContactsList, {
+          mozLoop: navigator.mozLoop,
           notifications: notifications,
           startForm: function() {}
         }));
 
       var promo = listView.getDOMNode().querySelector(".contacts-gravatar-promo");
       expect(promo).to.not.equal(null);
 
       checkGravatarContacts(false);
     });
 
     it("should not show the gravatars promo box when the 'contacts.gravatars.promo' pref is set", function() {
-      navigator.mozLoop.getLoopPref = function(pref) {
+      sandbox.stub(navigator.mozLoop, "getLoopPref", function(pref) {
         if (pref == "contacts.gravatars.promo") {
           return false;
         } else if (pref == "contacts.gravatars.show") {
           return true;
         }
         return "";
-      };
+      });
+
       listView = TestUtils.renderIntoDocument(
         React.createElement(loop.contacts.ContactsList, {
+          mozLoop: navigator.mozLoop,
           notifications: notifications,
           startForm: function() {}
         }));
 
       var promo = listView.getDOMNode().querySelector(".contacts-gravatar-promo");
       expect(promo).to.equal(null);
 
       checkGravatarContacts(true);
     });
 
     it("should hide the gravatars promo box when the 'use' button is clicked", function() {
       listView = TestUtils.renderIntoDocument(
         React.createElement(loop.contacts.ContactsList, {
+          mozLoop: navigator.mozLoop,
           notifications: notifications,
           startForm: function() {}
         }));
 
       React.addons.TestUtils.Simulate.click(listView.getDOMNode().querySelector(
         ".contacts-gravatar-promo .button-accept"));
 
       sinon.assert.calledTwice(navigator.mozLoop.setLoopPref);
 
       var promo = listView.getDOMNode().querySelector(".contacts-gravatar-promo");
       expect(promo).to.equal(null);
     });
 
     it("should should set the prefs correctly when the 'use' button is clicked", function() {
       listView = TestUtils.renderIntoDocument(
         React.createElement(loop.contacts.ContactsList, {
+          mozLoop: navigator.mozLoop,
           notifications: notifications,
           startForm: function() {}
         }));
 
       React.addons.TestUtils.Simulate.click(listView.getDOMNode().querySelector(
         ".contacts-gravatar-promo .button-accept"));
 
       sinon.assert.calledTwice(navigator.mozLoop.setLoopPref);
       sinon.assert.calledWithExactly(navigator.mozLoop.setLoopPref, "contacts.gravatars.promo", false);
       sinon.assert.calledWithExactly(navigator.mozLoop.setLoopPref, "contacts.gravatars.show", true);
     });
 
     it("should hide the gravatars promo box when the 'close' button is clicked", function() {
       listView = TestUtils.renderIntoDocument(
         React.createElement(loop.contacts.ContactsList, {
+          mozLoop: navigator.mozLoop,
           notifications: notifications,
           startForm: function() {}
         }));
 
       React.addons.TestUtils.Simulate.click(listView.getDOMNode().querySelector(
         ".contacts-gravatar-promo .button-close"));
 
       var promo = listView.getDOMNode().querySelector(".contacts-gravatar-promo");
       expect(promo).to.equal(null);
     });
 
     it("should set prefs correctly when the 'close' button is clicked", function() {
       listView = TestUtils.renderIntoDocument(
         React.createElement(loop.contacts.ContactsList, {
+          mozLoop: navigator.mozLoop,
           notifications: notifications,
           startForm: function() {}
         }));
 
       React.addons.TestUtils.Simulate.click(listView.getDOMNode().querySelector(
         ".contacts-gravatar-promo .button-close"));
 
       sinon.assert.calledOnce(navigator.mozLoop.setLoopPref);
       sinon.assert.calledWithExactly(navigator.mozLoop.setLoopPref,
         "contacts.gravatars.promo", false);
     });
   });
 
   describe("ContactsList", function () {
+    var node;
     beforeEach(function() {
-      navigator.mozLoop.calls = {
-        startDirectCall: sandbox.stub(),
-        clearCallInProgress: sandbox.stub()
-      };
-      navigator.mozLoop.contacts = {getAll: sandbox.stub()};
+      sandbox.stub(navigator.mozLoop.calls, "startDirectCall");
+      sandbox.stub(navigator.mozLoop.calls, "clearCallInProgress");
+    });
+
+    describe("#RenderNoContacts", function() {
+      beforeEach(function() {
+        sandbox.stub(navigator.mozLoop.contacts, "getAll", function(cb) {
+          cb(null, []);
+        });
+        listView = TestUtils.renderIntoDocument(
+          React.createElement(loop.contacts.ContactsList, {
+            mozLoop: navigator.mozLoop,
+            notifications: notifications,
+            startForm: function() {}
+          }));
+        node = listView.getDOMNode();
+      });
+
+      it("should not show a contacts title if no contacts", function() {
+        expect(node.querySelector(".contact-list-title")).to.eql(null);
+        sinon.assert.neverCalledWith(mozL10nGetSpy, "contact_list_title");
+      });
+
+      it("should show the no contacts view", function() {
+        expect(node.querySelector(".contact-list-empty")).to.not.eql(null);
+      });
 
-      listView = TestUtils.renderIntoDocument(
-        React.createElement(loop.contacts.ContactsList, {
-          notifications: notifications,
-          startForm: function() {}
-        }));
+      it("should display the no contacts strings", function() {
+        sinon.assert.calledWithExactly(mozL10nGetSpy,
+                                       "no_contacts_message_heading");
+        sinon.assert.calledWithExactly(mozL10nGetSpy,
+                                       "no_contacts_import_or_add");
+      });
+    });
+
+    describe("#RenderWithContacts", function() {
+      beforeEach(function() {
+        sandbox.stub(navigator.mozLoop.contacts, "getAll", function(cb) {
+          cb(null, [].concat(fakeContacts));
+        });
+        listView = TestUtils.renderIntoDocument(
+          React.createElement(loop.contacts.ContactsList, {
+            mozLoop: navigator.mozLoop,
+            notifications: notifications,
+            startForm: function() {}
+          }));
+        node = listView.getDOMNode();
+      });
+
+      it("should show a contacts title", function() {
+        expect(node.querySelector(".contact-list-title")).not.to.eql(null);
+        sinon.assert.calledWithExactly(mozL10nGetSpy, "contact_list_title");
+      });
     });
 
     describe("#handleContactAction", function() {
+      beforeEach(function() {
+        sandbox.stub(navigator.mozLoop.contacts, "getAll", function(cb) {
+          cb(null, []);
+        });
+        listView = TestUtils.renderIntoDocument(
+          React.createElement(loop.contacts.ContactsList, {
+            mozLoop: navigator.mozLoop,
+            notifications: notifications,
+            startForm: function() {}
+          }));
+        node = listView.getDOMNode();
+      });
+
       it("should call window.close when called with 'video-call' action",
         function() {
           listView.handleContactAction({}, "video-call");
 
           sinon.assert.calledOnce(fakeWindow.close);
       });
 
       it("should call window.close when called with 'audio-call' action",
         function() {
           listView.handleContactAction({}, "audio-call");
 
           sinon.assert.calledOnce(fakeWindow.close);
         });
     });
 
     describe("#handleImportButtonClick", function() {
+      beforeEach(function() {
+        sandbox.stub(navigator.mozLoop.contacts, "getAll", function(cb) {
+          cb(null, []);
+        });
+        listView = TestUtils.renderIntoDocument(
+          React.createElement(loop.contacts.ContactsList, {
+            mozLoop: navigator.mozLoop,
+            notifications: notifications,
+            startForm: function() {}
+          }));
+        node = listView.getDOMNode();
+      });
+
       it("should notify the end user from a succesful import", function() {
         sandbox.stub(notifications, "successL10n");
         navigator.mozLoop.startImport = function(opts, cb) {
           cb(null, {success: 42});
         };
 
         listView.handleImportButtonClick();
 
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -8,16 +8,17 @@ describe("loop.panel", function() {
   var expect = chai.expect;
   var TestUtils = React.addons.TestUtils;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
 
   var sandbox, notifications;
   var fakeXHR, fakeWindow, fakeMozLoop;
   var requests = [];
+  var mozL10nGetSpy;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     fakeXHR = sandbox.useFakeXMLHttpRequest();
     requests = [];
     // https://github.com/cjohansen/Sinon.JS/issues/393
     fakeXHR.xhr.onCreate = function (xhr) {
       requests.push(xhr);
@@ -72,29 +73,29 @@ describe("loop.panel", function() {
       logOutFromFxA: sandbox.stub(),
       notifyUITour: sandbox.stub(),
       openURL: sandbox.stub(),
       getSelectedTabMetadata: sandbox.stub(),
       userProfile: null
     };
 
     document.mozL10n.initialize(navigator.mozLoop);
+    sandbox.stub(document.mozL10n, "get").returns("Fake title");
   });
 
   afterEach(function() {
     delete navigator.mozLoop;
     loop.shared.mixins.setRootObject(window);
     sandbox.restore();
   });
 
   describe("#init", function() {
     beforeEach(function() {
       sandbox.stub(React, "render");
       sandbox.stub(document.mozL10n, "initialize");
-      sandbox.stub(document.mozL10n, "get").returns("Fake title");
     });
 
     it("should initalize L10n", function() {
       loop.panel.init();
 
       sinon.assert.calledOnce(document.mozL10n.initialize);
       sinon.assert.calledWithExactly(document.mozL10n.initialize,
         navigator.mozLoop);
@@ -344,79 +345,79 @@ describe("loop.panel", function() {
         beforeEach(function() {
           fakeMozLoop.userProfile = null;
         });
 
         it("should show a signin entry when user is not authenticated",
            function() {
              var view = mountTestComponent();
 
-             expect(view.getDOMNode().querySelectorAll(".icon-signout"))
+             expect(view.getDOMNode().querySelectorAll(".entry-settings-signout"))
                .to.have.length.of(0);
-             expect(view.getDOMNode().querySelectorAll(".icon-signin"))
+             expect(view.getDOMNode().querySelectorAll(".entry-settings-signin"))
                .to.have.length.of(1);
            });
 
         it("should hide any account entry when user is not authenticated",
            function() {
              var view = mountTestComponent();
 
              expect(view.getDOMNode().querySelectorAll(".icon-account"))
                .to.have.length.of(0);
            });
 
         it("should sign in the user on click when unauthenticated", function() {
           navigator.mozLoop.loggedInToFxA = false;
           var view = mountTestComponent();
 
           TestUtils.Simulate.click(view.getDOMNode()
-                                     .querySelector(".icon-signin"));
+                                     .querySelector(".entry-settings-signin"));
 
           sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
         });
       });
 
       it("should show a signout entry when user is authenticated", function() {
         navigator.mozLoop.userProfile = {email: "test@example.com"};
 
         var view = mountTestComponent();
 
-        expect(view.getDOMNode().querySelectorAll(".icon-signout"))
-          .to.have.length.of(1);
-        expect(view.getDOMNode().querySelectorAll(".icon-signin"))
-          .to.have.length.of(0);
+        sinon.assert.calledWithExactly(document.mozL10n.get,
+                                       "settings_menu_item_signout");
+        sinon.assert.neverCalledWith(document.mozL10n.get,
+                                     "settings_menu_item_signin");
       });
 
       it("should show an account entry when user is authenticated", function() {
         navigator.mozLoop.userProfile = {email: "test@example.com"};
 
         var view = mountTestComponent();
 
-        expect(view.getDOMNode().querySelectorAll(".icon-account"))
-          .to.have.length.of(1);
+        sinon.assert.calledWithExactly(document.mozL10n.get,
+                                       "settings_menu_item_settings");
       });
 
       it("should open the FxA settings when the account entry is clicked",
          function() {
            navigator.mozLoop.userProfile = {email: "test@example.com"};
 
            var view = mountTestComponent();
 
            TestUtils.Simulate.click(view.getDOMNode()
-                                      .querySelector(".icon-account"));
+                                      .querySelector(".entry-settings-account"));
 
            sinon.assert.calledOnce(navigator.mozLoop.openFxASettings);
          });
 
       it("should sign out the user on click when authenticated", function() {
         navigator.mozLoop.userProfile = {email: "test@example.com"};
         var view = mountTestComponent();
 
         TestUtils.Simulate.click(view.getDOMNode()
-                                   .querySelector(".icon-signout"));
+                                   .querySelector(".entry-settings-signout"));
 
         sinon.assert.calledOnce(navigator.mozLoop.logOutFromFxA);
       });
     });
 
     describe("Help", function() {
       var view, supportUrl;
 
@@ -437,27 +438,27 @@ describe("loop.panel", function() {
           return "unseen";
         };
       });
 
       it("should open a tab to the support page", function() {
         view = mountTestComponent();
 
         TestUtils.Simulate
-          .click(view.getDOMNode().querySelector(".icon-help"));
+          .click(view.getDOMNode().querySelector(".entry-settings-help"));
 
         sinon.assert.calledOnce(fakeMozLoop.openURL);
         sinon.assert.calledWithExactly(fakeMozLoop.openURL, supportUrl);
       });
 
       it("should close the panel", function() {
         view = mountTestComponent();
 
         TestUtils.Simulate
-          .click(view.getDOMNode().querySelector(".icon-help"));
+          .click(view.getDOMNode().querySelector(".entry-settings-help"));
 
         sinon.assert.calledOnce(fakeWindow.close);
       });
     });
 
     describe("#render", function() {
       it("should not render a ToSView when gettingStarted.seen is true", function() {
         navigator.mozLoop.getLoopPref = function() {
@@ -832,17 +833,17 @@ describe("loop.panel", function() {
        "conversation button",
       function() {
         navigator.mozLoop.userProfile = {email: fakeEmail};
         var view = createTestComponent(false);
 
         TestUtils.Simulate.click(view.getDOMNode().querySelector(".new-room-button"));
 
         sinon.assert.calledWith(dispatch, new sharedActions.CreateRoom({
-          nameTemplate: "fakeText",
+          nameTemplate: "Fake title",
           roomOwner: fakeEmail
         }));
       });
 
     it("should dispatch a CreateRoom action with context when clicking on the " +
        "Start a conversation button", function() {
       fakeMozLoop.userProfile = {email: fakeEmail};
       var favicon = "data:image/x-icon;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
@@ -863,17 +864,17 @@ describe("loop.panel", function() {
       var node = view.getDOMNode();
 
       // Select the checkbox
       TestUtils.Simulate.click(node.querySelector(".checkbox-wrapper"));
 
       TestUtils.Simulate.click(node.querySelector(".new-room-button"));
 
       sinon.assert.calledWith(dispatch, new sharedActions.CreateRoom({
-        nameTemplate: "fakeText",
+        nameTemplate: "Fake title",
         roomOwner: fakeEmail,
         urls: [{
           location: "http://invalid.com",
           description: "fakeSite",
           thumbnail: favicon
         }]
       }));
     });
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -135,18 +135,18 @@ var fakeContacts = [{
           return false;
       }
     },
     hasEncryptionKey: true,
     setLoopPref: function(){},
     releaseCallData: function() {},
     copyString: function() {},
     getUserAvatar: function(emailAddress) {
-      return "http://www.gravatar.com/avatar/" + (Math.ceil(Math.random() * 3) === 2 ?
-        "0a996f0fe2727ef1668bdb11897e4459" : "foo") + ".jpg?default=blank&s=40";
+      var avatarUrl = "http://www.gravatar.com/avatar/0a996f0fe2727ef1668bdb11897e4459.jpg?default=blank&s=40";
+      return Math.ceil(Math.random() * 3) === 2 ? avatarUrl : null;
     },
     getSelectedTabMetadata: function(callback) {
       callback({
         previews: ["chrome://branding/content/about-logo.png"],
         description: "sample webpage description",
         url: "https://www.example.com"
       });
     },
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -53,16 +53,18 @@ body {
 }
 
 .showcase > section > h1 {
   margin: 1em 0;
   border-bottom: 1px solid #aaa;
 }
 
 .showcase > section .comp {
+  /* contain absolute positioned elements such as dropdowns */
+  position: relative;
   margin: 0 auto; /* width is usually set programmatically */
 }
 
 .showcase > section .comp.dashed,
 .showcase > section .comp > iframe.dashed
 {
   border: 1px dashed #ccc;
 }
@@ -162,12 +164,12 @@ body {
 }
 
 /* Temporary until bug 1168829 is completed */
 .standalone.text-chat-example .text-chat-view {
   height: 400px;
 }
 
 /* Force dropdown menus to display. */
-.force-menu-show * {
+.force-menu-show .icons,
+.force-menu-show .dropdown-menu {
   display: inline-block !important;
 }
-
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -1,28 +1,30 @@
 /* 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/. */
 
-/* global Frame:false uncaughtError:true */
+/* global Frame:false uncaughtError:true fakeContacts:true */
 
 (function() {
   "use strict";
 
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener("DOMContentLoaded", loop.panel.init);
   document.removeEventListener("DOMContentLoaded", loop.conversation.init);
 
   var sharedActions = loop.shared.actions;
 
   // 1. Desktop components
   // 1.1 Panel
   var AvailabilityDropdown = loop.panel.AvailabilityDropdown;
   var PanelView = loop.panel.PanelView;
   var SignInRequestView = loop.panel.SignInRequestView;
+  var ContactDropdown = loop.contacts.ContactDropdown;
+  var ContactDetail = loop.contacts.ContactDetail;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var OngoingConversationView = loop.conversationViews.OngoingConversationView;
   var CallFailedView = loop.conversationViews.CallFailedView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
@@ -446,16 +448,27 @@
   };
 
   var mockMozLoopLoggedInLongEmail = _.cloneDeep(navigator.mozLoop);
   mockMozLoopLoggedInLongEmail.userProfile = {
     email: "reallyreallylongtext@example.com",
     uid: "0354b278a381d3cb408bb46ffc01266"
   };
 
+  var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
+
+  var mozLoopNoContacts = _.cloneDeep(navigator.mozLoop);
+  mozLoopNoContacts.userProfile = {
+    email: "reallyreallylongtext@example.com",
+    uid: "0354b278a381d3cb408bb46ffc01266"
+  };
+  mozLoopNoContacts.contacts.getAll = function(callback) {
+    callback(null, []);
+  };
+
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
@@ -734,16 +747,24 @@
             React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Contact list tab long email"}, 
               React.createElement(PanelView, {client: mockClient, 
                          dispatcher: dispatcher, 
                          mozLoop: mockMozLoopLoggedInLongEmail, 
                          notifications: notifications, 
                          roomStore: roomStore, 
                          selectedTab: "contacts"})
             ), 
+            React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Contact list tab (no contacts)"}, 
+              React.createElement(PanelView, {client: mockClient, 
+                         dispatcher: dispatcher, 
+                         mozLoop: mozLoopNoContacts, 
+                         notifications: notifications, 
+                         roomStore: roomStore, 
+                         selectedTab: "contacts"})
+            ), 
             React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Error Notification"}, 
               React.createElement(PanelView, {client: mockClient, 
                          dispatcher: dispatcher, 
                          mozLoop: navigator.mozLoop, 
                          notifications: errNotifications, 
                          roomStore: roomStore})
             ), 
             React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Error Notification - authenticated"}, 
@@ -779,16 +800,40 @@
             ), 
             React.createElement(Example, {cssClass: "force-menu-show", dashed: true, 
                      style: {width: "332px", height: "200px"}, 
                      summary: "AvailabilityDropdown Expanded"}, 
               React.createElement(AvailabilityDropdown, null)
             )
           ), 
 
+          React.createElement(Section, {name: "ContactDetail"}, 
+            React.createElement(Example, {cssClass: "force-menu-show", dashed: true, 
+                     style: {width: "300px", height: "272px"}, 
+                     summary: "ContactDetail"}, 
+              React.createElement(ContactDetail, {contact: fakeContacts[0], 
+                handleContactAction: function() {}})
+            )
+          ), 
+
+          React.createElement(Section, {name: "ContactDropdown"}, 
+            React.createElement(Example, {dashed: true, style: {width: "300px", height: "272px"}, 
+                     summary: "ContactDropdown not blocked can edit"}, 
+              React.createElement(ContactDropdown, {blocked: false, 
+                               canEdit: true, 
+                               handleAction: function () {}})
+            ), 
+            React.createElement(Example, {dashed: true, style: {width: "300px", height: "272px"}, 
+                     summary: "ContactDropdown blocked can't edit"}, 
+              React.createElement(ContactDropdown, {blocked: true, 
+                               canEdit: false, 
+                               handleAction: function () {}})
+            )
+          ), 
+
           React.createElement(Section, {name: "AcceptCallView"}, 
             React.createElement(Example, {dashed: true, style: {width: "300px", height: "272px"}, 
                      summary: "Default / incoming video call"}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_VIDEO, 
                                 callerId: "Mr Smith", 
                                 dispatcher: dispatcher, 
                                 mozLoop: mockMozLoopLoggedIn})
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -1,28 +1,30 @@
 /* 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/. */
 
-/* global Frame:false uncaughtError:true */
+/* global Frame:false uncaughtError:true fakeContacts:true */
 
 (function() {
   "use strict";
 
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener("DOMContentLoaded", loop.panel.init);
   document.removeEventListener("DOMContentLoaded", loop.conversation.init);
 
   var sharedActions = loop.shared.actions;
 
   // 1. Desktop components
   // 1.1 Panel
   var AvailabilityDropdown = loop.panel.AvailabilityDropdown;
   var PanelView = loop.panel.PanelView;
   var SignInRequestView = loop.panel.SignInRequestView;
+  var ContactDropdown = loop.contacts.ContactDropdown;
+  var ContactDetail = loop.contacts.ContactDetail;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var OngoingConversationView = loop.conversationViews.OngoingConversationView;
   var CallFailedView = loop.conversationViews.CallFailedView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
@@ -446,16 +448,27 @@
   };
 
   var mockMozLoopLoggedInLongEmail = _.cloneDeep(navigator.mozLoop);
   mockMozLoopLoggedInLongEmail.userProfile = {
     email: "reallyreallylongtext@example.com",
     uid: "0354b278a381d3cb408bb46ffc01266"
   };
 
+  var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
+
+  var mozLoopNoContacts = _.cloneDeep(navigator.mozLoop);
+  mozLoopNoContacts.userProfile = {
+    email: "reallyreallylongtext@example.com",
+    uid: "0354b278a381d3cb408bb46ffc01266"
+  };
+  mozLoopNoContacts.contacts.getAll = function(callback) {
+    callback(null, []);
+  };
+
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
@@ -734,16 +747,24 @@
             <Example dashed={true} style={{width: "332px"}} summary="Contact list tab long email">
               <PanelView client={mockClient}
                          dispatcher={dispatcher}
                          mozLoop={mockMozLoopLoggedInLongEmail}
                          notifications={notifications}
                          roomStore={roomStore}
                          selectedTab="contacts" />
             </Example>
+            <Example dashed={true} style={{width: "332px"}} summary="Contact list tab (no contacts)">
+              <PanelView client={mockClient}
+                         dispatcher={dispatcher}
+                         mozLoop={mozLoopNoContacts}
+                         notifications={notifications}
+                         roomStore={roomStore}
+                         selectedTab="contacts" />
+            </Example>
             <Example dashed={true} style={{width: "332px"}} summary="Error Notification">
               <PanelView client={mockClient}
                          dispatcher={dispatcher}
                          mozLoop={navigator.mozLoop}
                          notifications={errNotifications}
                          roomStore={roomStore} />
             </Example>
             <Example dashed={true} style={{width: "332px"}} summary="Error Notification - authenticated">
@@ -779,16 +800,40 @@
             </Example>
             <Example cssClass="force-menu-show" dashed={true}
                      style={{width: "332px", height: "200px"}}
                      summary="AvailabilityDropdown Expanded">
               <AvailabilityDropdown />
             </Example>
           </Section>
 
+          <Section name="ContactDetail">
+            <Example cssClass="force-menu-show" dashed={true}
+                     style={{width: "300px", height: "272px"}}
+                     summary="ContactDetail">
+              <ContactDetail contact={fakeContacts[0]}
+                handleContactAction={function() {}} />
+            </Example>
+          </Section>
+
+          <Section name="ContactDropdown">
+            <Example dashed={true} style={{width: "300px", height: "272px"}}
+                     summary="ContactDropdown not blocked can edit">
+              <ContactDropdown blocked={false}
+                               canEdit={true}
+                               handleAction={function () {}} />
+            </Example>
+            <Example dashed={true} style={{width: "300px", height: "272px"}}
+                     summary="ContactDropdown blocked can't edit">
+              <ContactDropdown blocked={true}
+                               canEdit={false}
+                               handleAction={function () {}} />
+            </Example>
+          </Section>
+
           <Section name="AcceptCallView">
             <Example dashed={true} style={{width: "300px", height: "272px"}}
                      summary="Default / incoming video call">
               <div className="fx-embedded">
                 <AcceptCallView callType={CALL_TYPES.AUDIO_VIDEO}
                                 callerId="Mr Smith"
                                 dispatcher={dispatcher}
                                 mozLoop={mockMozLoopLoggedIn} />
--- a/browser/devtools/performance/modules/logic/marker-utils.js
+++ b/browser/devtools/performance/modules/logic/marker-utils.js
@@ -355,16 +355,30 @@ const Formatters = {
     if ("causeName" in marker && !JS_MARKER_MAP[marker.causeName]) {
       let cause = PREFS["show-platform-data"] ? marker.causeName : GECKO_SYMBOL;
       return {
         [L10N.getStr("marker.field.causeName")]: cause
       };
     }
   },
 
+  GCFields: function (marker) {
+    let fields = Object.create(null);
+    let cause = marker.cause;
+    let label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
+
+    fields[L10N.getStr("marker.field.causeName")] = label;
+
+    if ("nonincrementalReason" in marker) {
+      fields[L10N.getStr("marker.field.nonIncrementalCause")] = marker.nonincrementalReason;
+    }
+
+    return fields;
+  },
+
   DOMEventFields: function (marker) {
     let fields = Object.create(null);
     if ("type" in marker) {
       fields[L10N.getStr("marker.field.DOMEventType")] = marker.type;
     }
     if ("eventPhase" in marker) {
       let phase;
       if (marker.eventPhase === Ci.nsIDOMEvent.AT_TARGET) {
--- a/browser/devtools/performance/modules/markers.js
+++ b/browser/devtools/performance/modules/markers.js
@@ -96,20 +96,17 @@ const TIMELINE_BLUEPRINT = {
     group: 1,
     colorName: "graphs-yellow",
     label: L10N.getStr("marker.label.parseXML"),
   },
   "GarbageCollection": {
     group: 1,
     colorName: "graphs-red",
     label: Formatters.GCLabel,
-    fields: [
-      { property: "causeName", label: L10N.getStr("marker.field.causeName") },
-      { property: "nonincrementalReason", label: L10N.getStr("marker.field.nonIncrementalCause") }
-    ],
+    fields: Formatters.GCFields,
   },
   "nsCycleCollector::Collect": {
     group: 1,
     colorName: "graphs-red",
     label: L10N.getStr("marker.label.cycleCollection"),
     fields: Formatters.CycleCollectionFields,
   },
   "nsCycleCollector::ForgetSkippable": {
--- a/browser/devtools/performance/test/unit/test_marker-utils.js
+++ b/browser/devtools/performance/test/unit/test_marker-utils.js
@@ -34,16 +34,22 @@ add_task(function () {
 
   fields = Utils.getMarkerFields({ name: "DOMEvent", eventPhase: Ci.nsIDOMEvent.AT_TARGET, type: "mouseclick" });
   equal(fields.length, 2, "getMarkerFields() returns multiple fields when using a fields function");
   equal(fields[0].label, "Event Type:", "getMarkerFields() correctly returns fields via function (1)");
   equal(fields[0].value, "mouseclick", "getMarkerFields() correctly returns fields via function (2)");
   equal(fields[1].label, "Phase:", "getMarkerFields() correctly returns fields via function (3)");
   equal(fields[1].value, "Target", "getMarkerFields() correctly returns fields via function (4)");
 
+  fields = Utils.getMarkerFields({ name: "GarbageCollection", cause: "ALLOC_TRIGGER" });
+  equal(fields[0].value, "Too Many Allocations", "Uses L10N for GC reasons");
+
+  fields = Utils.getMarkerFields({ name: "GarbageCollection", cause: "NOT_A_GC_REASON" });
+  equal(fields[0].value, "NOT_A_GC_REASON", "Defaults to enum for GC reasons when not L10N'd");
+
   equal(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "(Gecko)",
     "Correctly obfuscates JS markers when platform data is off.");
   Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true);
   equal(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "Some Platform Field",
     "Correctly deobfuscates JS markers when platform data is on.");
 
   equal(Utils.getMarkerClassName("Javascript"), "Function Call",
     "getMarkerClassName() returns correct string when defined via function");
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -390,16 +390,21 @@ These should match what Safari and other
 <!ENTITY customizeMenu.addMoreItems.label "Add More Items…">
 <!ENTITY customizeMenu.addMoreItems.accesskey "A">
 
 <!ENTITY openCmd.commandkey           "l">
 <!ENTITY urlbar.placeholder2          "Search or enter address">
 <!ENTITY urlbar.accesskey             "d">
 <!ENTITY urlbar.switchToTab.label     "Switch to tab:">
 
+<!ENTITY urlbar.searchSuggestionsNotification.question "Would you like to improve your search experience with suggestions?">
+<!ENTITY urlbar.searchSuggestionsNotification.learnMore "Learn more…">
+<!ENTITY urlbar.searchSuggestionsNotification.disable "No">
+<!ENTITY urlbar.searchSuggestionsNotification.enable "Yes">
+
 <!-- 
   Comment duplicated from browser-sets.inc:
 
   Search Command Key Logic works like this:
 
   Unix: Ctrl+J (0.8, 0.9 support)
         Ctrl+K (cross platform binding)
   Mac:  Cmd+K (cross platform binding)
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -123,33 +123,45 @@ import_contacts_failure_message=Some con
 ## when user's contacts have been successfully imported.
 ## Semicolon-separated list of plural forms. See:
 ## http://developer.mozilla.org/en/docs/Localization_and_Plurals
 ## In this item, don't translate the part between {{..}}
 import_contacts_success_message={{total}} contact was successfully imported.;{{total}} contacts were successfully imported.
 ## LOCALIZATION NOTE(sync_contacts_button): This button is displayed in place of
 ## importing_contacts_button once contacts have been imported once.
 sync_contacts_button=Sync Contacts
+## LOCALIZATION NOTE(no_contacts_message_heading): Title shown when user has no
+## contacts in his address book
+no_contacts_message_heading=No contacts yet
+## LOCALIZATION NOTE(no_contacts_import_or_add): Subheading inviting the user
+## to add people to his contact list
+no_contacts_import_or_add=Import or add someone
 
 ## LOCALIZATION NOTE(import_failed_description simple): Displayed when an import of
 ## contacts fails. This is displayed in the error field.
 import_failed_description_simple=Sorry, contact import failed
 import_failed_description_some=Some contacts could not be imported
 import_failed_support_button=Help
 
 ## LOCALIZATION NOTE(remove_contact_menu_button2): Displayed in the contact list in
 ## a pop-up menu next to the contact's name.
 remove_contact_menu_button2=Remove Contact…
+## LOCALIZATION NOTE(remove_contact_title): Displayed in the contact list in
+## a pop-up menu next to the contact's name.
+remove_contact_menu_button3=Remove Contact
 ## LOCALIZATION NOTE(confirm_delete_contact_alert): This is an alert that is displayed
 ## to confirm deletion of a contact.
 confirm_delete_contact_alert=Are you sure you want to delete this contact?
 ## LOCALIZATION NOTE(confirm_delete_contact_remove_button, confirm_delete_contact_cancel_button):
 ## These are displayed on the alert with confirm_delete_contact_alert
 confirm_delete_contact_remove_button=Remove Contact
 confirm_delete_contact_cancel_button=Cancel
+## LOCALIZATION NOTE(contact_list_title): This is in uppercase in English for
+## emphasis, please do what is appropriate for specific locales.
+contact_list_title=MY CONTACTS
 
 ## LOCALIZATION NOTE(block_contact_menu_button): Displayed in the contact list in
 ## a pop-up menu next to the contact's name, used to block a contact from calling
 ## the user.
 block_contact_menu_button=Block Contact
 ## LOCALIZATION NOTE(unblock_contact_menu_button): Displayed in the contact list in
 ## a pop-up menu next to the contact's name, used to unblock a contact and allow them
 ## to call the user.
--- a/browser/locales/en-US/chrome/browser/preferences/privacy.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/privacy.dtd
@@ -23,16 +23,19 @@
 <!ENTITY  locbar.history.accesskey      "H">
 <!ENTITY  locbar.bookmarks.label        "Bookmarks">
 <!ENTITY  locbar.bookmarks.accesskey    "k">
 <!ENTITY  locbar.openpage.label         "Open tabs">
 <!ENTITY  locbar.openpage.accesskey     "O">
 <!ENTITY  locbar.searches.label         "Related searches from the default search engine">
 <!ENTITY  locbar.searches.accesskey     "d">
 
+<!ENTITY  suggestionSettings.label      "Change preferences for search engine suggestions…">
+<!ENTITY  suggestionSettings.accesskey  "g">
+
 <!ENTITY  acceptCookies.label           "Accept cookies from sites">
 <!ENTITY  acceptCookies.accesskey       "A">
 
 <!ENTITY  acceptThirdParty.pre.label      "Accept third-party cookies:">
 <!ENTITY  acceptThirdParty.pre.accesskey  "c">
 <!ENTITY  acceptThirdParty.always.label   "Always">
 <!ENTITY  acceptThirdParty.never.label    "Never">
 <!ENTITY  acceptThirdParty.visited.label  "From visited">
--- a/browser/locales/en-US/chrome/browser/preferences/search.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/search.dtd
@@ -4,16 +4,19 @@
 
 <!ENTITY defaultSearchEngine.label             "Default Search Engine">
 
 <!ENTITY chooseYourDefaultSearchEngine.label   "Choose your default search engine. &brandShortName; uses it in the location bar, search bar, and start page.">
 
 <!ENTITY provideSearchSuggestions.label        "Provide search suggestions">
 <!ENTITY provideSearchSuggestions.accesskey    "s">
 
+<!ENTITY showURLBarSuggestions.label           "Show search suggestions in location bar results">
+<!ENTITY showURLBarSuggestions.accesskey       "l">
+
 <!ENTITY redirectWindowsSearch.label "Use this search engine for searches from Windows">
 <!ENTITY redirectWindowsSearch.accesskey "W">
 
 <!ENTITY oneClickSearchEngines.label           "One-click search engines">
 
 <!ENTITY chooseWhichOneToDisplay.label         "The search bar lets you search alternate engines directly. Choose which ones to display.">
 
 <!ENTITY engineNameColumn.label                "Search Engine">
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -957,19 +957,17 @@ toolbarbutton[constrain-size="true"][cui
 
 .urlbar-display {
   margin-top: 0;
   margin-bottom: 0;
   -moz-margin-start: 0;
   color: GrayText;
 }
 
-#PopupAutoCompleteRichResult > richlistbox {
-  transition: height 100ms;
-}
+%include ../shared/urlbarSearchSuggestionsNotification.inc.css
 
 #search-container {
   min-width: calc(54px + 11ch);
 }
 
 /* identity box */
 
 #identity-box:-moz-locale-dir(ltr) {
@@ -1131,17 +1129,19 @@ richlistitem[type~="action"][actiontype=
 
 .ac-result-type-tag,
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
   width: 16px;
   height: 16px;
 }
 
-.ac-comment {
+.ac-comment,
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description,
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
   font-size: 1.05em;
 }
 
 .ac-extra > .ac-comment {
   font-size: inherit;
 }
 
 .ac-url-text,
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -91,16 +91,17 @@ browser.jar:
   skin/classic/browser/search-pref.png                      (../shared/search/search-pref.png)
   skin/classic/browser/search-indicator.png                 (../shared/search/search-indicator.png)
   skin/classic/browser/search-engine-placeholder.png        (../shared/search/search-engine-placeholder.png)
   skin/classic/browser/badge-add-engine.png                 (../shared/search/badge-add-engine.png)
   skin/classic/browser/search-indicator-badge-add.png       (../shared/search/search-indicator-badge-add.png)
   skin/classic/browser/search-history-icon.svg              (../shared/search/history-icon.svg)
   skin/classic/browser/search-indicator-magnifying-glass.svg   (../shared/search/search-indicator-magnifying-glass.svg)
   skin/classic/browser/search-arrow-go.svg                  (../shared/search/search-arrow-go.svg)
+  skin/classic/browser/info.svg                             (../shared/info.svg)
   skin/classic/browser/Security-broken.png
   skin/classic/browser/setDesktopBackground.css
   skin/classic/browser/slowStartup-16.png
   skin/classic/browser/theme-switcher-icon.png              (../shared/theme-switcher-icon.png)
   skin/classic/browser/Toolbar.png
   skin/classic/browser/Toolbar-inverted.png
   skin/classic/browser/Toolbar-small.png
   skin/classic/browser/undoCloseTab.png                        (../shared/undoCloseTab.png)
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1787,24 +1787,22 @@ toolbarbutton[constrain-size="true"][cui
 }
 
 .urlbar-display {
   margin-top: 0;
   margin-bottom: 0;
   color: GrayText;
 }
 
-#PopupAutoCompleteRichResult > richlistbox {
-  transition: height 100ms;
-}
-
 #PopupAutoCompleteRichResult {
   margin-top: 2px;
 }
 
+%include ../shared/urlbarSearchSuggestionsNotification.inc.css
+
 /* ----- AUTOCOMPLETE ----- */
 
 #treecolAutoCompleteImage {
   max-width: 36px;
 }
 
 .ac-result-type-bookmark,
 .autocomplete-treebody::-moz-tree-image(bookmark, treecolAutoCompleteImage) {
@@ -1840,18 +1838,18 @@ richlistitem[selected="true"][current="t
 }
 
 .ac-extra > .ac-comment {
   font-size: inherit;
 }
 
 .ac-url-text,
 .ac-action-text {
+  font: message-box;
   color: -moz-nativehyperlinktext;
-  font: message-box;
 }
 
 richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
   list-style-image: url("chrome://browser/skin/actionicon-tab.png");
   -moz-image-region: rect(0, 16px, 11px, 0);
   padding: 0 3px;
 }
 
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -122,16 +122,17 @@ browser.jar:
   skin/classic/browser/search-engine-placeholder@2x.png        (../shared/search/search-engine-placeholder@2x.png)
   skin/classic/browser/badge-add-engine.png                    (../shared/search/badge-add-engine.png)
   skin/classic/browser/badge-add-engine@2x.png                 (../shared/search/badge-add-engine@2x.png)
   skin/classic/browser/search-indicator-badge-add.png          (../shared/search/search-indicator-badge-add.png)
   skin/classic/browser/search-indicator-badge-add@2x.png       (../shared/search/search-indicator-badge-add@2x.png)
   skin/classic/browser/search-history-icon.svg                 (../shared/search/history-icon.svg)
   skin/classic/browser/search-indicator-magnifying-glass.svg   (../shared/search/search-indicator-magnifying-glass.svg)
   skin/classic/browser/search-arrow-go.svg                     (../shared/search/search-arrow-go.svg)
+  skin/classic/browser/info.svg                                (../shared/info.svg)
   skin/classic/browser/slowStartup-16.png
   skin/classic/browser/theme-switcher-icon.png                 (../shared/theme-switcher-icon.png)
   skin/classic/browser/theme-switcher-icon@2x.png              (../shared/theme-switcher-icon@2x.png)
   skin/classic/browser/Toolbar.png
   skin/classic/browser/Toolbar@2x.png
   skin/classic/browser/Toolbar-inverted.png
   skin/classic/browser/Toolbar-inverted@2x.png
   skin/classic/browser/toolbarbutton-dropmarker.png
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/info.svg
@@ -0,0 +1,9 @@
+<?xml version="1.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/. -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <circle fill="#00a1f2" cx="8" cy="8" r="8" />
+  <circle fill="#fff" cx="8" cy="4" r="1.25" />
+  <rect x="7" y="7" width="2" height="6" rx="1" ry="1" fill="#fff" />
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
@@ -0,0 +1,55 @@
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] {
+  border-bottom: 1px solid hsla(210, 4%, 10%, 0.14);
+  background-color: hsla(210, 4%, 10%, 0.07);
+  padding: 6px 0;
+  -moz-padding-start: 44px;
+  -moz-padding-end: 6px;
+  background-image: url("chrome://browser/skin/info.svg");
+  background-clip: padding-box;
+  background-position: 20px center;
+  background-repeat: no-repeat;
+  background-size: 16px 16px;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"]:-moz-locale-dir(rtl) {
+  background-position: right 20px center;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description {
+  margin: 0;
+  padding: 0;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description > label.text-link {
+  -moz-margin-start: 0;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
+  -moz-appearance: none;
+  -moz-user-focus: ignore;
+  min-width: 80px;
+  border-radius: 3px;
+  padding: 4px 16px;
+  margin: 0;
+  -moz-margin-start: 10px;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button[anonid="search-suggestions-notification-disable"] {
+  color: hsl(210, 0%, 38%);
+  background-color: hsl(210, 0%, 88%);
+  border: 1px solid hsl(210, 0%, 82%);
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button[anonid="search-suggestions-notification-disable"]:hover {
+  background-color: hsl(210, 0%, 84%);
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button[anonid="search-suggestions-notification-enable"] {
+  color: white;
+  background-color: hsl(93, 82%, 44%);
+  border: 1px solid hsl(93, 82%, 44%);
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button[anonid="search-suggestions-notification-enable"]:hover {
+  background-color: hsl(93, 82%, 40%);
+}
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1424,19 +1424,17 @@ html|*.urlbar-input:-moz-lwtheme::-moz-p
 
 .urlbar-display {
   margin-top: 0;
   margin-bottom: 0;
   -moz-margin-start: 0;
   color: GrayText;
 }
 
-#PopupAutoCompleteRichResult > richlistbox {
-  transition: height 100ms;
-}
+%include ../shared/urlbarSearchSuggestionsNotification.inc.css
 
 #search-container {
   min-width: calc(54px + 11ch);
 }
 
 /* identity box */
 
 #identity-box:-moz-locale-dir(ltr) {
@@ -1559,27 +1557,30 @@ richlistitem[type~="action"][actiontype=
 
 .ac-result-type-tag,
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
   width: 16px;
   height: 16px;
 }
 
-.ac-comment {
+.ac-comment,
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description,
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
   font-size: 1.06em;
 }
 
-.ac-extra > .ac-comment {
+.ac-extra > .ac-comment,
+.ac-url-text,
+.ac-action-text {
   font-size: 1em;
 }
 
 .ac-url-text,
 .ac-action-text {
-  font-size: 1em;
   color: -moz-nativehyperlinktext;
 }
 
 @media (-moz-os-version: windows-xp) and (-moz-windows-default-theme) {
   .ac-url-text:not([selected="true"]),
   .ac-action-text:not([selected="true"]) {
     color: #008800;
   }
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -119,16 +119,17 @@ browser.jar:
         skin/classic/browser/search-engine-placeholder@2x.png        (../shared/search/search-engine-placeholder@2x.png)
         skin/classic/browser/badge-add-engine.png                    (../shared/search/badge-add-engine.png)
         skin/classic/browser/badge-add-engine@2x.png                 (../shared/search/badge-add-engine@2x.png)
         skin/classic/browser/search-indicator-badge-add.png          (../shared/search/search-indicator-badge-add.png)
         skin/classic/browser/search-indicator-badge-add@2x.png       (../shared/search/search-indicator-badge-add@2x.png)
         skin/classic/browser/search-history-icon.svg                 (../shared/search/history-icon.svg)
         skin/classic/browser/search-indicator-magnifying-glass.svg   (../shared/search/search-indicator-magnifying-glass.svg)
         skin/classic/browser/search-arrow-go.svg                     (../shared/search/search-arrow-go.svg)
+        skin/classic/browser/info.svg                                (../shared/info.svg)
         skin/classic/browser/setDesktopBackground.css
         skin/classic/browser/slowStartup-16.png
         skin/classic/browser/theme-switcher-icon.png                 (../shared/theme-switcher-icon.png)
         skin/classic/browser/Toolbar.png
         skin/classic/browser/Toolbar@2x.png
         skin/classic/browser/Toolbar-aero.png
         skin/classic/browser/Toolbar-aero@2x.png
         skin/classic/browser/Toolbar-inverted.png
--- a/dom/contacts/tests/mochitest.ini
+++ b/dom/contacts/tests/mochitest.ini
@@ -1,16 +1,16 @@
 [DEFAULT]
 skip-if = e10s
 support-files = shared.js
 
 [test_contacts_basics.html]
 skip-if = (toolkit == 'gonk' && debug) #debug-only failure
 [test_contacts_basics2.html]
-skip-if = (toolkit == 'gonk' && debug) #debug-only failure
+skip-if = (toolkit == 'gonk' && debug) || (os == 'win' && os_version == '5.1') #debug-only failure, bug 967258 on XP
 [test_contacts_blobs.html]
 skip-if = (toolkit == 'gonk' && debug) #debug-only failure
 [test_contacts_events.html]
 [test_contacts_getall.html]
 skip-if = (toolkit == 'gonk' && debug) || (toolkit == 'android' && processor == 'x86') #debug-only failure #x86 only
 [test_contacts_getall2.html]
 skip-if = (toolkit == 'gonk' && debug) #debug-only failure
 [test_contacts_international.html]
--- a/mobile/android/base/SiteIdentity.java
+++ b/mobile/android/base/SiteIdentity.java
@@ -8,23 +8,24 @@ package org.mozilla.gecko;
 import android.util.Log;
 import org.json.JSONObject;
 
 import android.text.TextUtils;
 
 public class SiteIdentity {
     private final String LOGTAG = "GeckoSiteIdentity";
     private SecurityMode mSecurityMode;
-    private MixedMode mMixedMode;
+    private boolean mSecure;
+    private MixedMode mMixedModeActive;
+    private MixedMode mMixedModeDisplay;
     private TrackingMode mTrackingMode;
     private String mHost;
     private String mOwner;
     private String mSupplemental;
     private String mVerifier;
-    private boolean mEncrypted;
     private String mOrigin;
 
     // The order of the items here relate to image levels in
     // site_security_level.xml
     public enum SecurityMode {
         UNKNOWN("unknown"),
         IDENTIFIED("identified"),
         VERIFIED("verified");
@@ -54,18 +55,18 @@ public class SiteIdentity {
             return mId;
         }
     }
 
     // The order of the items here relate to image levels in
     // site_security_level.xml
     public enum MixedMode {
         UNKNOWN("unknown"),
-        MIXED_CONTENT_BLOCKED("mixed_content_blocked"),
-        MIXED_CONTENT_LOADED("mixed_content_loaded");
+        MIXED_CONTENT_BLOCKED("blocked"),
+        MIXED_CONTENT_LOADED("loaded");
 
         private final String mId;
 
         private MixedMode(String id) {
             mId = id;
         }
 
         public static MixedMode fromString(String id) {
@@ -127,38 +128,45 @@ public class SiteIdentity {
 
     public void resetIdentity() {
         mSecurityMode = SecurityMode.UNKNOWN;
         mOrigin = null;
         mHost = null;
         mOwner = null;
         mSupplemental = null;
         mVerifier = null;
-        mEncrypted = false;
+        mSecure = false;
     }
 
     public void reset() {
         resetIdentity();
-        mMixedMode = MixedMode.UNKNOWN;
+        mMixedModeActive = MixedMode.UNKNOWN;
+        mMixedModeDisplay = MixedMode.UNKNOWN;
         mTrackingMode = TrackingMode.UNKNOWN;
     }
 
     void update(JSONObject identityData) {
         if (identityData == null) {
             reset();
             return;
         }
 
         try {
             JSONObject mode = identityData.getJSONObject("mode");
 
             try {
-                mMixedMode = MixedMode.fromString(mode.getString("mixed"));
+                mMixedModeDisplay = MixedMode.fromString(mode.getString("mixed_display"));
             } catch (Exception e) {
-                mMixedMode = MixedMode.UNKNOWN;
+                mMixedModeDisplay = MixedMode.UNKNOWN;
+            }
+
+            try {
+                mMixedModeActive = MixedMode.fromString(mode.getString("mixed_active"));
+            } catch (Exception e) {
+                mMixedModeActive = MixedMode.UNKNOWN;
             }
 
             try {
                 mTrackingMode = TrackingMode.fromString(mode.getString("tracking"));
             } catch (Exception e) {
                 mTrackingMode = TrackingMode.UNKNOWN;
             }
 
@@ -170,17 +178,17 @@ public class SiteIdentity {
             }
 
             try {
                 mOrigin = identityData.getString("origin");
                 mHost = identityData.getString("host");
                 mOwner = identityData.optString("owner", null);
                 mSupplemental = identityData.optString("supplemental", null);
                 mVerifier = identityData.getString("verifier");
-                mEncrypted = identityData.optBoolean("encrypted", false);
+                mSecure = identityData.optBoolean("secure", false);
             } catch (Exception e) {
                 resetIdentity();
             }
         } catch (Exception e) {
             reset();
         }
     }
 
@@ -203,20 +211,24 @@ public class SiteIdentity {
     public String getSupplemental() {
         return mSupplemental;
     }
 
     public String getVerifier() {
         return mVerifier;
     }
 
-    public boolean getEncrypted() {
-        return mEncrypted;
+    public boolean isSecure() {
+        return mSecure;
     }
 
-    public MixedMode getMixedMode() {
-        return mMixedMode;
+    public MixedMode getMixedModeActive() {
+        return mMixedModeActive;
+    }
+
+    public MixedMode getMixedModeDisplay() {
+        return mMixedModeDisplay;
     }
 
     public TrackingMode getTrackingMode() {
         return mTrackingMode;
     }
 }
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -11,16 +11,17 @@ import java.util.Collections;
 import java.util.EnumSet;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.SkewHandler;
 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AccountPickler;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.authenticator.FxADefaultLoginStateMachineDelegate;
@@ -38,16 +39,17 @@ import org.mozilla.gecko.sync.SyncConfig
 import org.mozilla.gecko.sync.ThreadPool;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+import org.mozilla.gecko.sync.telemetry.TelemetryContract;
 import org.mozilla.gecko.tokenserver.TokenServerClient;
 import org.mozilla.gecko.tokenserver.TokenServerClientDelegate;
 import org.mozilla.gecko.tokenserver.TokenServerException;
 import org.mozilla.gecko.tokenserver.TokenServerToken;
 
 import android.accounts.Account;
 import android.content.AbstractThreadedSyncAdapter;
 import android.content.ContentProviderClient;
@@ -83,22 +85,24 @@ public class FxAccountSyncAdapter extend
     this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID);
   }
 
   protected static class SyncDelegate extends FxAccountSyncDelegate {
     @Override
     public void handleSuccess() {
       Logger.info(LOG_TAG, "Sync succeeded.");
       super.handleSuccess();
+      TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_COMPLETED, 1);
     }
 
     @Override
     public void handleError(Exception e) {
       Logger.error(LOG_TAG, "Got exception syncing.", e);
       super.handleError(e);
+      TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED, 1);
     }
 
     @Override
     public void handleCannotSync(State finalState) {
       Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
       super.handleCannotSync(finalState);
     }
 
@@ -398,16 +402,17 @@ public class FxAccountSyncAdapter extend
     final EnumSet<FirefoxAccounts.SyncHint> syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras);
     FirefoxAccounts.logSyncHints(syncHints);
 
     // This applies even to forced syncs, but only on success.
     if (this.lastSyncRealtimeMillis > 0L &&
         (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime()) {
       Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) +
                            ": minimum interval not met.");
+      TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED_BACKOFF, 1);
       return;
     }
 
     // Pickle in a background thread to avoid strict mode warnings.
     ThreadPool.run(new Runnable() {
       @Override
       public void run() {
         try {
@@ -476,16 +481,18 @@ public class FxAccountSyncAdapter extend
       try {
         state = fxAccount.getState();
       } catch (Exception e) {
         fxAccount.releaseSharedAccountStateLock();
         syncDelegate.handleError(e);
         return;
       }
 
+      TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_STARTED, 1);
+
       final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
       stateMachine.advance(state, StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) {
         @Override
         public void handleNotMarried(State notMarried) {
           Logger.info(LOG_TAG, "handleNotMarried: in " + notMarried.getStateLabel());
           schedulePolicy.onHandleFinal(notMarried.getNeededAction());
           syncDelegate.handleCannotSync(notMarried);
         }
--- a/mobile/android/base/home/PanelRecyclerView.java
+++ b/mobile/android/base/home/PanelRecyclerView.java
@@ -3,31 +3,34 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
 import org.mozilla.gecko.home.PanelLayout.PanelView;
-import org.mozilla.gecko.home.RecyclerViewItemClickListener.OnClickListener;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemClickListener;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemLongClickListener;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 
 /**
  * RecyclerView implementation for grid home panels.
  */
 @SuppressLint("ViewConstructor") // View is only created from code
-public class PanelRecyclerView extends RecyclerView implements DatasetBacked, PanelView, OnClickListener {
+public class PanelRecyclerView extends RecyclerView
+        implements DatasetBacked, PanelView, OnItemClickListener, OnItemLongClickListener {
     private final PanelRecyclerViewAdapter adapter;
     private final GridLayoutManager layoutManager;
     private final PanelViewItemHandler itemHandler;
     private final float columnWidth;
     private final boolean autoFit;
     private final HomeConfig.ViewConfig viewConfig;
 
     private PanelLayout.OnItemOpenListener itemOpenListener;
@@ -64,17 +67,19 @@ public class PanelRecyclerView extends R
         int verticalSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_vertical_spacing);
         int outerSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_outer_spacing);
 
         addItemDecoration(new SpacingDecoration(horizontalSpacing, verticalSpacing));
 
         setPadding(outerSpacing, outerSpacing, outerSpacing, outerSpacing);
         setClipToPadding(false);
 
-        addOnItemTouchListener(new RecyclerViewItemClickListener(context, this, this));
+        RecyclerViewClickSupport.addTo(this)
+            .setOnItemClickListener(this)
+            .setOnItemLongClickListener(this);
     }
 
     @Override
     protected void onMeasure(int widthSpec, int heightSpec) {
         super.onMeasure(widthSpec, heightSpec);
 
         if (autoFit) {
             // Adjust span based on space available (What GridView does when you say numColumns="auto_fit")
@@ -118,36 +123,36 @@ public class PanelRecyclerView extends R
     }
 
     @Override
     public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory) {
         contextMenuInfoFactory = factory;
     }
 
     @Override
-    public void onClick(View view, int position) {
+    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
         if (viewConfig.hasHeaderConfig()) {
             if (position == 0) {
                 itemOpenListener.onItemOpen(viewConfig.getHeaderConfig().getUrl(), null);
                 return;
             }
 
             position--;
         }
 
         itemHandler.openItemAtPosition(adapter.getCursor(), position);
     }
 
     @Override
-    public void onLongClick(View view, int position) {
+    public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
         Cursor cursor = adapter.getCursor();
         cursor.moveToPosition(position);
 
-        contextMenuInfo = contextMenuInfoFactory.makeInfoForCursor(view, position, -1, cursor);
-        showContextMenuForChild(PanelRecyclerView.this);
+        contextMenuInfo = contextMenuInfoFactory.makeInfoForCursor(recyclerView, position, -1, cursor);
+        return showContextMenuForChild(PanelRecyclerView.this);
     }
 
     private class PanelSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
         @Override
         public int getSpanSize(int position) {
             if (position == 0 && viewConfig.hasHeaderConfig()) {
                 return layoutManager.getSpanCount();
             }
deleted file mode 100644
--- a/mobile/android/base/home/RecyclerViewItemClickListener.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/* -*- 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.support.v7.widget.RecyclerView;
-import android.view.GestureDetector;
-import android.view.HapticFeedbackConstants;
-import android.view.MotionEvent;
-import android.view.View;
-
-/**
- * RecyclerView.OnItemTouchListener implementation that will notify an OnClickListener about clicks and long clicks
- * on items displayed by the RecyclerView.
- */
-public class RecyclerViewItemClickListener implements RecyclerView.OnItemTouchListener,
-                                                      GestureDetector.OnGestureListener {
-    public interface OnClickListener {
-        void onClick(View view, int position);
-        void onLongClick(View view, int position);
-    }
-
-    private final OnClickListener clickListener;
-    private final GestureDetector gestureDetector;
-    private final RecyclerView recyclerView;
-
-    public RecyclerViewItemClickListener(Context context, RecyclerView recyclerView, OnClickListener clickListener) {
-        this.clickListener = clickListener;
-        this.gestureDetector = new GestureDetector(context, this);
-        this.recyclerView = recyclerView;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
-        return gestureDetector.onTouchEvent(event);
-    }
-
-    @Override
-    public boolean onSingleTapUp(MotionEvent event) {
-        View childView = recyclerView.findChildViewUnder(event.getX(), event.getY());
-
-        if (childView != null) {
-            final int position = recyclerView.getChildAdapterPosition(childView);
-            clickListener.onClick(childView, position);
-        }
-
-        return true;
-    }
-
-    @Override
-    public void onLongPress(MotionEvent event) {
-        View childView = recyclerView.findChildViewUnder(event.getX(), event.getY());
-
-        if (childView != null) {
-            final int position = recyclerView.getChildAdapterPosition(childView);
-            childView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
-            clickListener.onLongClick(childView, position);
-        }
-    }
-
-    @Override
-    public boolean onDown(MotionEvent e) {
-        return false;
-    }
-
-    @Override
-    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
-        return false;
-    }
-
-    @Override
-    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
-        return false;
-    }
-
-    @Override
-    public void onShowPress(MotionEvent e) {}
-
-    @Override
-    public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {}
-
-    @Override
-    public void onRequestDisallowInterceptTouchEvent(boolean b) {}
-}
--- a/mobile/android/base/home/SearchEngineBar.java
+++ b/mobile/android/base/home/SearchEngineBar.java
@@ -12,21 +12,22 @@ import android.support.v7.widget.LinearL
 import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.TypedValue;
 import android.view.View;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.mozglue.RobocopTarget;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport;
 
 import java.util.List;
 
 public class SearchEngineBar extends RecyclerView
-        implements RecyclerViewItemClickListener.OnClickListener {
+        implements RecyclerViewClickSupport.OnItemClickListener {
     private static final String LOGTAG = SearchEngineBar.class.getSimpleName();
 
     private static final float ICON_CONTAINER_MIN_WIDTH_DP = 72;
     private static final float LABEL_CONTAINER_WIDTH_DP = 48;
     private static final float DIVIDER_HEIGHT_DP = 1;
 
     public interface OnSearchBarClickListener {
         void onSearchBarClickListener(SearchEngine searchEngine);
@@ -61,17 +62,19 @@ public class SearchEngineBar extends Rec
 
         mAdapter = new SearchEngineAdapter(context);
         mAdapter.setIconContainerWidth(mIconContainerWidth);
         mLayoutManager = new LinearLayoutManager(context);
         mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
 
         setAdapter(mAdapter);
         setLayoutManager(mLayoutManager);
-        addOnItemTouchListener(new RecyclerViewItemClickListener(context, this, this));
+
+        RecyclerViewClickSupport.addTo(this)
+            .setOnItemClickListener(this);
     }
 
     public void setSearchEngines(List<SearchEngine> searchEngines) {
         mAdapter.setSearchEngines(searchEngines);
     }
 
     public void setOnSearchBarClickListener(OnSearchBarClickListener listener) {
         mOnSearchBarClickListener = listener;
@@ -109,36 +112,31 @@ public class SearchEngineBar extends Rec
     @Override
     public void onDraw(Canvas canvas) {
         super.onDraw(canvas);
 
         canvas.drawRect(0, 0, getWidth(), mDividerHeight, mDividerPaint);
     }
 
     @Override
-    public void onClick(View view, int position) {
+    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
         if (mOnSearchBarClickListener == null) {
             throw new IllegalStateException(
                     OnSearchBarClickListener.class.getSimpleName() + " is not initializer."
             );
         }
 
         if (position == 0) {
             return;
         }
 
         final SearchEngine searchEngine = mAdapter.getItem(position);
         mOnSearchBarClickListener.onSearchBarClickListener(searchEngine);
     }
 
-    @Override
-    public void onLongClick(View view, int position) {
-        // do nothing
-    }
-
     /**
      * We manually add the override for getAdapter because we see this method getting stripped
      * out during compile time by aggressive proguard rules.
      */
     @RobocopTarget
     @Override
     public SearchEngineAdapter getAdapter() {
         return mAdapter;
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -559,16 +559,18 @@ The layout of the identity dialog preven
 substitution variables.  If it is difficult to translate the sense of the string
 with that structure, consider a translation which ignores the preceding domain and
 just addresses the organization to follow, e.g. "This site is run by " -->
 <!ENTITY identity_connection_secure "Secure Connection">
 <!ENTITY identity_connection_insecure "Insecure connection">
 
 <!-- Mixed content notifications in site identity popup -->
 <!ENTITY mixed_content_blocked_all "&brandShortName; has blocked insecure elements on this page.">
+<!ENTITY mixed_content_blocked_some "&brandShortName; has blocked some insecure elements on this page.">
+<!ENTITY mixed_content_display_loaded "This page has some insecure elements.">
 <!ENTITY mixed_content_protection_disabled "You have disabled protection from insecure elements.">
 
 <!ENTITY loaded_mixed_content_message "This page is displaying content that isn\'t secure.">
 <!ENTITY blocked_mixed_content_message_top "&brandShortName; has blocked content that isn\'t secure.">
 <!ENTITY blocked_mixed_content_message_bottom "Most websites will still work properly even when this content is blocked.">
 
 <!-- Tracking content notifications in site identity popup -->
 <!ENTITY doorhanger_tracking_title "Tracking protection">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -337,17 +337,16 @@ gbjar.sources += [
     'home/PanelRecyclerViewAdapter.java',
     'home/PanelRefreshLayout.java',
     'home/PanelViewAdapter.java',
     'home/PanelViewItemHandler.java',
     'home/PinSiteDialog.java',
     'home/ReadingListPanel.java',
     'home/ReadingListRow.java',
     'home/RecentTabsPanel.java',
-    'home/RecyclerViewItemClickListener.java',
     'home/RemoteTabsBaseFragment.java',
     'home/RemoteTabsExpandableListFragment.java',
     'home/RemoteTabsExpandableListState.java',
     'home/RemoteTabsPanel.java',
     'home/RemoteTabsSplitPlaneFragment.java',
     'home/RemoteTabsStaticFragment.java',
     'home/SearchEngine.java',
     'home/SearchEngineAdapter.java',
@@ -534,16 +533,17 @@ gbjar.sources += [
     'widget/FloatingHintEditText.java',
     'widget/FlowLayout.java',
     'widget/GeckoActionProvider.java',
     'widget/GeckoPopupMenu.java',
     'widget/GeckoSwipeRefreshLayout.java',
     'widget/GeckoViewFlipper.java',
     'widget/IconTabWidget.java',
     'widget/LoginDoorHanger.java',
+    'widget/RecyclerViewClickSupport.java',
     'widget/ResizablePathDrawable.java',
     'widget/RoundedCornerLayout.java',
     'widget/SiteLogins.java',
     'widget/SquaredImageView.java',
     'widget/SquaredRelativeLayout.java',
     'widget/SwipeDismissListViewTouchListener.java',
     'widget/TabThumbnailWrapper.java',
     'widget/ThumbnailView.java',
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..22163c303eb3c110e605e9737827985e30a2aae0
GIT binary patch
literal 937
zc$@*L16KTrP)<h;3K|Lk000e1NJLTq001oj002A)1^@s6@{lv600001b5ch_0Itp)
z=>Px&V@X6oRA>e5TF*}tK@@&(+M+>zMH2|dL<|=_nGh1Z5fYWsw3MqiPu}#TAu-XT
zG2%((Z}6z4P16RG#v3G!9@PT@5~9IGBLxF3?T&8>gzc7nyOk05FugD{-+SNt_;&VX
zr(FaMpH%8ypA_W;NGmD<2SCDk0C^4oe*nT)1i1nza%i1OCzIFLk6RLtLy%7AhH6&z
zi3E9rc!Uow3jyatBK$Csh<@-9e`LsCiO%J6XLl;QFUiSUQp%P&5K+>VczwDpayJ@{
z{yb6{v)FRMOeS+>w+3k{I>_+)gm5Vmf<!zX|Ku~F9@diRxw-5$3#>f3+to@DE|y#`
z2@vCxld%OZ&W`yOoy}%1RI0V2l+b4<&@o1Y^|o+mI2Mbo`Qh_(^FXm!jFhXj)V~m{
zL@Hjzuj=>_9rhEn_HF$RwXq>TvDD(ID_{|j+MQHwPY(OaCH{IHfLX6Pp4+2PD0KWP
zZ7mT|FH44)5|Pk!a$@3(GcKLZU#`mCr}R%GotR_(y{D^VaBOUB%dy@t^>n&a+PX~{
ztYEsC9!yQfpEoeuCW*xOXPQpEoy**(n|Q_sIfuRb^<Fsv``mL+Qx~4HCpgDKh-aK&
zd%TLHOUsD{uX>7)75%1A=%M{NYA@U{MqG`A@NvVM$D}P1dPMtu)jbSH_v*Swu%bho
z+gIswx^5wXI7@A+>`vc|sCbpvjO?wbdaAA)uGU%6bm!B9kNZao$ho8Lh<6@emla);
zd~$bt`?9+<cf>2tt1c^=obGfl4dB(9+;YA1__`jV-OB#obtC$u{5MV;rx^##_F&A>
z3K^%h+R2dBPcG_ova$L59vzEDB+Bl?cpRY9#>K<AV}$VDvEqL!fqmqqM~IaJGL*PM
z$GFAr&NDC6BLL>}`QGhvWq}Ttmkxr0`j<}a%XDfV32*OIXCwq)urg8^Tdr9%V*h)%
zpqksP%YvKKJ!F3~666)xZ(80-T_5ALahh?!Y!Aj9t&nkAtDOv)ahfqlD`cG3Y9~Wx
zoMz0?3K^%h+R2a^rx|m!LdI#Wb~0qfX~rC_ka1e8oeUZMOpPT54;)l~=ZU_2TnwBt
zMv&4gL|9ByOofRH5>!!oMKTuqNU!l+qjxaogSu7fx=5eWsr3E<GrIrc_~bcY00000
LNkvXXu0mjf;2^;(
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..57d949ad00b36f2d79412889582e8c0fcdf62d10
GIT binary patch
literal 528
zc$@(c0`L8aP)<h;3K|Lk000e1NJLTq0015U0015c1^@s6J20-I00001b5ch_0Itp)
z=>Px$%1J~)R7efgma$I5Fc60C+=K;I)-H$*o&Y2e0;xjf4S1LgJO*N;bEQ@Y34|En
zEsBI#nHdmiOzg(46Fc$Q5d)U2{JGfo<<DKy0Q|SXXM=MwWA7VdHu70=3|KtLy_}uk
zMgAO`hlOsC>ycjp2b5Fjh!G6t^W+pbKhUgF%a{((L@*eDXLf2=%wA<ig20wrl_|n?
z5ZLWj(=(+gM>4q8t)c|FCWC8kWk2IBpqOQE>p`zI32eK?367VSVgle~&82rZ30!k4
zC>$4Sn4x8m=GqIk-D;#;5NquPTW;OBM%3@R6CCVT;R&ug!EtUiJyXhcYz0TVRTPiw
zR&YPJnw}}uJUYQ%w+uFPB2(*NB|2O?KLq%E@KXEhYbHc^gCrfvaQ<^&zPomsOY}M7
zYx$x%)aRA3z$>@KLvvxTb7W^Y&L7Pjjz{hP4VnpP+Rj-6duyvG;!&sRuAD;qhNIRs
zFujx66i43+F#*`r$6Z~oyVf-hs%=PfnRjK>+5mex*-Q|tjSiQOT7D;&b%@PJ-Y05(
zmmG=pRgH^1^5(t^m&LwFk;pt-!)y5ZD9X>6?Y?<C_jCA!&EXBed*`x0z4!~6e#0&-
S<U`H?0000<MNUMnLSTZMlkY14
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0a1f4867e20d11a2bd7d56e7100fb48038593a5e
GIT binary patch
literal 544
zc$@(s0^j|KP)<h;3K|Lk000e1NJLTq0015U0015c1^@s6J20-I00001b5ch_0Itp)
z=>Px$+DSw~R7efgmcdQ}F%X8^6}_n3d;;Q4pTL90XhIAGym0biIQSUGn_dN$7!r-~
zJ%lIlWQ-@FwbRY4yWMu%DPANgoenee_3t#KjQzLIC`~V5#O|BNBtE3+377l@awV6i
zm)-6wwN64!fOrMHmQo?r06V4z89bU!&lK<oVPJy~*P$~R%mf=&irajZpt6GmHq~OO
z7}X@ORV~tqs)NBUwaBHa4h9$1a=6Qk-|3-jgcoCSpTh_OSJX;&-|uy<U@&E4ZHnq3
zu&G2cInQAQTl)fg?FCoVBGEC2s`i3SwGLj{9iX}vOjV2Y5Y@HdD78rUsCI&5)gsrY
z+6i7uEz)aLTfttnNwS5tS<%j&nNg>@VZ>(3nw{zZAqV5}>=tYxmV+$G@txIrHh-<W
zK(yMGd@=*qS@R8k-tTlu@aj?f)W+<<QnLj1_BMy|MZz}sVW@}DB6m9sTdzyt(3_kj
zY}D(u=P<x_@XBsOb>3rIZ$WXR4V493&eU78Ri@Zi#a{p3YGX(CRP@T0VBc@J3Z65D
zzGV8S;hhY92rdvA2#m?(@fgxII1%h1vKBc_%GDtx>7#BocWypDzl3_y?2#)G5|rZi
i=I*xs;~}K|ZRig%qU^@0H8TDH0000<MNUMnLSTY9tN|VX
--- a/mobile/android/base/resources/drawable-large-v11/site_security_level.xml
+++ b/mobile/android/base/resources/drawable-large-v11/site_security_level.xml
@@ -2,15 +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/. -->
 
 <level-list xmlns:android="http://schemas.android.com/apk/res/android">
 
     <item android:maxLevel="0" android:drawable="@drawable/site_security_unknown"/>
     <item android:maxLevel="1" android:drawable="@drawable/lock_secure"/>
-    <!-- Bug 1185600 will handle Passive Mixed Content and resolve this icon duplication -->
     <item android:maxLevel="2" android:drawable="@drawable/lock_secure"/>
-    <item android:maxLevel="3" android:drawable="@drawable/shield_enabled"/>
-    <item android:maxLevel="4" android:drawable="@drawable/shield_disabled"/>
-    <item android:maxLevel="5" android:drawable="@drawable/lock_disabled"/>
+    <item android:maxLevel="3" android:drawable="@drawable/warning_minor"/>
+    <item android:maxLevel="4" android:drawable="@drawable/lock_disabled"/>
+    <item android:maxLevel="5" android:drawable="@drawable/shield_enabled"/>
+    <item android:maxLevel="6" android:drawable="@drawable/shield_disabled"/>
 
 </level-list>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5f52e8a8b7a68b69d49324660bba650af478db5d
GIT binary patch
literal 1085
zc$@(}1j74?P)<h;3K|Lk000e1NJLTq002Ay002-31^@s6MamXa00001b5ch_0Itp)
z=>Px&_en%SRCodHTTN&aK@^@hF|mfUieRO^w%|ceT09g4dkWNO(xN91-u*dgMG)yh
z@Dja=cP}a^ZKAQzQx&17dh?|9+F}Jk4YhX3j`I>yX(riyGf9-4%tD%-H}Ac9-}~Oo
z&Th8Ax_m4wER1*+krl!}!59n+!G;+V@`DGg0kAdMmf!q+JO;(d$;r>PzV7^mZn#}s
zTpZlm_OAMZO)(*kX^jE!fdiZGOT-_h)9DQ@TY_>&0WU2rrGAt*ulv4#Lynyyj31;@
z=A7RjNF^UkOiYx66m|MK2Dn%(O$!i@WXT@Z2_Q^Ymi!kGa5<aJ%!TPv`Z+}@NJj_(
z#aFLq1+W*L28-jOPxM_KLyi>`3#G3u2g}RL`!>FQdn}l^KpCuE9boW$@X*0a<KyGM
zOnn(MjLiN^um}(nTOG3vremg=IzD!CR)W4_YTt(4(T|^2#j~eR-kSI@D(Tt+g>$z3
zIb{?y$Yo5h^Ck<KHekH;eE(Tq?ZyJ710GlJ{r#y?-Ak7C9LDRcK3MQ@!2!-MCid_n
z2L@98_`!3OVV~S}c{+eG28>0=26eR<#vRn=);4ba$eRiYW<C+;Ghtgs+=6a<eqJt2
zQyL)3$-cyh&W=orVF~t{23TM2ly2s-ndvaF2oQBp7j0zIM;YNr;TD#lx1*v3m;vN=
zEJIFY;J318&*VeYly0;^Tcx9gpt@gg(br5&E@>>e-(F?4nUh8tmNBx`DA#EhilsSf
zdGC$KMk!T<<C7W2TRd98;NhI5>9*FY`JgRi^w=~2*Y0Oq2UzcsB~zegyKd9$8-1X!
zW_^t^YHVmnW*BeY8wYCI=0FOvscoZ>?28zi^iKvXuX`lGan4p6<2G$meNkh>ILLti
zK9&g%KbcI*M>d@~=)UTYjKas&08HC#=x+6bLe%g@-U#Uc4vH-@eGYJBs4@)HF>IK)
z$e1~XBSV#8ppId~#6`x;F&r7H3<Grx8zwF?W<AI7&O^LHsdS3@Vn+UYGA8eRhay8+
zV4$!rf0bEb9P+tL<{he50T*7DZp-t#|LeD+z?eJJ=>x!>+_}s>KxRcnypeuwFK-`W
zaGGNo_W)R|lPXqTo3q%q|3Iv=jBQz<D1?b2+vly41qWD3iDd%Euw{fQa2&%*N-PsN
zhAkshf#VofQev6FF>D#33LM9<k`l`Vj$z9PRp2;=m6TW}a12{Ur~=0^tfa&;fn(S*
zLKQfUVI?J&2^_<g5vss(3@a(IOyC%{j8FwmFER}4N*b&cz&fwKxMeNC-M|nl$G6m^
znEDUOyFEm!<s(*(ujJudYL<hpy@`id>m>EcyOw_dBL_Pl-J0MO00000NkvXXu0mjf
DXJz!5
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a5c86af709d2b2985f98d37869f3eff528af9099
GIT binary patch
literal 876
zc$@)j1C#uTP)<h;3K|Lk000e1NJLTq001Ze001Zm1^@s6jQ+T700001b5ch_0Itp)
z=>Px&CP_p=R9Fe+m+NlRFcim+<0jSKfqmsNHZHsI2(Wet#wNi8pZUrQFyJ|OjrFoU
z0s{sT2qeBBF0X*5HgyiiPSPfglh|ocK}3pTAD_!F|JsfV@V{Kx;g7K6d{y@$e^}Rh
zvpN#tp}PqnUZr#K8|=1!lD66R9QzPmMDQYO)!DLMWtN#0#J;-$BcWHS!8h5K|GEsK
zXnb}BMS)U9V?|Vnv6&RauDcF_C|1bf8?5cWNz}Az&ZZz1s25`d!SlFT<yl!l?6_-y
z!b};)*V(rJwhCZ%1<^to8{et7sH7ksxvSs{J4IZ+#%}qiQ}s)`)f60As23wtiJ@3)
z1+nK|2O+GLP@J5vvX=iYW;S`+E10_~Ss!?FQQbgo-IW`wa}I4*3evuJjaKd{phkIu
zW398FGA$l2EW&W`JyVv+NK&!wzt4>280-|33h3Y{c?>7(u$yN;wDn{x2G6u(IYwhZ
zI<Z^@hIexESeWsy%4HZ~3<!+gJV!mlTA`cFA#)jCMu6D&uF%ZdG0bV;hC5--a#tfj
zI!@K2Ud$s^lsOHqQb6o_D>V1+8RW8ap`0prNmMByZ7cPv7XzskU9MswAa=ZEntQu(
z&ibFY8`j9rTnI>ig#L_rF^}36#6jHEJ#zu^$h%DH?dgJQwIS|>we(qY0qHn(V6*S3
z&s$U@+18g$Sshg?Wtw!f-(u`}OCUxc$SwZ`COM*(JS=#RRYwe8lKNX2mwZD2ys^Ss
z!%x#LjcJ4G2Wnnez{w~A5(E7=SX(4zbm-5QoQwM@D|Ipo(yF=yIQmFqmVnc#t@{D6
z#xmO;eojVeAgZXBOj)^2EUI=YK?)F$y^DZ&KjmTEF*vZ6<)Ye(A>o$-gy9V8(E^cY
zLUk^T7fYg$VN<T?r%B&2AoiOVXrezz4$h^GxF52F)yIIetw{1o-<FQH7VIsYlQ*bs
z#x}v&--%e1L9Jr!HqS%g%RPvX-R9d6;J8byJ@^tT7AW=p7C0#lhma}yNg1~1E10Hw
z#1txs@Jr(|UsJ>sEeiI8zA4}4OO&XtScvIgxPAlhc@GRk`P7>L0000<MNUMnLSTYQ
CBbtQ(
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4519e04d778d8d4c9212631a2166e28c33e9070b
GIT binary patch
literal 921
zc$@*517`e*P)<h;3K|Lk000e1NJLTq001Ze001Zm1^@s6jQ+T700001b5ch_0Itp)
z=>Px&Q%OWYR9Fesm)lO%Kp4ko+O9qYw;m8YaPkq*cFO{iD2d*B>k9yq_!i!K4c*;#
z;SoHciAEFeJiLO5(9V3$*WF^7w$o|1WU<N8Ieg#$`}NPvY?qJ=e$l4*Jg@&42zh~e
z)PSvy>%MA2yAi-%ueTI&@;VJ<8RV|(x^L2Bb=pRqBsnJnva2&D$LRFOcCgzWEHa34
zoieGlw6b#ltqw?0%-9Zc!gh<qRdYo-^7vQ|dcMEFBd&DFu#Cly<Ghm#>-NWT5F*$u
zE5sRpUKXR8ukGMozduhH=s9E3g|&A3y$YmguC{}egSInOVr9@;4)%Hna}f_KIbs>D
zdDp#nBm<Q7YdOexr0re;P$vg-D?8`~!5tp5%1&T0qh-!@Z2JSYb+5_}ien|;JxT_b
zTjmsg#A6Tg019*Sf=6SO9NhE$+myi(0A%tI%(m@csJnl7I0b)Be#mtxr2r<KTXUR`
z@?25Bl7m#7X?bucp1@&yb$R(G-sKB(X#_j+xT>!WDBf6Zi6z@niD}JIvO3h#iYD5C
z5zw6?_w-5)qfi62baS)-J#TPREV?ya(Xu$yLd~)`oYw*r&)kT62@tx7nn_=EF9qE9
z{aLa0)>R_)=IHuRTb*4BNW`vdV(rNov8Kk3Rs#0>{TXrSK9I#5>kI9;hC)yY7%_4#
z?j=0vV#vvM($BjW0(QFxH-vi|c}b0q0c^yz3PcM5ISIF`tUdgCOKPOs$(Z79V6CG0
z%y%jV?gzm%gzS@O7Ct~m5MLH7^C^xWN9-j5az7JOJiuwooL#l;FLH<_55)bD;}hw_
zVUhqk4q5JZaIC5RV~}CZO*t&%px9N{Lk6EkU^PHlxq6?sOghtUx4))SGZ3R)l^IVb
zom1P$+tFu0dcDJI1mIyU0F4*cQe9CyW=I8|0c3DeeeI3JaD(7GA%2q0lB%QIG#C%J
z(!r;I-e7PQfILc1PNEG2HjyQ%eY%Uq4<RH!1x^BV?|8HXLGbUG(M`<886%b_I2G?=
z-}kSaL>$i`GaEmV5q8NkrX9!m7CRO)j_zF)ICR7uF%AEvK&UejpA<zCrr1I8WAL{k
vb|O;=({NK5<Qbv#`$S0ciXsa#{SVh)X%a+0RwQFw00000NkvXXu0mjfgr%t{
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..af1b4b71a859b41b63dcfe1c8c1e729f67e3ad0c
GIT binary patch
literal 1933
zc%0>x{W}u~0LQnrLoS))NtdEXDsLB>ZZ4+|i@lv;YHM-o5sEU}n1qDXF=|d;HWQ0-
zQ{J<g(rIk-GV|6ADZ^$STVCsym-A2DPxr(3^Ld`nKk&`^6%9Ag-=hxz01SN2cw)DV
z-&(w$&K61C(?4$s7>k9U2GsQKodp1N;Xa<H0^))53{?2}K$A{QlC#jIOq%a!`?%)z
zXcLyM%?p%PIA`POWVzJqRPI@$th*L^*fa?P;cBlhLmo0VhH$VZwez?+K|0o&nD5(t
za7n!~R#5l=Jy5u$-fZiYl%z&1s=sdbom99sYvjF*EZoU2RRIn=jyB$W{V4u<*7A!c
z^4W7quL4)ieMe4?%ego$SGsCZy)xbPPV7hLRSio135}T$WmHf$hWHOeQmItp8@2wZ
zNtFQNmM*G3cSmJkNGavuf(>?Q(X^Z#heW~l$T^sj)TnL9>e+tVIgHbj`6v1cvC=f^
zn8?e<5QAKUIFV&%iQ9<GQj24Pw#q+YxzGqlK2DL3yY_RC9dl_h?TOn?YD~;R>gKS@
zStKhDQQsnS`gSZUDg^?;#_G2p6x?!_VrJJ)!ElM6NWF+~r6YpHv&!(NE*m$h?lLAl
z8Q`+jQu{v4TPRcGekmw#e3}&VWBRk_{w3Y;<u`9mm+dOX<kvb<e4p5UwOT)Sz?ARg
z?tVdBw~0JuJe7E5<t5Ul)uzr4l*1Xqq~BEk(B7us$wj4fd)QdDm?w`c<rarPLui+d
z*Ezd++hpFqU?D_xscoi<rdnI<KWGY5&}b(<6&bBdBlW7l->*q(W40@590CqGhPt6s
zR|`ve_vt6inwrFsoUzrLGP8o~dgP=iR&KK06bdErD;Qh2u^t8njxEcGW{ob*3#IMe
zaZVJY(Q6Cz=zAgIw<h65mW;qW{yPh=ARS%Z=O^BZL?ZR^i)7pK&(eU|gN$xw#SCj(
z=Hu);MP}Qfgg=mw@s6(Kw6Jm9Y(gYF`PP#j7lmdpEs<HS_`NucNUX;6@Ay!?Ja#JT
z%U{nvAL;Us-|r>2ew~I+>66&cw*boM!zx6eGC7JAw~Q%`uSXyZpUPx5!_Q1_u5rv^
z$J`M%0u5nwAX@kP{@Q!Nn9-POruPeW)2vWhW=QzV{yVzW10JTBnXsJDb>Wt^4^|oI
z7+8M<(8UvDmseE5T@^{7!mxou_vNLPsWKlo!nt^#{;LN-#iVLO6Rr8xSiL#Acb$cM
z!frm*Bz3Hd1arA$+H4fd?av%B3@T2%-y?1ng44*Fg{XT4+8_$j+t73#cBA&8{Fa+v
zW>R>z!%tutpD-S8)%eRZg67HQAJyuP=9Tv;oI$v>;PROuxxj6S3GC8IB_lwGGc>%R
zxb!m2KNpA<NB&;<sP&ac^kcJqIQBhjD7^*(rCp=i#f}*fHaCO~N|fR+8M;^AdN&Zp
zCW~M4<iGSyMQG7l5~Yx7ScOv=@97unTlRH49E}WqUP&9yp`YZ$6=15_DY{^p{lKES
zc*dcX&-AXI{{~}=Is<CNRzkvIX09Xc?A{3!gbhvAslZg=3ZUSkwfC&z`s@370pu4L
z4W9VmaJUA+%gf74_V?4@uk&@e<~Udhn7&4XDDS>Er-K5Hu(o+j-GNB<`T|`*Fn}jo
z57<*?;$iil<h%B!kAsfhPgFIvIZCNry9d<%K+SbnOZLq8Xv5|45}#f_lcDTJaRTGj
zuXZ`aNc+h{lGxaxzXU}zrSne7V&yQpX%x!o+_1;?7qg`u+1BB%9s`%%(NY!pOa0+d
zgGe4V;_0W(2FS*pfrFjO`e~6FsAZqitE8NW7A(qRF0xVkiF(!1|6;pi$xE3L%a1rG
zp8dE+9p77dvCbnJ{_Q)3&L`@)IZa7`7=mItM3$^hOD(<ueim<(V9c_9MdLpFh}P(Z
zPJQ?82qoiu01NGr252Dzd#t}vfnD@+ub4{CWB-S7uX6YA2UdH*p&(9y+ni^rkwm<l
z&V5~aT|k|HMt7c&=DG8uj;gU`Imgz46tf=OOtK$teimy^p)})(MRw-YlAZfxQvsVs
z?3E!NZ_*(Ej@(D+Jkio>oA15Z^|WpMo|1Ba!K+7v&OU-Q)x0(f9-N1c{dTDapVYr6
zu^Z2);WMg!RICcit$<wQuvp!p$Pu!$vK+_GL~}QEANtRM-rQ*rZv893#|!OQb2{Yq
EzapHcuK)l5
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a40dbcdb9fb7f99ae1c4aeb12006f72d0d2d131e
GIT binary patch
literal 900
zc$@)*1AF|5P)<h;3K|Lk000e1NJLTq002Ay002A)1^@s6I{evk00001b5ch_0Itp)
z=>Px&K1oDDRA>e5n#+#UFcd(qlXf)X6XrvB?AXqO9XlR>z<$^u#0F-^H}DJdl4ivY
z!5;Vz7HCwO7$-BgvJ>0!>)Nf9N>!ZeNAB@S($p=${F-NAo`IMd;OF&oeo_7G-=Z8d
zR!CnK(kZkLzg|3nb^BHH-@6}U1&it_J6wMY9n-g8<=Y{ueZxPC{s`L*wFh^xs9}9N
zX9MO}^&xQnaFm>g^>w2Pnm{&SXyLsP!B%JN6jqT1*k~7X&S*Icu+=U?XR=HVJZTq!
zGg&4FcCw4m87z|mJKIIz43<fOz3d`1v1Kw~Z@UOgY?%x=$Sy(?S|$Mwwu``omPvr4
z>>@Og<>bK8b`hA!a&q7ayDMOD3d{Y2{@s-UR>(No5?D_9eYm&V@T*bc7^vVaJ8nPW
zZnx!^)v1`Ik8(^Y*X)QLHP=ckVkfQPDI?4MFQZ&_F}cyym~vZ(ax%k{c11GmS*txB
zKd-no*MyeS8BT0MnSg|2Cjm}&XI3bsorHJnB)}7QW-0TCVj|1Yz~pvujuKgp2F_v^
zp%Yk+0?uX^ffHDc0?uj|p(8B^18293z>$`Nfs5Eh=m^U}z{Tt$aD?R`;G%XBI?%E=
zaB;f`9BA1acq{B8bbw_q;H|NXzyX%MfMf0Qn+FG&D{gf$taI9WT6P9bVs~}>Y6S7`
z_Vp;{6Y#XG{X(ug9>1u+Yk$zzH8#h(HLZ8BxL^5L3s?>xM05FH0p{MO8peLGljf1B
zT8=vb_jYY-Rq}|X%8sidZl|>8by9@cF~>>ZB-d_^8F0LoCxK({%sE&$$8fN01tzfT
zw-CP}aV$7kURZ~6m@#YrVWqf3vy=7=;4jvkiGSpY)p$@A=dm(qdzZ6u;ofMNz%Fb)
zj${)2h>~M5<q6A1;GMd5hZAHuGqu1ZcDtmy2aVxN7aO+m-M3iQK6oU0s?w!N^Q5Ia
zlHuuV1l-%*_NOWxpf09)`Wm0omh)dX=2pivEDA8$wOeXdG}p2M3=O;~l^fY{2TUq3
z<B!P>Tx#F}lL`#%cd5MSj>~?@W2?aR@`d=F{ziOB|06fr$#TyiSnB1=oy>nudY%Ed
a8Tb#<U}mmMb+S(Y0000<MNUMnLSTY3k*cQv
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a7d788c1cf6e14a91a23db233801ace407c4f910
GIT binary patch
literal 934
zc$@*I16lluP)<h;3K|Lk000e1NJLTq002Ay002A)1^@s6I{evk00001b5ch_0Itp)
z=>Px&U`a$lRA>e5n!R!pF%ZWke+6D455ho4m6-%=T+jh|1Nx-ltATVpLuv|RLo(xv
zjyHfe-~p(D@mZXVm(l5@yIpCe#Ed<Yk#28K`v3NC*@_Y&cCTFp{;LYe+%%Kf?6FwM
z*XwyZ>g9Mi{E$;aR^Zv}^4@Z_{89oAHjs;6r+0riJp7sqx|3@nJ<n1V@5=^Yb!f1O
z>3)`1HsI;>;!r4cr$$xA*7s^R(gd;r7h?6whtPD-_e^L!3$WK+P@U1`EWj8CF*#75
zm^L|ZtGl2+lgr70Db7)Hp-$8`DKOeyP@Tc$q`)DLg**fG!nDbN$?k&M#4aZTj&UmH
zS*V+@O#&S3E~rfCauQ$;hd9Q7`k5^|aJ0LiE|JUZz%9-!q?9MO_xk(#NB2v<1u)ue
z%UE_g>YDZUVeM|AiVNS&veP*o4<EcU?@wl*Pt<Dl){L3`uIwI-Mn{)sET+%uaGN7?
zuYYcfb;Zn_wlU3IOrH%HvvwaH9Q?3Fxnh+Z=C#ZQ%<Y5;%{ev;FveXIfZS(vnFW~B
z0ben==7<JP?k-e-$>nHZuF3;fQ_C?|6mT|op$ZHxM*(wG99Xbz?3}^C+1-VTM7kUd
z99wD7AJWFm9Rz%B?m~4UTn++`sW51cZ9_GXfv??Ns8pcKWZ=-sf=<phL<<S{#<&Yr
z3vihP98yuxrrVG6`M!DWi`h9%*HletU=DZH)#8~gUj6!gY{y8~M3;Nm!KsyWjjNP;
zGnsx8w+6j)egA5)JT8xS{{Fv*>j7kJqYZToggVpG?lzrOA$289!=8Pi-qIZICZ#P0
z$7*Z^=2*L_Vb|j$*K#W`wsKjZS|-70Z3Ir>F6kh?g|B`a;d0}rG^S$pS^bgnFAXoo
zqlafk#+m-qE<e31hx5jLa(AQI`-$y!IDxzWzK%=^_=+-PQT-N|y};b=MnMoCZqn`q
zPT{WBtT!6Ac2~^KAzwGR>|Dc56(Ub=JmB1EmAfw^g{|vb%e9@BcK4?5s<2gY?Y?Sn
z=~V7wMRMl3SSrA&)-ERmX)MQDt^gOJcupE{3zBW73a}LN?iMhRB-%_BV4=i!(rDX}
z{LPeE0ak<V6CuUC+yeeIleV$HW++)yzx#9>_3t$ADuArOAIcxQd}skM=l}o!07*qo
IM6N<$f-7Uir2qf`
--- a/mobile/android/base/resources/drawable/site_security_level.xml
+++ b/mobile/android/base/resources/drawable/site_security_level.xml
@@ -2,15 +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/. -->
 
 <level-list xmlns:android="http://schemas.android.com/apk/res/android">
 
     <item android:maxLevel="0" android:drawable="@android:color/transparent"/>
     <item android:maxLevel="1" android:drawable="@drawable/lock_secure"/>
-    <!-- Bug 1185600 will handle Passive Mixed Content and resolve this icon duplication -->
     <item android:maxLevel="2" android:drawable="@drawable/lock_secure"/>
-    <item android:maxLevel="3" android:drawable="@drawable/shield_enabled"/>
-    <item android:maxLevel="4" android:drawable="@drawable/shield_disabled"/>
-    <item android:maxLevel="5" android:drawable="@drawable/lock_disabled"/>
+    <item android:maxLevel="3" android:drawable="@drawable/warning_minor"/>
+    <item android:maxLevel="4" android:drawable="@drawable/lock_disabled"/>
+    <item android:maxLevel="5" android:drawable="@drawable/shield_enabled"/>
+    <item android:maxLevel="6" android:drawable="@drawable/shield_disabled"/>
 
 </level-list>
--- a/mobile/android/base/resources/values/ids.xml
+++ b/mobile/android/base/resources/values/ids.xml
@@ -4,10 +4,11 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
 
     <item type="id" name="tabQueueNotification"/>
     <item type="id" name="guestNotification"/>
     <item type="id" name="original_height"/>
     <item type="id" name="menu_items"/>
+    <item type="id" name="recycler_view_click_support" />
 
 </resources>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -464,16 +464,18 @@
   <string name="bookmarkdefaults_title_marketplace">@bookmarks_marketplace@</string>
   <string name="bookmarkdefaults_url_marketplace">https://marketplace.firefox.com/</string>
 
   <!-- Site identity popup -->
   <string name="identity_connection_secure">&identity_connection_secure;</string>
   <string name="identity_connection_insecure">&identity_connection_insecure;</string>
 
   <string name="mixed_content_blocked_all">&mixed_content_blocked_all;</string>
+  <string name="mixed_content_blocked_some">&mixed_content_blocked_some;</string>
+  <string name="mixed_content_display_loaded">&mixed_content_display_loaded;</string>
   <string name="mixed_content_protection_disabled">&mixed_content_protection_disabled;</string>
 
   <string name="doorhanger_tracking_title">&doorhanger_tracking_title;</string>
   <string name="doorhanger_tracking_state_enabled">&doorhanger_tracking_state_enabled;</string>
   <string name="doorhanger_tracking_state_disabled">&doorhanger_tracking_state_disabled;</string>
   <string name="doorhanger_tracking_message_enabled">&doorhanger_tracking_message_enabled1;</string>
   <string name="doorhanger_tracking_message_disabled">&doorhanger_tracking_message_disabled1;</string>
 
--- a/mobile/android/base/sync/telemetry/TelemetryContract.java
+++ b/mobile/android/base/sync/telemetry/TelemetryContract.java
@@ -40,9 +40,17 @@ public class TelemetryContract {
 
   /**
    * We are (now) a Sync 1.5 (Firefox Accounts-based) client that migrated from
    * Sync 1.1. We have presented the user the "complete upgrade" notification
    * and they have successfully completed the upgrade process by entering their
    * Firefox Account credentials.
    */
   public static final String SYNC11_MIGRATIONS_COMPLETED = "FENNEC_SYNC11_MIGRATIONS_COMPLETED";
+
+  public static final String SYNC_STARTED = "FENNEC_SYNC_NUMBER_OF_SYNCS_STARTED";
+
+  public static final String SYNC_COMPLETED = "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED";
+
+  public static final String SYNC_FAILED = "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED";
+
+  public static final String SYNC_FAILED_BACKOFF = "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED_BACKOFF";
 }
--- a/mobile/android/base/toolbar/SiteIdentityPopup.java
+++ b/mobile/android/base/toolbar/SiteIdentityPopup.java
@@ -291,54 +291,91 @@ public class SiteIdentityPopup extends A
         }
     }
 
     private void toggleIdentityKnownContainerVisibility(final boolean isIdentityKnown) {
         final int identityInfoVisibility = isIdentityKnown ? View.VISIBLE : View.GONE;
         mIdentityKnownContainer.setVisibility(identityInfoVisibility);
     }
 
+    /**
+     * Update the Site Identity content to reflect connection state.
+     *
+     * The connection state should reflect the combination of:
+     * a) Connection encryption
+     * b) Mixed Content state (Active/Display Mixed content, loaded, blocked, none, etc)
+     * and update the icons and strings to inform the user of that state.
+     *
+     * @param siteIdentity SiteIdentity information about the connection.
+     */
     private void updateConnectionState(final SiteIdentity siteIdentity) {
-        if (siteIdentity.getEncrypted()) {
+        if (!siteIdentity.isSecure()) {
+            if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_LOADED) {
+                // Active Mixed Content loaded because user has disabled blocking.
+                mIcon.setImageResource(R.drawable.lock_disabled);
+                clearSecurityStateIcon();
+                mMixedContentActivity.setVisibility(View.VISIBLE);
+                mMixedContentActivity.setText(R.string.mixed_content_protection_disabled);
+
+                mLink.setVisibility(View.VISIBLE);
+            } else if (siteIdentity.getMixedModeDisplay() == MixedMode.MIXED_CONTENT_LOADED) {
+                // Passive Mixed Content loaded.
+                mIcon.setImageResource(R.drawable.lock_inactive);
+                setSecurityStateIcon(R.drawable.warning_major, 1);
+                mMixedContentActivity.setVisibility(View.VISIBLE);
+                if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_BLOCKED) {
+                    mMixedContentActivity.setText(R.string.mixed_content_blocked_some);
+                } else {
+                    mMixedContentActivity.setText(R.string.mixed_content_display_loaded);
+                }
+                mLink.setVisibility(View.VISIBLE);
+
+            } else {
+                // Unencrypted connection with no mixed content.
+                mIcon.setImageResource(R.drawable.globe_light);
+                clearSecurityStateIcon();
+
+                mMixedContentActivity.setVisibility(View.GONE);
+                mLink.setVisibility(View.GONE);
+            }
+
+            mSecurityState.setText(R.string.identity_connection_insecure);
+            mSecurityState.setTextColor(mResources.getColor(R.color.placeholder_active_grey));
+        } else {
+            // Connection is secure.
             mIcon.setImageResource(R.drawable.lock_secure);
 
+            setSecurityStateIcon(R.drawable.img_check, 2);
             mSecurityState.setTextColor(mResources.getColor(R.color.affirmative_green));
-            final Drawable stateIcon = ContextCompat.getDrawable(mContext, R.drawable.img_check);
-            stateIcon.setBounds(0, 0, stateIcon.getIntrinsicWidth()/2, stateIcon.getIntrinsicHeight()/2);
-            mSecurityState.setCompoundDrawables(stateIcon, null, null, null);
-            mSecurityState.setCompoundDrawablePadding((int) mResources.getDimension(R.dimen.doorhanger_drawable_padding));
             mSecurityState.setText(R.string.identity_connection_secure);
 
-            if (siteIdentity.getMixedMode() == MixedMode.MIXED_CONTENT_BLOCKED) {
+            // Mixed content has been blocked, if present.
+            if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_BLOCKED ||
+                siteIdentity.getMixedModeDisplay() == MixedMode.MIXED_CONTENT_BLOCKED) {
                 mMixedContentActivity.setVisibility(View.VISIBLE);
                 mMixedContentActivity.setText(R.string.mixed_content_blocked_all);
                 mLink.setVisibility(View.VISIBLE);
             } else {
                 mMixedContentActivity.setVisibility(View.GONE);
                 mLink.setVisibility(View.GONE);
             }
-        } else {
-            if (siteIdentity.getMixedMode() == MixedMode.MIXED_CONTENT_LOADED) {
-                mIcon.setImageResource(R.drawable.lock_disabled);
-                mMixedContentActivity.setVisibility(View.VISIBLE);
-                mMixedContentActivity.setText(R.string.mixed_content_protection_disabled);
-                mLink.setVisibility(View.VISIBLE);
-            } else {
-                mIcon.setImageResource(R.drawable.globe_light);
-                mMixedContentActivity.setVisibility(View.GONE);
-                mLink.setVisibility(View.GONE);
-            }
-
-            mSecurityState.setText(R.string.identity_connection_insecure);
-            mSecurityState.setTextColor(mResources.getColor(R.color.placeholder_active_grey));
-            mSecurityState.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
-            mSecurityState.setCompoundDrawablePadding(0);
         }
     }
 
+    private void clearSecurityStateIcon() {
+        mSecurityState.setCompoundDrawablePadding(0);
+        mSecurityState.setCompoundDrawables(null, null, null, null);
+    }
+
+    private void setSecurityStateIcon(int resource, int factor) {
+        final Drawable stateIcon = ContextCompat.getDrawable(mContext, resource);
+        stateIcon.setBounds(0, 0, stateIcon.getIntrinsicWidth()/factor, stateIcon.getIntrinsicHeight()/factor);
+        mSecurityState.setCompoundDrawables(stateIcon, null, null, null);
+        mSecurityState.setCompoundDrawablePadding((int) mResources.getDimension(R.dimen.doorhanger_drawable_padding));
+    }
     private void updateIdentityInformation(final SiteIdentity siteIdentity) {
         String owner = siteIdentity.getOwner();
         if (owner == null) {
             mOwner.setVisibility(View.GONE);
             mOwnerSupplemental.setVisibility(View.GONE);
         } else {
             mOwner.setVisibility(View.VISIBLE);
             mOwner.setText(owner);
--- a/mobile/android/base/toolbar/ToolbarDisplayLayout.java
+++ b/mobile/android/base/toolbar/ToolbarDisplayLayout.java
@@ -120,19 +120,22 @@ public class ToolbarDisplayLayout extend
 
     private AlphaAnimation mLockFadeIn;
     private TranslateAnimation mTitleSlideLeft;
     private TranslateAnimation mTitleSlideRight;
 
     private final SiteIdentityPopup mSiteIdentityPopup;
     private int mSecurityImageLevel;
 
-    private final int LEVEL_SHIELD_ENABLED = 3;
-    private final int LEVEL_SHIELD_DISABLED = 4;
-    private final int LEVEL_LOCK_DISABLED = 5;
+    // Levels for displaying Mixed Content state icons.
+    private final int LEVEL_WARNING_MINOR = 3;
+    private final int LEVEL_LOCK_DISABLED = 4;
+    // Levels for displaying Tracking Protection state icons.
+    private final int LEVEL_SHIELD_ENABLED = 5;
+    private final int LEVEL_SHIELD_DISABLED = 6;
 
     private PropertyAnimator mForwardAnim;
 
     private final ForegroundColorSpan mUrlColor;
     private final ForegroundColorSpan mBlockedColor;
     private final ForegroundColorSpan mDomainColor;
     private final ForegroundColorSpan mPrivateDomainColor;
 
@@ -414,39 +417,44 @@ public class ToolbarDisplayLayout extend
             siteIdentity = null;
         } else {
             siteIdentity = tab.getSiteIdentity();
         }
 
         mSiteIdentityPopup.setSiteIdentity(siteIdentity);
 
         final SecurityMode securityMode;
-        final MixedMode mixedMode;
+        final MixedMode activeMixedMode;
+        final MixedMode displayMixedMode;
         final TrackingMode trackingMode;
         if (siteIdentity == null) {
             securityMode = SecurityMode.UNKNOWN;
-            mixedMode = MixedMode.UNKNOWN;
+            activeMixedMode = MixedMode.UNKNOWN;
+            displayMixedMode = MixedMode.UNKNOWN;
             trackingMode = TrackingMode.UNKNOWN;
         } else {
             securityMode = siteIdentity.getSecurityMode();
-            mixedMode = siteIdentity.getMixedMode();
+            activeMixedMode = siteIdentity.getMixedModeActive();
+            displayMixedMode = siteIdentity.getMixedModeDisplay();
             trackingMode = siteIdentity.getTrackingMode();
         }
 
         // This is a bit tricky, but we have one icon and three potential indicators.
         // Default to the identity level
         int imageLevel = securityMode.ordinal();
 
         // Check to see if any protection was overridden first
         if (trackingMode == TrackingMode.TRACKING_CONTENT_LOADED) {
             imageLevel = LEVEL_SHIELD_DISABLED;
         } else if (trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED) {
             imageLevel = LEVEL_SHIELD_ENABLED;
-        } else if (mixedMode == MixedMode.MIXED_CONTENT_LOADED) {
+        } else if (activeMixedMode == MixedMode.MIXED_CONTENT_LOADED) {
             imageLevel = LEVEL_LOCK_DISABLED;
+        } else if (displayMixedMode == MixedMode.MIXED_CONTENT_LOADED) {
+            imageLevel = LEVEL_WARNING_MINOR;
         }
 
         if (mSecurityImageLevel != imageLevel) {
             mSecurityImageLevel = imageLevel;
             mSiteSecurity.setImageLevel(mSecurityImageLevel);
             updatePageActions(flags);
         }
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/RecyclerViewClickSupport.java
@@ -0,0 +1,105 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.widget;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import org.mozilla.gecko.R;
+
+/**
+ * {@link RecyclerViewClickSupport} implementation that will notify an OnClickListener about clicks and long clicks
+ * on items displayed by the RecyclerView.
+ * @see <a href="http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/">littlerobots.nl</a>
+ */
+public class RecyclerViewClickSupport {
+    private final RecyclerView mRecyclerView;
+    private OnItemClickListener mOnItemClickListener;
+    private OnItemLongClickListener mOnItemLongClickListener;
+    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (mOnItemClickListener != null) {
+                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
+                mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
+            }
+        }
+    };
+    private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
+        @Override
+        public boolean onLongClick(View v) {
+            if (mOnItemLongClickListener != null) {
+                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
+                return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
+            }
+            return false;
+        }
+    };
+    private RecyclerView.OnChildAttachStateChangeListener mAttachListener
+            = new RecyclerView.OnChildAttachStateChangeListener() {
+        @Override
+        public void onChildViewAttachedToWindow(View view) {
+            if (mOnItemClickListener != null) {
+                view.setOnClickListener(mOnClickListener);
+            }
+            if (mOnItemLongClickListener != null) {
+                view.setOnLongClickListener(mOnLongClickListener);
+            }
+        }
+
+        @Override
+        public void onChildViewDetachedFromWindow(View view) {
+
+        }
+    };
+
+    private RecyclerViewClickSupport(RecyclerView recyclerView) {
+        mRecyclerView = recyclerView;
+        mRecyclerView.setTag(R.id.recycler_view_click_support, this);
+        mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
+    }
+
+    public static RecyclerViewClickSupport addTo(RecyclerView view) {
+        RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
+        if (support == null) {
+            support = new RecyclerViewClickSupport(view);
+        }
+        return support;
+    }
+
+    public static RecyclerViewClickSupport removeFrom(RecyclerView view) {
+        RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
+        if (support != null) {
+            support.detach(view);
+        }
+        return support;
+    }
+
+    public RecyclerViewClickSupport setOnItemClickListener(OnItemClickListener listener) {
+        mOnItemClickListener = listener;
+        return this;
+    }
+
+    public RecyclerViewClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
+        mOnItemLongClickListener = listener;
+        return this;
+    }
+
+    private void detach(RecyclerView view) {
+        view.removeOnChildAttachStateChangeListener(mAttachListener);
+        view.setTag(R.id.recycler_view_click_support, null);
+    }
+
+    public interface OnItemClickListener {
+
+        void onItemClicked(RecyclerView recyclerView, int position, View v);
+    }
+
+    public interface OnItemLongClickListener {
+
+        boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
+    }
+}
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -7087,33 +7087,33 @@ var CharacterEncoding = {
     browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
   }
 };
 
 var IdentityHandler = {
   // No trusted identity information. No site identity icon is shown.
   IDENTITY_MODE_UNKNOWN: "unknown",
 
-  // Minimal SSL CA-signed domain verification. Blue lock icon is shown.
-  IDENTITY_MODE_DOMAIN_VERIFIED: "verified",
-
-  // High-quality identity information. Green lock icon is shown.
+  // Domain-Validation SSL CA-signed domain verification (DV).
   IDENTITY_MODE_IDENTIFIED: "identified",
 
+  // Extended-Validation SSL CA-signed identity information (EV). A more rigorous validation process.
+  IDENTITY_MODE_VERIFIED: "verified",
+
   // The following mixed content modes are only used if "security.mixed_content.block_active_content"
   // is enabled. Our Java frontend coalesces them into one indicator.
 
   // No mixed content information. No mixed content icon is shown.
   MIXED_MODE_UNKNOWN: "unknown",
 
-  // Blocked active mixed content. Shield icon is shown, with a popup option to load content.
-  MIXED_MODE_CONTENT_BLOCKED: "mixed_content_blocked",
-
-  // Loaded active mixed content. Yellow triangle icon is shown.
-  MIXED_MODE_CONTENT_LOADED: "mixed_content_loaded",
+  // Blocked active mixed content.
+  MIXED_MODE_CONTENT_BLOCKED: "blocked",
+
+  // Loaded active mixed content.
+  MIXED_MODE_CONTENT_LOADED: "loaded",
 
   // The following tracking content modes are only used if tracking protection
   // is enabled. Our Java frontend coalesces them into one indicator.
 
   // No tracking content information. No tracking content icon is shown.
   TRACKING_MODE_UNKNOWN: "unknown",
 
   // Blocked active tracking content. Shield icon is shown, with a popup option to load content.
@@ -7159,37 +7159,49 @@ var IdentityHandler = {
     return result;
   },
 
   /**
    * Determines the identity mode corresponding to the icon we show in the urlbar.
    */
   getIdentityMode: function getIdentityMode(aState) {
     if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
-      return this.IDENTITY_MODE_IDENTIFIED;
+      return this.IDENTITY_MODE_VERIFIED;
     }
 
     if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
-      return this.IDENTITY_MODE_DOMAIN_VERIFIED;
+      return this.IDENTITY_MODE_IDENTIFIED;
     }
 
     return this.IDENTITY_MODE_UNKNOWN;
   },
 
-  getMixedMode: function getMixedMode(aState) {
-    if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
-      return this.MIXED_MODE_CONTENT_BLOCKED;
-    }
-
+  getMixedDisplayMode: function getMixedDisplayMode(aState) {
+    if (aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) {
+        return this.MIXED_MODE_CONTENT_LOADED;
+    }
+
+    if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT) {
+        return this.MIXED_MODE_CONTENT_BLOCKED;
+    }
+
+    return this.MIXED_MODE_UNKNOWN;
+  },
+
+  getMixedActiveMode: function getActiveDisplayMode(aState) {
     // Only show an indicator for loaded mixed content if the pref to block it is enabled
     if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
          !Services.prefs.getBoolPref("security.mixed_content.block_active_content")) {
       return this.MIXED_MODE_CONTENT_LOADED;
     }
 
+    if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
+      return this.MIXED_MODE_CONTENT_BLOCKED;
+    }
+
     return this.MIXED_MODE_UNKNOWN;
   },
 
   getTrackingMode: function getTrackingMode(aState, aBrowser) {
     if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
       Telemetry.addData("TRACKING_PROTECTION_SHIELD", 2);
       return this.TRACKING_MODE_CONTENT_BLOCKED;
     }
@@ -7230,34 +7242,38 @@ var IdentityHandler = {
     } catch (ex) {
       // Can sometimes throw if the URL being visited has no host/hostname,
       // e.g. about:blank. The _state for these pages means we won't need these
       // properties anyways, though.
     }
     this._lastLocation = locationObj;
 
     let identityMode = this.getIdentityMode(aState);
-    let mixedMode = this.getMixedMode(aState);
+    let mixedDisplay = this.getMixedDisplayMode(aState);
+    let mixedActive = this.getMixedActiveMode(aState);
     let trackingMode = this.getTrackingMode(aState, aBrowser);
     let result = {
       origin: locationObj.origin,
       mode: {
         identity: identityMode,
-        mixed: mixedMode,
+        mixed_display: mixedDisplay,
+        mixed_active: mixedActive,
         tracking: trackingMode
       }
     };
 
     // Don't show identity data for pages with an unknown identity or if any
     // mixed content is loaded (mixed display content is loaded by default).
     if (identityMode == this.IDENTITY_MODE_UNKNOWN || aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
+      result.secure = false;
       return result;
     }
 
-    result.encrypted = true;
+    result.secure = true;
+
     result.host = this.getEffectiveHost();
 
     let iData = this.getIdentityData();
     result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
 
     // If the cert is identified, then we can populate the results with credentials
     if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
       result.owner = iData.subjectOrg;
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -142,16 +142,17 @@ browser = false
 b2g = false
 [test_set_window_size.py]
 b2g = false
 skip-if = os == "linux" # Bug 1085717
 [test_with_using_context.py]
 
 [test_modal_dialogs.py]
 b2g = false
+skip-if = true # Disabled so bug 959567 can land
 [test_key_actions.py]
 [test_mouse_action.py]
 b2g = false
 [test_teardown_context_preserved.py]
 b2g = false
 [test_file_upload.py]
 b2g = false
 skip-if = os == "win" # http://bugs.python.org/issue14574
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -330,9 +330,13 @@ user_pref("dom.serviceWorkers.periodic-u
 
 // Enable speech synth test service, and disable built in platform services.
 user_pref("media.webspeech.synth.test", true);
 
 // Turn off search suggestions in the location bar so as not to trigger network
 // connections.
 user_pref("browser.urlbar.suggest.searches", false);
 
+// Turn off the location bar search suggestions opt-in.  It interferes with
+// tests that don't expect it to be there.
+user_pref("browser.urlbar.userMadeSearchSuggestionsChoice", true);
+
 user_pref("dom.audiochannel.mutedByDefault", false);
--- a/toolkit/components/aboutperformance/content/aboutPerformance.js
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.js
@@ -424,42 +424,57 @@ let View = {
    * be displayed in the order of `subset`.
    * @param {string} id The id of the DOM element that will contain the items.
    * @param {string} nature The nature of the subset. One of "addons", "webpages" or "system".
    * @param {string} currentMode The current display mode. One of MODE_GLOBAL or MODE_RECENT.
    */
   updateCategory: function(subset, id, nature, deltaT, currentMode) {
     subset = subset.slice().sort(Delta.revCompare);
 
+    let watcherAlerts = null;
+    if (nature == "addons") {
+      watcherAlerts = AddonWatcher.alerts;
+    }
+
     // Grab everything from the DOM before cleaning up
     let eltContainer = this._setupStructure(id);
 
     // An array of `cachedElements` that need to be added
     let toAdd = [];
     for (let delta of subset) {
       let cachedElements = this._grabOrCreateElements(delta, nature);
       toAdd.push(cachedElements);
       cachedElements.eltTitle.textContent = delta.readableName;
       cachedElements.eltName.textContent = `Full name: ${delta.fullName}.`;
       cachedElements.eltLoaded.textContent = `Measure start: ${Math.round(delta.age/1000)} seconds ago.`
       cachedElements.eltProcess.textContent = `Process: ${delta.processId} (${delta.isChildProcess?"child":"parent"}).`;
+      let jankSuffix = "";
+      let cpowSuffix = "";
+      if (watcherAlerts) {
+        let deltaAlerts = watcherAlerts.get(delta.addonId);
+        if (deltaAlerts) {
+          jankSuffix = ` (${deltaAlerts.alerts.longestDuration} alerts)`;
+          cpowSuffix = ` (${deltaAlerts.alerts.totalCPOWTime} alerts)`;
+        }
+      }
+
       let eltImpact = cachedElements.eltImpact;
       if (currentMode == MODE_RECENT) {
         cachedElements.eltRoot.setAttribute("impact", delta.jank.longestDuration + 1);
         if (Delta.compare(delta, Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE) <= 0) {
           eltImpact.textContent = ` currently performs well.`;
         } else if (Delta.compare(delta, Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE)) {
           eltImpact.textContent = ` may currently be slowing down ${BRAND_NAME}.`;
         } else {
           eltImpact.textContent = ` is currently considerably slowing down ${BRAND_NAME}.`;
         }
-        cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.jank.longestDuration + 1}/${delta.jank.durations.length}.`;
+        cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.jank.longestDuration + 1}/${delta.jank.durations.length}${jankSuffix}.`;
         cachedElements.eltCPU.textContent = `CPU usage: ${Math.min(100, Math.ceil(delta.jank.totalUserTime/deltaT))}%.`;
         cachedElements.eltSystem.textContent = `System usage: ${Math.min(100, Math.ceil(delta.jank.totalSystemTime/deltaT))}%.`;
-        cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.cpow.totalCPOWTime/deltaT)}%.`;
+        cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.cpow.totalCPOWTime/deltaT)}%${cpowSuffix}.`;
       } else {
         if (delta.alerts.length == 0) {
           eltImpact.textContent = " has performed well so far.";
           cachedElements.eltFPS.textContent = `Impact on framerate: no impact.`;
         } else {
           let sum = /* medium impact */ delta.alerts[0] + /* high impact */ delta.alerts[1];
           let frequency = sum * 1000 / delta.age;
 
@@ -479,21 +494,21 @@ let View = {
             // At this stage, `sum != 0`
             if (delta.alerts[1] / sum > MIN_PROPORTION_FOR_MAJOR_IMPACT) {
               describeImpact = "When this happens, the slowdown is generally important."
             } else {
               describeImpact = "When this happens, the slowdown is generally noticeable."
             }
 
             eltImpact.textContent = ` ${describeFrequency} ${describeImpact}`;
-            cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.alerts[1]} high-impacts, ${delta.alerts[0]} medium-impact.`;
+            cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.alerts[1]} high-impacts, ${delta.alerts[0]} medium-impact${jankSuffix}.`;
           }
           cachedElements.eltCPU.textContent = `CPU usage: ${Math.min(100, Math.ceil(delta.jank.totalUserTime/delta.age))}% (total ${delta.jank.totalUserTime}ms).`;
           cachedElements.eltSystem.textContent = `System usage: ${Math.min(100, Math.ceil(delta.jank.totalSystemTime/delta.age))}% (total ${delta.jank.totalSystemTime}ms).`;
-          cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.cpow.totalCPOWTime/delta.age)}% (total ${delta.cpow.totalCPOWTime}ms).`;
+          cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.cpow.totalCPOWTime/delta.age)}% (total ${delta.cpow.totalCPOWTime}ms)${cpowSuffix}.`;
         }
       }
     }
     this._insertElements(toAdd, id);
   },
 
   _insertElements: function(elements, id) {
     let eltContainer = document.getElementById(id);
@@ -676,17 +691,17 @@ let View = {
 
       for (let [name, className] of [
         ["eltName", "name"],
         ["eltFPS", "fps"],
         ["eltCPU", "cpu"],
         ["eltSystem", "system"],
         ["eltCPOW", "cpow"],
         ["eltLoaded", "loaded"],
-        ["eltProcess", "process"]
+        ["eltProcess", "process"],
       ]) {
         let elt = document.createElement("li");
         elt.classList.add(className);
         eltDetails.appendChild(elt);
         cachedElements[name] = elt;
       }
     }
 
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -182,17 +182,18 @@ let Bookmarks = Object.freeze({
 
       // If it's a tag, notify OnItemChanged to all bookmarks for this URL.
       let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
       if (isTagging) {
         for (let entry of (yield fetchBookmarksByURL(item))) {
           notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
                                                toPRTime(entry.lastModified),
                                                entry.type, entry._parentId,
-                                               entry.guid, entry.parentGuid ]);
+                                               entry.guid, entry.parentGuid,
+                                               "" ]);
         }
       }
 
       // Remove non-enumerable properties.
       return Object.assign({}, item);
     }.bind(this));
   },
 
@@ -320,35 +321,36 @@ let Bookmarks = Object.freeze({
             item.lastModified != updatedItem.lastModified) {
           notify(observers, "onItemChanged", [ updatedItem._id, "lastModified",
                                                false,
                                                `${toPRTime(updatedItem.lastModified)}`,
                                                toPRTime(updatedItem.lastModified),
                                                updatedItem.type,
                                                updatedItem._parentId,
                                                updatedItem.guid,
-                                               updatedItem.parentGuid ]);
+                                               updatedItem.parentGuid, "" ]);
         }
         if (updateInfo.hasOwnProperty("title")) {
           notify(observers, "onItemChanged", [ updatedItem._id, "title",
                                                false, updatedItem.title,
                                                toPRTime(updatedItem.lastModified),
                                                updatedItem.type,
                                                updatedItem._parentId,
                                                updatedItem.guid,
-                                               updatedItem.parentGuid ]);
+                                               updatedItem.parentGuid, "" ]);
         }
         if (updateInfo.hasOwnProperty("url")) {
           notify(observers, "onItemChanged", [ updatedItem._id, "uri",
                                                false, updatedItem.url.href,
                                                toPRTime(updatedItem.lastModified),
                                                updatedItem.type,
                                                updatedItem._parentId,
                                                updatedItem.guid,
-                                               updatedItem.parentGuid ]);
+                                               updatedItem.parentGuid,
+                                               item.url.href ]);
         }
         // If the item was moved, notify onItemMoved.
         if (item.parentGuid != updatedItem.parentGuid ||
             item.index != updatedItem.index) {
           notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
                                              item.index, updatedItem._parentId,
                                              updatedItem.index, updatedItem.type,
                                              updatedItem.guid, item.parentGuid,
@@ -405,17 +407,18 @@ let Bookmarks = Object.freeze({
                                            item.parentGuid ]);
 
       let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
       if (isUntagging) {
         for (let entry of (yield fetchBookmarksByURL(item))) {
           notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
                                                toPRTime(entry.lastModified),
                                                entry.type, entry._parentId,
-                                               entry.guid, entry.parentGuid ]);
+                                               entry.guid, entry.parentGuid,
+                                               "" ]);
         }
       }
 
       // Remove non-enumerable properties.
       return Object.assign({}, item);
     });
   },
 
@@ -867,17 +870,18 @@ function fetchBookmarkByPosition(info) {
   }));
 }
 
 function fetchBookmarksByURL(info) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByURL",
     Task.async(function*(db) {
 
     let rows = yield db.executeCached(
-      `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+      `/* do not warn (bug no): not worth to add an index */
+       SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
               b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
               b.id AS _id, b.parent AS _parentId,
               (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
               p.parent AS _grandParentId
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        WHERE h.url = :url
@@ -1423,13 +1427,14 @@ Task.async(function* (db, folderGuids) {
                                          item.guid, item.parentGuid ]);
 
     let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
     if (isUntagging) {
       for (let entry of (yield fetchBookmarksByURL(item))) {
         notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
                                              toPRTime(entry.lastModified),
                                              entry.type, entry._parentId,
-                                             entry.guid, entry.parentGuid ]);
+                                             entry.guid, entry.parentGuid,
+                                             "" ]);
       }
     }
   }
 });
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -112,17 +112,18 @@ function* notifyKeywordChange(url, keywo
   let observers = PlacesUtils.bookmarks.getObservers();
   gIgnoreKeywordNotifications = true;
   for (let bookmark of bookmarks) {
     notify(observers, "onItemChanged", [ bookmark.id, "keyword", false,
                                          keyword,
                                          bookmark.lastModified * 1000,
                                          bookmark.type,
                                          bookmark.parentId,
-                                         bookmark.guid, bookmark.parentGuid
+                                         bookmark.guid, bookmark.parentGuid,
+                                         ""
                                        ]);
   }
   gIgnoreKeywordNotifications = false;
 }
 
 /**
  * Serializes the given node in JSON format.
  *
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -31,17 +31,17 @@ const PREF_RESTRICT_SWITCHTAB =     [ "r
 const PREF_RESTRICT_SEARCHES =      [ "restrict.searces",       "$" ];
 const PREF_MATCH_TITLE =            [ "match.title",            "#" ];
 const PREF_MATCH_URL =              [ "match.url",              "@" ];
 
 const PREF_SUGGEST_HISTORY =        [ "suggest.history",        true ];
 const PREF_SUGGEST_BOOKMARK =       [ "suggest.bookmark",       true ];
 const PREF_SUGGEST_OPENPAGE =       [ "suggest.openpage",       true ];
 const PREF_SUGGEST_HISTORY_ONLYTYPED = [ "suggest.history.onlyTyped", false ];
-const PREF_SUGGEST_SEARCHES =       [ "suggest.searches",       true ];
+const PREF_SUGGEST_SEARCHES =       [ "suggest.searches",       false ];
 
 const PREF_MAX_CHARS_FOR_SUGGEST =  [ "maxCharsForSearchSuggestions", 20];
 
 // Match type constants.
 // These indicate what type of search function we should be using.
 const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
 const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
 const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -8,17 +8,17 @@
 interface nsIFile;
 interface nsIURI;
 interface nsITransaction;
 interface nsINavHistoryBatchCallback;
 
 /**
  * Observer for bookmarks changes.
  */
-[scriptable, uuid(8ab925f8-af9b-4837-afa0-ffed507212ce)]
+[scriptable, uuid(cff3efcc-e144-490d-9f23-8b6f6dd09e7f)]
 interface nsINavBookmarkObserver : nsISupports
 {
   /**
    * Notifies that a batch transaction has started.
    * Other notifications will be sent during the batch, but the observer is
    * guaranteed that onEndUpdateBatch() will be called at its completion.
    * During a batch the observer should do its best to reduce the work done to
    * handle notifications, since multiple changes are going to happen in a short
@@ -117,37 +117,51 @@ interface nsINavBookmarkObserver : nsISu
    * @param aItemType
    *        The type of the item to be removed (see TYPE_* constants below).
    * @param aParentId
    *        The id of the folder containing the item.
    * @param aGuid
    *        The unique ID associated with the item.
    * @param aParentGuid
    *        The unique ID associated with the item's parent.
+   * @param aOldValue
+   *        For certain properties, this is set to the new value of the
+   *        property (see the list below).
    *
    * @note List of values that may be associated with properties:
    *       aProperty     | aNewValue
    *       =====================================================================
-   *       cleartime      | Empty string (all visits to this item were removed).
+   *       cleartime     | Empty string (all visits to this item were removed).
    *       title         | The new title.
    *       favicon       | The "moz-anno" URL of the new favicon.
    *       uri           | new URL.
    *       tags          | Empty string (tags for this item changed)
    *       dateAdded     | PRTime (as string) when the item was first added.
    *       lastModified  | PRTime (as string) when the item was last modified.
+   *
+   *       aProperty     | aOldValue
+   *       =====================================================================
+   *       cleartime     | Empty string (currently unused).
+   *       title         | Empty string (currently unused).
+   *       favicon       | Empty string (currently unused).
+   *       uri           | old URL.
+   *       tags          | Empty string (currently unused).
+   *       dateAdded     | Empty string (currently unused).
+   *       lastModified  | Empty string (currently unused).
    */
   void onItemChanged(in long long aItemId,
                      in ACString aProperty,
                      in boolean aIsAnnotationProperty,
                      in AUTF8String aNewValue,
                      in PRTime aLastModified,
                      in unsigned short aItemType,
                      in long long aParentId,
                      in ACString aGuid,
-                     in ACString aParentGuid);
+                     in ACString aParentGuid,
+                     in AUTF8String aOldValue);
 
   /**
    * Notifies that the item was visited.  Can be invoked only for TYPE_BOOKMARK
    * items.
    *
    * @param aItemId
    *        The id of the bookmark that was visited.
    * @param aVisitId
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -546,17 +546,18 @@ nsNavBookmarks::InsertBookmark(int64_t a
                        OnItemChanged(bookmarks[i].id,
                                      NS_LITERAL_CSTRING("tags"),
                                      false,
                                      EmptyCString(),
                                      bookmarks[i].lastModified,
                                      TYPE_BOOKMARK,
                                      bookmarks[i].parentId,
                                      bookmarks[i].guid,
-                                     bookmarks[i].parentGuid));
+                                     bookmarks[i].parentGuid,
+                                     EmptyCString()));
     }
   }
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
@@ -651,17 +652,18 @@ nsNavBookmarks::RemoveItem(int64_t aItem
                        OnItemChanged(bookmarks[i].id,
                                      NS_LITERAL_CSTRING("tags"),
                                      false,
                                      EmptyCString(),
                                      bookmarks[i].lastModified,
                                      TYPE_BOOKMARK,
                                      bookmarks[i].parentId,
                                      bookmarks[i].guid,
-                                     bookmarks[i].parentGuid));
+                                     bookmarks[i].parentGuid,
+                                     EmptyCString()));
     }
 
   }
 
   return NS_OK;
 }
 
 
@@ -1127,17 +1129,18 @@ nsNavBookmarks::RemoveFolderChildren(int
                          OnItemChanged(bookmarks[i].id,
                                        NS_LITERAL_CSTRING("tags"),
                                        false,
                                        EmptyCString(),
                                        bookmarks[i].lastModified,
                                        TYPE_BOOKMARK,
                                        bookmarks[i].parentId,
                                        bookmarks[i].guid,
-                                       bookmarks[i].parentGuid));
+                                       bookmarks[i].parentGuid,
+                                       EmptyCString()));
       }
     }
   }
 
   return NS_OK;
 }
 
 
@@ -1408,17 +1411,18 @@ nsNavBookmarks::SetItemDateAdded(int64_t
                    OnItemChanged(bookmark.id,
                                  NS_LITERAL_CSTRING("dateAdded"),
                                  false,
                                  nsPrintfCString("%lld", bookmark.dateAdded),
                                  bookmark.dateAdded,
                                  bookmark.type,
                                  bookmark.parentId,
                                  bookmark.guid,
-                                 bookmark.parentGuid));
+                                 bookmark.parentGuid,
+                                 EmptyCString()));
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetItemDateAdded(int64_t aItemId, PRTime* _dateAdded)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
@@ -1454,17 +1458,18 @@ nsNavBookmarks::SetItemLastModified(int6
                    OnItemChanged(bookmark.id,
                                  NS_LITERAL_CSTRING("lastModified"),
                                  false,
                                  nsPrintfCString("%lld", bookmark.lastModified),
                                  bookmark.lastModified,
                                  bookmark.type,
                                  bookmark.parentId,
                                  bookmark.guid,
-                                 bookmark.parentGuid));
+                                 bookmark.parentGuid,
+                                 EmptyCString()));
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetItemLastModified(int64_t aItemId, PRTime* _lastModified)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
@@ -1522,17 +1527,18 @@ nsNavBookmarks::SetItemTitle(int64_t aIt
                    OnItemChanged(bookmark.id,
                                  NS_LITERAL_CSTRING("title"),
                                  false,
                                  title,
                                  bookmark.lastModified,
                                  bookmark.type,
                                  bookmark.parentId,
                                  bookmark.guid,
-                                 bookmark.parentGuid));
+                                 bookmark.parentGuid,
+                                 EmptyCString()));
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetItemTitle(int64_t aItemId,
                              nsACString& _title)
 {
@@ -2005,17 +2011,18 @@ nsNavBookmarks::ChangeBookmarkURI(int64_
                    OnItemChanged(bookmark.id,
                                  NS_LITERAL_CSTRING("uri"),
                                  false,
                                  spec,
                                  bookmark.lastModified,
                                  bookmark.type,
                                  bookmark.parentId,
                                  bookmark.guid,
-                                 bookmark.parentGuid));
+                                 bookmark.parentGuid,
+                                 bookmark.url));
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetFolderIdForItem(int64_t aItemId, int64_t* _parentId)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
@@ -2039,16 +2046,17 @@ nsNavBookmarks::GetBookmarkIdsForURITArr
                                            bool aSkipTags)
 {
   NS_ENSURE_ARG(aURI);
 
   // Double ordering covers possible lastModified ties, that could happen when
   // importing, syncing or due to extensions.
   // Note: not using a JOIN is cheaper in this case.
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+    "/* do not warn (bug 1175249) */ "
     "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent "
     "FROM moz_bookmarks b "
     "JOIN moz_bookmarks t on t.id = b.parent "
     "WHERE b.fk = (SELECT id FROM moz_places WHERE url = :page_url) "
     "ORDER BY b.lastModified DESC, b.id DESC "
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
@@ -2082,16 +2090,17 @@ nsNavBookmarks::GetBookmarksForURI(nsIUR
                                    nsTArray<BookmarkData>& aBookmarks)
 {
   NS_ENSURE_ARG(aURI);
 
   // Double ordering covers possible lastModified ties, that could happen when
   // importing, syncing or due to extensions.
   // Note: not using a JOIN is cheaper in this case.
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+    "/* do not warn (bug 1175249) */ "
     "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent "
     "FROM moz_bookmarks b "
     "JOIN moz_bookmarks t on t.id = b.parent "
     "WHERE b.fk = (SELECT id FROM moz_places WHERE url = :page_url) "
     "ORDER BY b.lastModified DESC, b.id DESC "
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
@@ -2297,17 +2306,18 @@ nsNavBookmarks::SetKeywordForBookmark(in
                        OnItemChanged(bookmarks[i].id,
                                      NS_LITERAL_CSTRING("keyword"),
                                      false,
                                      EmptyCString(),
                                      bookmarks[i].lastModified,
                                      TYPE_BOOKMARK,
                                      bookmarks[i].parentId,
                                      bookmarks[i].guid,
-                                     bookmarks[i].parentGuid));
+                                     bookmarks[i].parentGuid,
+                                     EmptyCString()));
     }
 
     return NS_OK;
   }
 
   // A keyword can only be associated to a single URI.  Check if the requested
   // keyword was already associated, in such a case we will need to notify about
   // the change.
@@ -2349,17 +2359,18 @@ nsNavBookmarks::SetKeywordForBookmark(in
                        OnItemChanged(bookmarks[i].id,
                                      NS_LITERAL_CSTRING("keyword"),
                                      false,
                                      EmptyCString(),
                                      bookmarks[i].lastModified,
                                      TYPE_BOOKMARK,
                                      bookmarks[i].parentId,
                                      bookmarks[i].guid,
-                                     bookmarks[i].parentGuid));
+                                     bookmarks[i].parentGuid,
+                                     EmptyCString()));
     }
 
     stmt = mDB->GetStatement(
       "UPDATE moz_keywords SET place_id = :place_id WHERE keyword = :keyword"
     );
     NS_ENSURE_STATE(stmt);
   }
   else {
@@ -2388,17 +2399,18 @@ nsNavBookmarks::SetKeywordForBookmark(in
                      OnItemChanged(bookmarks[i].id,
                                    NS_LITERAL_CSTRING("keyword"),
                                    false,
                                    NS_ConvertUTF16toUTF8(keyword),
                                    bookmarks[i].lastModified,
                                    TYPE_BOOKMARK,
                                    bookmarks[i].parentId,
                                    bookmarks[i].guid,
-                                   bookmarks[i].parentGuid));
+                                   bookmarks[i].parentGuid,
+                                   EmptyCString()));
   }
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetKeywordForBookmark(int64_t aBookmarkId, nsAString& aKeyword)
@@ -2584,17 +2596,18 @@ nsNavBookmarks::NotifyItemChanged(const 
                    OnItemChanged(aData.bookmark.id,
                                  aData.property,
                                  aData.isAnnotation,
                                  aData.newValue,
                                  aData.bookmark.lastModified,
                                  aData.bookmark.type,
                                  aData.bookmark.parentId,
                                  aData.bookmark.guid,
-                                 aData.bookmark.parentGuid));
+                                 aData.bookmark.parentGuid,
+                                 aData.oldValue));
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIObserver
 
 NS_IMETHODIMP
 nsNavBookmarks::Observe(nsISupports *aSubject, const char *aTopic,
                         const char16_t *aData)
@@ -2815,17 +2828,18 @@ nsNavBookmarks::OnItemAnnotationSet(int6
                    OnItemChanged(bookmark.id,
                                  aName,
                                  true,
                                  EmptyCString(),
                                  bookmark.lastModified,
                                  bookmark.type,
                                  bookmark.parentId,
                                  bookmark.guid,
-                                 bookmark.parentGuid));
+                                 bookmark.parentGuid,
+                                 EmptyCString()));
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::OnPageAnnotationRemoved(nsIURI* aPage, const nsACString& aName)
 {
   return NS_OK;
--- a/toolkit/components/places/nsNavBookmarks.h
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -50,16 +50,17 @@ namespace places {
     PRTime time;
   };
 
   struct ItemChangeData {
     BookmarkData bookmark;
     nsCString property;
     bool isAnnotation;
     nsCString newValue;
+    nsCString oldValue;
   };
 
   typedef void (nsNavBookmarks::*ItemVisitMethod)(const ItemVisitData&);
   typedef void (nsNavBookmarks::*ItemChangeMethod)(const ItemChangeData&);
 
   enum BookmarkDate {
     DATE_ADDED = 0
   , LAST_MODIFIED
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -2851,17 +2851,18 @@ NS_IMETHODIMP
 nsNavHistoryQueryResultNode::OnItemChanged(int64_t aItemId,
                                            const nsACString& aProperty,
                                            bool aIsAnnotationProperty,
                                            const nsACString& aNewValue,
                                            PRTime aLastModified,
                                            uint16_t aItemType,
                                            int64_t aParentId,
                                            const nsACString& aGUID,
-                                           const nsACString& aParentGUID)
+                                           const nsACString& aParentGUID,
+                                           const nsACString& aOldValue)
 {
   // History observers should not get OnItemChanged
   // but should get the corresponding history notifications instead.
   // For bookmark queries, "all bookmark" observers should get OnItemChanged.
   // For example, when a title of a bookmark changes, we want that to refresh.
 
   if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS) {
     switch (aItemType) {
@@ -2897,17 +2898,17 @@ nsNavHistoryQueryResultNode::OnItemChang
       NS_ENSURE_SUCCESS(rv, rv);
     }
   }
 
   return nsNavHistoryResultNode::OnItemChanged(aItemId, aProperty,
                                                aIsAnnotationProperty,
                                                aNewValue, aLastModified,
                                                aItemType, aParentId, aGUID,
-                                               aParentGUID);
+                                               aParentGUID, aOldValue);
 }
 
 NS_IMETHODIMP
 nsNavHistoryQueryResultNode::OnItemVisited(int64_t aItemId,
                                            int64_t aVisitId,
                                            PRTime aTime,
                                            uint32_t aTransitionType,
                                            nsIURI* aURI,
@@ -3662,17 +3663,18 @@ NS_IMETHODIMP
 nsNavHistoryResultNode::OnItemChanged(int64_t aItemId,
                                       const nsACString& aProperty,
                                       bool aIsAnnotationProperty,
                                       const nsACString& aNewValue,
                                       PRTime aLastModified,
                                       uint16_t aItemType,
                                       int64_t aParentId,
                                       const nsACString& aGUID,
-                                      const nsACString& aParentGUID)
+                                      const nsACString& aParentGUID,
+                                      const nsACString& aOldValue)
 {
   if (aItemId != mItemId)
     return NS_OK;
 
   mLastModified = aLastModified;
 
   nsNavHistoryResult* result = GetResult();
   NS_ENSURE_STATE(result);
@@ -3752,25 +3754,26 @@ NS_IMETHODIMP
 nsNavHistoryFolderResultNode::OnItemChanged(int64_t aItemId,
                                             const nsACString& aProperty,
                                             bool aIsAnnotationProperty,
                                             const nsACString& aNewValue,
                                             PRTime aLastModified,
                                             uint16_t aItemType,
                                             int64_t aParentId,
                                             const nsACString& aGUID,
-                                            const nsACString&aParentGUID)
+                                            const nsACString& aParentGUID,
+                                            const nsACString& aOldValue)
 {
   RESTART_AND_RETURN_IF_ASYNC_PENDING();
 
   return nsNavHistoryResultNode::OnItemChanged(aItemId, aProperty,
                                                aIsAnnotationProperty,
                                                aNewValue, aLastModified,
                                                aItemType, aParentId, aGUID,
-                                               aParentGUID);
+                                               aParentGUID, aOldValue);
 }
 
 /**
  * Updates visit count and last visit time and refreshes.
  */
 NS_IMETHODIMP
 nsNavHistoryFolderResultNode::OnItemVisited(int64_t aItemId,
                                             int64_t aVisitId,
@@ -4457,21 +4460,23 @@ NS_IMETHODIMP
 nsNavHistoryResult::OnItemChanged(int64_t aItemId,
                                   const nsACString &aProperty,
                                   bool aIsAnnotationProperty,
                                   const nsACString &aNewValue,
                                   PRTime aLastModified,
                                   uint16_t aItemType,
                                   int64_t aParentId,
                                   const nsACString& aGUID,
-                                  const nsACString& aParentGUID)
+                                  const nsACString& aParentGUID,
+                                  const nsACString& aOldValue)
 {
   ENUMERATE_ALL_BOOKMARKS_OBSERVERS(
     OnItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue,
-                  aLastModified, aItemType, aParentId, aGUID, aParentGUID));
+                  aLastModified, aItemType, aParentId, aGUID, aParentGUID,
+                  aOldValue));
 
   // Note: folder-nodes set their own bookmark observer only once they're
   // opened, meaning we cannot optimize this code path for changes done to
   // folder-nodes.
 
   FolderObserverList* list = BookmarkFolderObserversForId(aParentId, false);
   if (!list)
     return NS_OK;
@@ -4485,17 +4490,17 @@ nsNavHistoryResult::OnItemChanged(int64_
       // if ExcludeItems is true we don't update non visible items
       bool excludeItems = (mRootNode->mOptions->ExcludeItems()) ||
                              folder->mOptions->ExcludeItems();
       if (node &&
           (!excludeItems || !(node->IsURI() || node->IsSeparator())) &&
           folder->StartIncrementalUpdate()) {
         node->OnItemChanged(aItemId, aProperty, aIsAnnotationProperty,
                             aNewValue, aLastModified, aItemType, aParentId,
-                            aGUID, aParentGUID);
+                            aGUID, aParentGUID, aOldValue);
       }
     }
   }
 
   // Note: we do NOT call history observers in this case.  This notification is
   // the same as other history notification, except that here we know the item
   // is a bookmark.  History observers will handle the history notification
   // instead.
--- a/toolkit/components/places/nsNavHistoryResult.h
+++ b/toolkit/components/places/nsNavHistoryResult.h
@@ -276,17 +276,18 @@ public:
   NS_IMETHOD OnItemChanged(int64_t aItemId,
                            const nsACString &aProperty,
                            bool aIsAnnotationProperty,
                            const nsACString &aValue,
                            PRTime aNewLastModified,
                            uint16_t aItemType,
                            int64_t aParentId,
                            const nsACString& aGUID,
-                           const nsACString& aParentGUID);
+                           const nsACString& aParentGUID,
+                           const nsACString &aOldValue);
 
 protected:
   virtual ~nsNavHistoryResultNode() {}
 
 public:
 
   nsNavHistoryResult* GetResult();
   nsNavHistoryQueryOptions* GetGeneratingOptions();
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks.js
@@ -38,21 +38,23 @@ let bookmarksObserver = {
     stmt.finalize();
   },
   onItemRemoved: function(id, folder, index, itemType) {
     this._itemRemovedId = id;
     this._itemRemovedFolder = folder;
     this._itemRemovedIndex = index;
   },
   onItemChanged: function(id, property, isAnnotationProperty, value,
-                          lastModified, itemType) {
+                          lastModified, itemType, parentId, guid, parentGuid,
+                          oldValue) {
     this._itemChangedId = id;
     this._itemChangedProperty = property;
     this._itemChanged_isAnnotationProperty = isAnnotationProperty;
     this._itemChangedValue = value;
+    this._itemChangedOldValue = oldValue;
   },
   onItemVisited: function(id, visitID, time) {
     this._itemVisitedId = id;
     this._itemVisitedVistId = visitID;
     this._itemVisitedTime = time;
   },
   onItemMoved: function(id, oldParent, oldIndex, newParent, newIndex,
                         itemType) {
@@ -447,16 +449,17 @@ add_task(function test_bookmarks() {
   do_print("lastModified = " + lastModified);
   do_print("lastModified2 = " + lastModified2);
   do_check_true(is_time_ordered(lastModified, lastModified2));
   do_check_true(is_time_ordered(dateAdded, lastModified2));
 
   do_check_eq(bookmarksObserver._itemChangedId, newId10);
   do_check_eq(bookmarksObserver._itemChangedProperty, "uri");
   do_check_eq(bookmarksObserver._itemChangedValue, "http://foo11.com/");
+  do_check_eq(bookmarksObserver._itemChangedOldValue, "http://foo10.com/");
 
   // test getBookmarkURI
   let newId11 = bs.insertBookmark(testRoot, uri("http://foo11.com/"),
                                   bs.DEFAULT_INDEX, "");
   let bmURI = bs.getBookmarkURI(newId11);
   do_check_eq("http://foo11.com/", bmURI.spec);
 
   // test getBookmarkURI with non-bookmark items
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
@@ -89,68 +89,68 @@ add_task(function* insert_bookmark_tag_n
 
   observer.check([ { name: "onItemAdded",
                      arguments: [ tagId, tagParentId, tag.index, tag.type,
                                   tag.url, null, tag.dateAdded,
                                   tag.guid, tag.parentGuid ] },
                    { name: "onItemChanged",
                      arguments: [ itemId, "tags", false, "",
                                   bm.lastModified, bm.type, parentId,
-                                  bm.guid, bm.parentGuid ] }
+                                  bm.guid, bm.parentGuid, "" ] }
                  ]);
 });
 
 add_task(function* update_bookmark_lastModified() {
   let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: new URL("http://lastmod.example.com/") });
   let observer = expectNotifications();
   bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                             lastModified: new Date() });
   let itemId = yield PlacesUtils.promiseItemId(bm.guid);
   let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
 
   observer.check([ { name: "onItemChanged",
                      arguments: [ itemId, "lastModified", false,
                                   `${bm.lastModified * 1000}`, bm.lastModified,
-                                  bm.type, parentId, bm.guid, bm.parentGuid ] }
+                                  bm.type, parentId, bm.guid, bm.parentGuid, "" ] }
                  ]);
 });
 
 add_task(function* update_bookmark_title() {
   let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: new URL("http://title.example.com/") });
   let observer = expectNotifications();
   bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                             title: "new title" });
   let itemId = yield PlacesUtils.promiseItemId(bm.guid);
   let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
 
   observer.check([ { name: "onItemChanged",
                      arguments: [ itemId, "title", false, bm.title,
                                   bm.lastModified, bm.type, parentId, bm.guid,
-                                  bm.parentGuid ] }
+                                  bm.parentGuid, "" ] }
                  ]);
 });
 
 add_task(function* update_bookmark_uri() {
   let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: new URL("http://url.example.com/") });
   let observer = expectNotifications();
   bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                             url: "http://mozilla.org/" });
   let itemId = yield PlacesUtils.promiseItemId(bm.guid);
   let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
 
   observer.check([ { name: "onItemChanged",
                      arguments: [ itemId, "uri", false, bm.url.href,
                                   bm.lastModified, bm.type, parentId, bm.guid,
-                                  bm.parentGuid ] }
+                                  bm.parentGuid, "http://url.example.com/" ] }
                  ]);
 });
 
 add_task(function* update_move_same_folder() {
   // Ensure there are at least two items in place (others test do so for us,
   // but we don't have to depend on that).
   let sep = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                  parentGuid: PlacesUtils.bookmarks.unfiledGuid });
@@ -259,17 +259,17 @@ add_task(function* remove_bookmark_tag_n
   let removed = yield PlacesUtils.bookmarks.remove(tag.guid);
 
   observer.check([ { name: "onItemRemoved",
                      arguments: [ tagId, tagParentId, tag.index, tag.type,
                                   tag.url, tag.guid, tag.parentGuid ] },
                    { name: "onItemChanged",
                      arguments: [ itemId, "tags", false, "",
                                   bm.lastModified, bm.type, parentId,
-                                  bm.guid, bm.parentGuid ] }
+                                  bm.guid, bm.parentGuid, "" ] }
                  ]);
 });
 
 add_task(function* remove_folder_notification() {
   let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                      parentGuid: PlacesUtils.bookmarks.unfiledGuid });
   let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid);
   let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid);
--- a/toolkit/components/places/tests/bookmarks/test_keywords.js
+++ b/toolkit/components/places/tests/bookmarks/test_keywords.js
@@ -39,17 +39,17 @@ function expectNotifications() {
     get(target, name) {
       if (name == "check") {
         PlacesUtils.bookmarks.removeObserver(observer);
         return expectedNotifications =>
           Assert.deepEqual(notifications, expectedNotifications);
       }
 
       if (name.startsWith("onItemChanged")) {
-        return (id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid) => {
+        return (id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid, oldVal) => {
           if (prop != "keyword")
             return;
           let args = Array.from(arguments, arg => {
             if (arg && arg instanceof Ci.nsIURI)
               return new URL(arg.spec);
             if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000)
               return new Date(parseInt(arg/1000));
             return arg;
@@ -92,17 +92,17 @@ add_task(function test_addBookmarkAndKey
                                          "test");
 
   PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
   let bookmark = yield PlacesUtils.bookmarks.fetch({ url: URI1 });
   observer.check([ { name: "onItemChanged",
                      arguments: [ itemId, "keyword", false, "keyword",
                                   bookmark.lastModified, bookmark.type,
                                   (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
-                                  bookmark.guid, bookmark.parentGuid ] }
+                                  bookmark.guid, bookmark.parentGuid, "" ] }
                  ]);
   yield PlacesTestUtils.promiseAsyncUpdates();
 
   check_keyword(URI1, "keyword");
   Assert.equal((yield foreign_count(URI1)), fc + 2); // + 1 bookmark + 1 keyword
 
   yield PlacesTestUtils.promiseAsyncUpdates();
   yield check_orphans();
@@ -143,22 +143,22 @@ add_task(function test_sameKeywordDiffer
 
   let bookmark1 = yield PlacesUtils.bookmarks.fetch({ url: URI1 });
   let bookmark2 = yield PlacesUtils.bookmarks.fetch({ url: URI2 });
   observer.check([ { name: "onItemChanged",
                      arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
                                   "keyword", false, "",
                                   bookmark1.lastModified, bookmark1.type,
                                   (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
-                                  bookmark1.guid, bookmark1.parentGuid ] },
+                                  bookmark1.guid, bookmark1.parentGuid, "" ] },
                     { name: "onItemChanged",
                      arguments: [ itemId, "keyword", false, "keyword",
                                   bookmark2.lastModified, bookmark2.type,
                                   (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
-                                  bookmark2.guid, bookmark2.parentGuid ] }
+                                  bookmark2.guid, bookmark2.parentGuid, "" ] }
                  ]);
   yield PlacesTestUtils.promiseAsyncUpdates();
 
   // The keyword should have been "moved" to the new URI.
   check_keyword(URI1, null);
   Assert.equal((yield foreign_count(URI1)), fc1 - 1); // - 1 keyword
   check_keyword(URI2, "keyword");
   Assert.equal((yield foreign_count(URI2)), fc2 + 2); // + 1 bookmark + 1 keyword
@@ -182,23 +182,23 @@ add_task(function test_sameURIDifferentK
 
   let bookmarks = [];
   yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark));
   observer.check([ { name: "onItemChanged",
                      arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)),
                                   "keyword", false, "keyword2",
                                   bookmarks[0].lastModified, bookmarks[0].type,
                                   (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)),
-                                  bookmarks[0].guid, bookmarks[0].parentGuid ] },
+                                  bookmarks[0].guid, bookmarks[0].parentGuid, "" ] },
                     { name: "onItemChanged",
                      arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)),
                                   "keyword", false, "keyword2",
                                   bookmarks[1].lastModified, bookmarks[1].type,
                                   (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)),
-                                  bookmarks[1].guid, bookmarks[1].parentGuid ] }
+                                  bookmarks[1].guid, bookmarks[1].parentGuid, "" ] }
                  ]);
   yield PlacesTestUtils.promiseAsyncUpdates();
 
   check_keyword(URI2, "keyword2");
   Assert.equal((yield foreign_count(URI2)), fc + 2); // + 1 bookmark + 1 keyword
 
   yield PlacesTestUtils.promiseAsyncUpdates();
   check_orphans();
@@ -238,29 +238,29 @@ add_task(function test_unsetKeyword() {
   let bookmarks = [];
   yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark));
   do_print(bookmarks.length);
   observer.check([ { name: "onItemChanged",
                      arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)),
                                   "keyword", false, "",
                                   bookmarks[0].lastModified, bookmarks[0].type,
                                   (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)),
-                                  bookmarks[0].guid, bookmarks[0].parentGuid ] },
+                                  bookmarks[0].guid, bookmarks[0].parentGuid, "" ] },
                     { name: "onItemChanged",
                      arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)),
                                   "keyword", false, "",
                                   bookmarks[1].lastModified, bookmarks[1].type,
                                   (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)),
-                                  bookmarks[1].guid, bookmarks[1].parentGuid ] },
+                                  bookmarks[1].guid, bookmarks[1].parentGuid, "" ] },
                     { name: "onItemChanged",
                      arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[2].guid)),
                                   "keyword", false, "",
                                   bookmarks[2].lastModified, bookmarks[2].type,
                                   (yield PlacesUtils.promiseItemId(bookmarks[2].parentGuid)),
-                                  bookmarks[2].guid, bookmarks[2].parentGuid ] }
+                                  bookmarks[2].guid, bookmarks[2].parentGuid, "" ] }
                  ]);
 
   check_keyword(URI1, null);
   check_keyword(URI2, null);
   Assert.equal((yield foreign_count(URI2)), fc - 1); // + 1 bookmark - 2 keyword
 
   yield PlacesTestUtils.promiseAsyncUpdates();
   check_orphans();
@@ -281,17 +281,17 @@ add_task(function test_addRemoveBookmark
   let parentId = yield PlacesUtils.promiseItemId(bookmark.parentGuid);
   PlacesUtils.bookmarks.removeItem(itemId);
 
   observer.check([ { name: "onItemChanged",
                      arguments: [ itemId,
                                   "keyword", false, "keyword",
                                   bookmark.lastModified, bookmark.type,
                                   parentId,
-                                  bookmark.guid, bookmark.parentGuid ] }
+                                  bookmark.guid, bookmark.parentGuid, "" ] }
                  ]);
 
   check_keyword(URI3, null);
   // Don't check the foreign count since the process is async.
   // The new test_keywords.js in unit is checking this though.
 
   yield PlacesTestUtils.promiseAsyncUpdates();
   check_orphans();
--- a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
+++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
@@ -16,30 +16,37 @@ let gBookmarksObserver = {
     }
 
     if (this.expected.length == 0) {
       run_next_test();
     }
   },
 
   // nsINavBookmarkObserver
-  onBeginUpdateBatch: function onBeginUpdateBatch()
-    this.validate(arguments.callee.name, arguments),
-  onEndUpdateBatch: function onEndUpdateBatch()
-    this.validate(arguments.callee.name, arguments),
-  onItemAdded: function onItemAdded()
-    this.validate(arguments.callee.name, arguments),
-  onItemRemoved: function onItemRemoved()
-    this.validate(arguments.callee.name, arguments),
-  onItemChanged: function onItemChanged()
-    this.validate(arguments.callee.name, arguments),
-  onItemVisited: function onItemVisited()
-    this.validate(arguments.callee.name, arguments),
-  onItemMoved: function onItemMoved()
-    this.validate(arguments.callee.name, arguments),
+  onBeginUpdateBatch() {
+    return this.validate("onBeginUpdateBatch", arguments);
+  },
+  onEndUpdateBatch() {
+    return this.validate("onEndUpdateBatch", arguments);
+  },
+  onItemAdded() {
+    return this.validate("onItemAdded", arguments);
+  },
+  onItemRemoved() {
+    return this.validate("onItemRemoved", arguments);
+  },
+  onItemChanged() {
+    return this.validate("onItemChanged", arguments);
+  },
+  onItemVisited() {
+    return this.validate("onItemVisited", arguments);
+  },
+  onItemMoved() {
+    return this.validate("onItemMoved", arguments);
+  },
 
   // nsISupports
   QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]),
 }
 
 add_test(function batch() {
   gBookmarksObserver.expected = [
     { name: "onBeginUpdateBatch",
@@ -127,16 +134,17 @@ add_test(function onItemChanged_title_bo
         { name: "property", check: function (v) v === "title" },
         { name: "isAnno", check: function (v) v === false },
         { name: "newValue", check: function (v) v === TITLE },
         { name: "lastModified", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
         { name: "parentId", check: function (v) v === PlacesUtils.unfiledBookmarksFolderId },
         { name: "guid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
         { name: "parentGuid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+        { name: "oldValue", check: function (v) typeof(v) == "string" },
       ] },
   ];
   PlacesUtils.bookmarks.setItemTitle(id, TITLE);
 });
 
 add_test(function onItemChanged_tags_bookmark() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
   let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
@@ -173,16 +181,17 @@ add_test(function onItemChanged_tags_boo
         { name: "property", check: function (v) v === "tags" },
         { name: "isAnno", check: function (v) v === false },
         { name: "newValue", check: function (v) v === "" },
         { name: "lastModified", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
         { name: "parentId", check: function (v) v === PlacesUtils.unfiledBookmarksFolderId },
         { name: "guid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
         { name: "parentGuid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+        { name: "oldValue", check: function (v) typeof(v) == "string" },
       ] },
     { name: "onItemRemoved", // This is the tag.
       args: [
         { name: "itemId", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "parentId", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "index", check: function (v) v === 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
         { name: "uri", check: function (v) v instanceof Ci.nsIURI && v.equals(uri) },
@@ -205,16 +214,17 @@ add_test(function onItemChanged_tags_boo
         { name: "property", check: function (v) v === "tags" },
         { name: "isAnno", check: function (v) v === false },
         { name: "newValue", check: function (v) v === "" },
         { name: "lastModified", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
         { name: "parentId", check: function (v) v === PlacesUtils.unfiledBookmarksFolderId },
         { name: "guid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
         { name: "parentGuid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+        { name: "oldValue", check: function (v) typeof(v) == "string" },
       ] },
   ];
   PlacesUtils.tagging.tagURI(uri, [TAG]);
   PlacesUtils.tagging.untagURI(uri, [TAG]);
 });
 
 add_test(function onItemMoved_bookmark() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
@@ -278,16 +288,17 @@ add_test(function onItemRemoved_bookmark
         { name: "property", check: function (v) v === "" },
         { name: "isAnno", check: function (v) v === true },
         { name: "newValue", check: function (v) v === "" },
         { name: "lastModified", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
         { name: "parentId", check: function (v) v === PlacesUtils.unfiledBookmarksFolderId },
         { name: "guid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
         { name: "parentGuid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+        { name: "oldValue", check: function (v) typeof(v) == "string" },
       ] },
     { name: "onItemRemoved",
       args: [
         { name: "itemId", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "parentId", check: function (v) v === PlacesUtils.unfiledBookmarksFolderId },
         { name: "index", check: function (v) v === 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
         { name: "uri", check: function (v) v instanceof Ci.nsIURI && v.equals(uri) },
@@ -307,16 +318,17 @@ add_test(function onItemRemoved_separato
         { name: "property", check: function (v) v === "" },
         { name: "isAnno", check: function (v) v === true },
         { name: "newValue", check: function (v) v === "" },
         { name: "lastModified", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
         { name: "parentId", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "guid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
         { name: "parentGuid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+        { name: "oldValue", check: function (v) typeof(v) == "string" },
       ] },
     { name: "onItemRemoved",
       args: [
         { name: "itemId", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "parentId", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "index", check: function (v) v === 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
         { name: "uri", check: function (v) v === null },
@@ -337,16 +349,17 @@ add_test(function onItemRemoved_folder()
         { name: "property", check: function (v) v === "" },
         { name: "isAnno", check: function (v) v === true },
         { name: "newValue", check: function (v) v === "" },
         { name: "lastModified", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_FOLDER },
         { name: "parentId", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "guid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
         { name: "parentGuid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+        { name: "oldValue", check: function (v) typeof(v) == "string" },
       ] },
     { name: "onItemRemoved",
       args: [
         { name: "itemId", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "parentId", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "index", check: function (v) v === 0 },
         { name: "itemType", check: function (v) v === PlacesUtils.bookmarks.TYPE_FOLDER },
         { name: "uri", check: function (v) v === null },
--- a/toolkit/components/places/tests/unifiedcomplete/test_enabled.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_enabled.js
@@ -54,11 +54,15 @@ add_task(function* test_sync_enabled() {
   }
   Assert.equal(Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"), true);
 
   // Disable autocoplete again, then re-enable it and check suggest prefs
   // have been reset.
   Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false);
   Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", true);
   for (let type of types.filter(t => t != "history")) {
-    Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), true);
+    if (type == "searches") {
+      Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false);
+    } else {
+      Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), true);
+    }
   }
 });
--- a/toolkit/components/places/tests/unit/test_keywords.js
+++ b/toolkit/components/places/tests/unit/test_keywords.js
@@ -180,31 +180,31 @@ add_task(function* test_addBookmarkAndKe
   let observer = expectBookmarkNotifications();
   yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
 
   observer.check([{ name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
                                  "keyword", false, "keyword",
                                  bookmark.lastModified, bookmark.type,
                                  (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
-                                 bookmark.guid, bookmark.parentGuid ] } ]);
+                                 bookmark.guid, bookmark.parentGuid, "" ] } ]);
 
   yield check_keyword(true, "http://example.com/", "keyword");
   Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword
 
   // Now remove the keyword.
   observer = expectBookmarkNotifications();
   yield PlacesUtils.keywords.remove("keyword");
 
   observer.check([{ name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
                                  "keyword", false, "",
                                  bookmark.lastModified, bookmark.type,
                                  (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
-                                 bookmark.guid, bookmark.parentGuid ] } ]);
+                                 bookmark.guid, bookmark.parentGuid, "" ] } ]);
 
   yield check_keyword(false, "http://example.com/", "keyword");
   Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // -1 keyword
 
   // Add again the keyword, then remove the bookmark.
   yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
 
   observer = expectBookmarkNotifications();
@@ -307,38 +307,38 @@ add_task(function* test_sameKeywordDiffe
   let observer = expectBookmarkNotifications();
   yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example2.com/" });
 
   observer.check([{ name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
                                  "keyword", false, "",
                                  bookmark1.lastModified, bookmark1.type,
                                  (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
-                                 bookmark1.guid, bookmark1.parentGuid ] },
+                                 bookmark1.guid, bookmark1.parentGuid, "" ] },
                   { name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
                                  "keyword", false, "keyword",
                                  bookmark2.lastModified, bookmark2.type,
                                  (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
-                                 bookmark2.guid, bookmark2.parentGuid ] } ]);
+                                 bookmark2.guid, bookmark2.parentGuid, "" ] } ]);
 
   yield check_keyword(false, "http://example1.com/", "keyword");
   Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1); // -1 keyword
   yield check_keyword(true, "http://example2.com/", "keyword");
   Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 keyword
 
   // Now remove the keyword.
   observer = expectBookmarkNotifications();
   yield PlacesUtils.keywords.remove("keyword");
   observer.check([{ name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
                                  "keyword", false, "",
                                  bookmark2.lastModified, bookmark2.type,
                                  (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
-                                 bookmark2.guid, bookmark2.parentGuid ] } ]);
+                                 bookmark2.guid, bookmark2.parentGuid, "" ] } ]);
 
   yield check_keyword(false, "http://example1.com/", "keyword");
   yield check_keyword(false, "http://example2.com/", "keyword");
   Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1);
   Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // -1 keyword
 
   yield PlacesUtils.bookmarks.remove(bookmark1);
   yield PlacesUtils.bookmarks.remove(bookmark2);
@@ -360,29 +360,29 @@ add_task(function* test_sameURIDifferent
   yield check_keyword(true, "http://example.com/", "keyword");
   Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword
 
   observer.check([{ name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
                                  "keyword", false, "keyword",
                                  bookmark.lastModified, bookmark.type,
                                  (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
-                                 bookmark.guid, bookmark.parentGuid ] } ]);
+                                 bookmark.guid, bookmark.parentGuid, "" ] } ]);
 
   observer = expectBookmarkNotifications();
   yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" });
   yield check_keyword(true, "http://example.com/", "keyword");
   yield check_keyword(true, "http://example.com/", "keyword2");
   Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +1 keyword
   observer.check([{ name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
                                  "keyword", false, "keyword2",
                                  bookmark.lastModified, bookmark.type,
                                  (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
-                                 bookmark.guid, bookmark.parentGuid ] } ]);
+                                 bookmark.guid, bookmark.parentGuid, "" ] } ]);
 
   // Add a third keyword.
   yield PlacesUtils.keywords.insert({ keyword: "keyword3", url: "http://example.com/" });
   yield check_keyword(true, "http://example.com/", "keyword");
   yield check_keyword(true, "http://example.com/", "keyword2");
   yield check_keyword(true, "http://example.com/", "keyword3");
   Assert.equal((yield foreign_count("http://example.com/")), fc + 4); // +1 keyword
 
@@ -392,17 +392,17 @@ add_task(function* test_sameURIDifferent
   yield check_keyword(false, "http://example.com/", "keyword");
   yield check_keyword(true, "http://example.com/", "keyword2");
   yield check_keyword(true, "http://example.com/", "keyword3");
   observer.check([{ name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
                                  "keyword", false, "",
                                  bookmark.lastModified, bookmark.type,
                                  (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
-                                 bookmark.guid, bookmark.parentGuid ] } ]);
+                                 bookmark.guid, bookmark.parentGuid, "" ] } ]);
   Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // -1 keyword
 
   // Now remove the bookmark.
   yield PlacesUtils.bookmarks.remove(bookmark);
   while ((yield foreign_count("http://example.com/")));
   yield check_keyword(false, "http://example.com/", "keyword");
   yield check_keyword(false, "http://example.com/", "keyword2");
   yield check_keyword(false, "http://example.com/", "keyword3");
@@ -424,40 +424,40 @@ add_task(function* test_deleteKeywordMul
 
   yield check_keyword(true, "http://example.com/", "keyword");
   Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +2 bookmark +1 keyword
   observer.check([{ name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
                                  "keyword", false, "keyword",
                                  bookmark2.lastModified, bookmark2.type,
                                  (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
-                                 bookmark2.guid, bookmark2.parentGuid ] },
+                                 bookmark2.guid, bookmark2.parentGuid, "" ] },
                   { name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
                                  "keyword", false, "keyword",
                                  bookmark1.lastModified, bookmark1.type,
                                  (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
-                                 bookmark1.guid, bookmark1.parentGuid ] } ]);
+                                 bookmark1.guid, bookmark1.parentGuid, "" ] } ]);
 
   observer = expectBookmarkNotifications();
   yield PlacesUtils.keywords.remove("keyword");
   yield check_keyword(false, "http://example.com/", "keyword");
   Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // -1 keyword
   observer.check([{ name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
                                  "keyword", false, "",
                                  bookmark2.lastModified, bookmark2.type,
                                  (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
-                                 bookmark2.guid, bookmark2.parentGuid ] },
+                                 bookmark2.guid, bookmark2.parentGuid, "" ] },
                   { name: "onItemChanged",
                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
                                  "keyword", false, "",
                                  bookmark1.lastModified, bookmark1.type,
                                  (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
-                                 bookmark1.guid, bookmark1.parentGuid ] } ]);
+                                 bookmark1.guid, bookmark1.parentGuid, "" ] } ]);
 
   // Now remove the bookmarks.
   yield PlacesUtils.bookmarks.remove(bookmark1);
   yield PlacesUtils.bookmarks.remove(bookmark2);
   Assert.equal((yield foreign_count("http://example.com/")), fc); // -2 bookmarks
 
   check_no_orphans();
 });
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8454,16 +8454,36 @@
     "n_buckets": 5,
     "description": "The number of Sync 1.5 'complete upgrade/migration' notifications offered by Android Sync."
   },
   "FENNEC_SYNC11_MIGRATIONS_COMPLETED": {
     "expires_in_version": "45",
     "kind": "count",
     "description": "The number of Sync 1.5 migrations completed by Android Sync."
   },
+  "FENNEC_SYNC_NUMBER_OF_SYNCS_STARTED": {
+    "expires_in_version": "45",
+    "kind": "count",
+    "description": "Counts the number of times that a sync has started."
+  },
+  "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED": {
+    "expires_in_version": "45",
+    "kind": "count",
+    "description": "Counts the number of times that a sync has completed with no errors."
+  },
+  "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED": {
+    "expires_in_version": "45",
+    "kind": "count",
+    "description": "Counts the number of times that a sync has failed with errors."
+  },
+  "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED_BACKOFF": {
+    "expires_in_version": "45",
+    "kind": "count",
+    "description": "Counts the number of times that a sync has failed because of trying to sync before server backoff interval has passed."
+  },
   "SLOW_SCRIPT_NOTICE_COUNT": {
     "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
     "expires_in_version": "never",
     "kind": "count",
     "description": "Count slow script notices"
   },
   "PLUGIN_HANG_NOTICE_COUNT": {
     "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1073,35 +1073,27 @@ extends="chrome://global/content/binding
           this._invalidate();
           ]]>
         </body>
       </method>
 
       <method name="_invalidate">
         <body>
           <![CDATA[
-          // To get a fixed height for the popup, instead of the default
-          // behavior that grows and shrinks it on result change, a consumer
-          // can set the height attribute. In such a case, instead of adjusting
-          // the richlistbox height, we just need to collapse unused items.
-          if (!this.hasAttribute("height")) {
-            // collapsed if no matches
-            this.richlistbox.collapsed = (this._matchCount == 0);
+          // collapsed if no matches
+          this.richlistbox.collapsed = (this._matchCount == 0);
 
-            // Update the richlistbox height.
-            if (this._adjustHeightTimeout) {
-              clearTimeout(this._adjustHeightTimeout);
-            }
-            if (this._shrinkTimeout) {
-              clearTimeout(this._shrinkTimeout);
-            }
-            this._adjustHeightTimeout = setTimeout(() => this.adjustHeight(), 0);
-          } else {
-            this._collapseUnusedItems();
+          // Update the richlistbox height.
+          if (this._adjustHeightTimeout) {
+            clearTimeout(this._adjustHeightTimeout);
           }
+          if (this._shrinkTimeout) {
+            clearTimeout(this._shrinkTimeout);
+          }
+          this._adjustHeightTimeout = setTimeout(() => this.adjustHeight(), 0);
 
           this._currentIndex = 0;
           if (this._appendResultTimeout) {
             clearTimeout(this._appendResultTimeout);
           }
           this._appendCurrentResult();
         ]]>
         </body>
@@ -1138,48 +1130,59 @@ extends="chrome://global/content/binding
 
       <method name="adjustHeight">
         <body>
           <![CDATA[
           // Figure out how many rows to show
           let rows = this.richlistbox.childNodes;
           let numRows = Math.min(this._matchCount, this.maxRows, rows.length);
 
+          this.removeAttribute("height");
+
           // Default the height to 0 if we have no rows to show
           let height = 0;
           if (numRows) {
             if (!this._rowHeight) {
               let firstRowRect = rows[0].getBoundingClientRect();
               this._rowHeight = firstRowRect.height;
-              this._rlbAnimated = !!window.getComputedStyle(this.richlistbox).transitionProperty;
+
+              let transition =
+                window.getComputedStyle(this.richlistbox).transitionProperty;
+              this._rlbAnimated = transition && transition != "none";
 
               // Set a fixed max-height to avoid flicker when growing the panel.
               this.richlistbox.style.maxHeight = (this._rowHeight * this.maxRows) + "px";
             }
 
             // Calculate the height to have the first row to last row shown
             height = this._rowHeight * numRows;
           }
 
+          let animate = this._rlbAnimated &&
+                        this.getAttribute("dontanimate") != "true";
           let currentHeight = this.richlistbox.getBoundingClientRect().height;
           if (height > currentHeight) {
             // Grow immediately.
-            this.richlistbox.style.height = height + "px";
+            if (animate) {
+              this.richlistbox.removeAttribute("height");
+              this.richlistbox.style.height = height + "px";
+            } else {
+              this.richlistbox.style.removeProperty("height");
+              this.richlistbox.height = height;
+            }
           } else {
             // Delay shrinking to avoid flicker.
             this._shrinkTimeout = setTimeout(() => {
-              this.richlistbox.style.height = height + "px";
-              if (this._rlbAnimated) {
-                let onTransitionEnd = () => {
-                  this.removeEventListener("transitionend", onTransitionEnd, true);
-                  this._collapseUnusedItems();
-                };
-                this.addEventListener("transitionend", onTransitionEnd, true);
+              this._collapseUnusedItems();
+              if (animate) {
+                this.richlistbox.removeAttribute("height");
+                this.richlistbox.style.height = height + "px";
               } else {
-                this._collapseUnusedItems();
+                this.richlistbox.style.removeProperty("height");
+                this.richlistbox.height = height;
               }
             }, this.mInput.shrinkDelay);
           }
           ]]>
         </body>
       </method>
 
       <method name="_getImageURLForResolution">