Bug 1564594 - Add an Enhanced Search webcompat site patch r=nalexander,denschub
authorThomas Wisniewski <twisniewski@mozilla.com>
Wed, 21 Aug 2019 20:32:27 +0000
changeset 553187 2fc4f931f041ebab1d02fe3433fdaba6f9a49fdf
parent 553186 a54dd9741b8db58e2a9de5a90e79125d0e7fb52d
child 553188 0b545428a2a7dc740773d1548274ded456b15fcb
push id2165
push userffxbld-merge
push dateMon, 14 Oct 2019 16:30:58 +0000
treeherdermozilla-release@0eae18af659f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander, denschub
bugs1564594
milestone70.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
Bug 1564594 - Add an Enhanced Search webcompat site patch r=nalexander,denschub This patch ultimately overrides the user-agent which Fennec users send to Google Search pages, choosing a phone- or tablet-specific UA as appropriate. It involves adding four new metadata keys to the webcompat addon's metadata for user-agent overrides: blocks, permanentPref, experiment, telemetryKey: - "blocks" specifies URLs for which any requests should be aborted. This allows us to block the Google service worker for now, as it has caused "content corrupted" issues in the past with other enhanced search addons (see https://github.com/wisniewskit/google-search-fixer/issues/1). - "permanentPref" specifies an about:config preference, which dictates whether the injection is used. Users may set this to `false` to disable the injection outright, and permanently; `true` or `undefined` values allow the injection to function normally. - "experiment" specifies the name of the experiment this feature is optionally gated behind. Only a Fennec-specific implementation is provided in this patch. The implementation simply queries Switchboard to determine if the experiment is active. - "telemetryKey" specifies which telemetry key should be flipped to "true" when this injection runs. We will use this to note whether enhanced search has actually been enabled for this profile. Only a Fennec-specific implementation is given in this patch, which actually just sets a Fennec Shared Preference, which Fennec's core telemetry ping later reads in Java to know what the ping should contain. Differential Revision: https://phabricator.services.mozilla.com/D41074
browser/extensions/webcompat/data/ua_overrides.js
browser/extensions/webcompat/experiment-apis/experiments.js
browser/extensions/webcompat/experiment-apis/experiments.json
browser/extensions/webcompat/experiment-apis/sharedPreferences.js
browser/extensions/webcompat/experiment-apis/sharedPreferences.json
browser/extensions/webcompat/lib/ua_overrides.js
browser/extensions/webcompat/manifest.json
browser/extensions/webcompat/moz.build
mobile/android/base/java/org/mozilla/gecko/Experiments.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
mobile/android/extensions/webcompat/data/ua_overrides.js
mobile/android/extensions/webcompat/experiment-apis/experiments.js
mobile/android/extensions/webcompat/experiment-apis/experiments.json
mobile/android/extensions/webcompat/experiment-apis/sharedPreferences.js
mobile/android/extensions/webcompat/experiment-apis/sharedPreferences.json
mobile/android/extensions/webcompat/lib/ua_overrides.js
mobile/android/extensions/webcompat/manifest.json
mobile/android/extensions/webcompat/moz.build
--- a/browser/extensions/webcompat/data/ua_overrides.js
+++ b/browser/extensions/webcompat/data/ua_overrides.js
@@ -13,29 +13,50 @@
  * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
  */
 const AVAILABLE_UA_OVERRIDES = [
   {
     id: "testbed-override",
     platform: "all",
     domain: "webcompat-addon-testbed.herokuapp.com",
     bug: "0000000",
-    hidden: true,
     config: {
+      hidden: true,
       matches: ["*://webcompat-addon-testbed.herokuapp.com/*"],
       uaTransformer: originalUA => {
         return (
           UAHelpers.getPrefix(originalUA) +
           " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36 for WebCompat"
         );
       },
     },
   },
   {
     /*
+     * Bug 1564594 - Create UA override for Enhanced Search on Firefox Android
+     *
+     * Enables the Chrome Google Search experience for Fennec users.
+     */
+    id: "bug1564594",
+    platform: "android",
+    domain: "Enhanced Search",
+    bug: "1567945",
+    config: {
+      matches: /^https?:\/\/(www|encrypted|maps|news|images)\.google(\.com?)?\.([a-z])+?($|\/)/,
+      blocks: /^https?:\/\/www\.google(\.com?)?\.([a-z])+?\/serviceworker($|\/)/,
+      permanentPref: "enable_enhanced_search",
+      telemetryKey: "enhancedSearchUsed",
+      experiment: "enhanced-search-experiment",
+      uaTransformer: originalUA => {
+        return UAHelpers.getDeviceAppropriateChromeUA();
+      },
+    },
+  },
+  {
+    /*
      * Bug 1563839 - rolb.santanderbank.com - Build UA override
      * WebCompat issue #33462 - https://webcompat.com/issues/33462
      *
      * santanderbank expects UA to have 'like Gecko', otherwise it runs
      * xmlDoc.onload whose support has been dropped. It results in missing labels in forms
      * and some other issues.  Adding 'like Gecko' fixes those issues.
      */
     id: "bug1563839",
@@ -314,14 +335,31 @@ const AVAILABLE_UA_OVERRIDES = [
       uaTransformer: originalUA => {
         return originalUA.replace(/Firefox.+$/, "");
       },
     },
   },
 ];
 
 const UAHelpers = {
+  getDeviceAppropriateChromeUA() {
+    if (!UAHelpers._deviceAppropriateChromeUA) {
+      const RunningFirefoxVersion = (navigator.userAgent.match(
+        /Firefox\/([0-9.]+)/
+      ) || ["", "58.0"])[1];
+      const RunningAndroidVersion =
+        navigator.userAgent.match(/Android\/[0-9.]+/) || "Android 6.0";
+      const ChromeVersionToMimic = "76.0.3809.111";
+      const ChromePhoneUA = `Mozilla/5.0 (Linux; ${RunningAndroidVersion}; Nexus 5 Build/MRA58N) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ChromeVersionToMimic} Mobile Safari/537.36`;
+      const ChromeTabletUA = `Mozilla/5.0 (Linux; ${RunningAndroidVersion}; Nexus 7 Build/JSS15Q) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ChromeVersionToMimic} Safari/537.36`;
+      const IsPhone = navigator.userAgent.includes("Mobile");
+      UAHelpers._deviceAppropriateChromeUA = IsPhone
+        ? ChromePhoneUA
+        : ChromeTabletUA;
+    }
+    return UAHelpers._deviceAppropriateChromeUA;
+  },
   getPrefix(originalUA) {
     return originalUA.substr(0, originalUA.indexOf(")") + 1);
   },
 };
 
 module.exports = AVAILABLE_UA_OVERRIDES;
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/experiments.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, Services, XPCOMUtils */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  EventDispatcher: "resource://gre/modules/Messaging.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+});
+
+this.experiments = class extends ExtensionAPI {
+  getAPI(context) {
+    function promiseActiveExperiments() {
+      return EventDispatcher.instance.sendRequestForResult({
+        type: "Experiments:GetActive",
+      });
+    }
+    return {
+      experiments: {
+        async isActive(name) {
+          if (!Services.androidBridge || !Services.androidBridge.isFennec) {
+            return undefined;
+          }
+          return promiseActiveExperiments().then(experiments => {
+            return experiments.includes(name);
+          });
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/experiments.json
@@ -0,0 +1,21 @@
+[
+  {
+    "namespace": "experiments",
+    "description": "experimental API extension to allow checking the status of Fennec experiments via Switchboard",
+    "functions": [
+      {
+        "name": "isActive",
+        "type": "function",
+        "description": "Determine if a given experiment is active.",
+        "parameters": [
+          {
+            "name": "name",
+            "type": "string",
+            "description": "The experiment's name"
+          }
+        ],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/sharedPreferences.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, Services, XPCOMUtils */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+  SharedPreferences: "resource://gre/modules/SharedPreferences.jsm",
+});
+
+this.sharedPreferences = class extends ExtensionAPI {
+  getAPI(context) {
+    const extensionIDBase = context.extension.id.split("@")[0];
+    const extensionBaseKey = `extensions.${extensionIDBase}`;
+
+    return {
+      sharedPreferences: {
+        async setBoolPref(name, value) {
+          if (!Services.androidBridge || !Services.androidBridge.isFennec) {
+            return;
+          }
+          SharedPreferences.forApp().setBoolPref(
+            `${extensionBaseKey}.${value}`,
+            value
+          );
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/sharedPreferences.json
@@ -0,0 +1,26 @@
+[
+  {
+    "namespace": "sharedPreferences",
+    "description": "experimental API extension to allow setting SharedPreferences on Fennec",
+    "functions": [
+      {
+        "name": "setBoolPref",
+        "type": "function",
+        "description": "Set the value of a boolean Fennec SharedPreference",
+        "parameters": [
+          {
+            "name": "name",
+            "type": "string",
+            "description": "The key name"
+          },
+          {
+            "name": "value",
+            "type": "boolean",
+            "description": "The new value"
+          }
+        ],
+        "async": true
+      }
+    ]
+  }
+]
--- a/browser/extensions/webcompat/lib/ua_overrides.js
+++ b/browser/extensions/webcompat/lib/ua_overrides.js
@@ -47,45 +47,162 @@ class UAOverrides {
     return this._overridesEnabled;
   }
 
   enableOverride(override) {
     if (override.active) {
       return;
     }
 
-    const { matches, uaTransformer } = override.config;
+    const { blocks, matches, telemetryKey, uaTransformer } = override.config;
+    // Where standard extension match-patterns do the job, we use them.
+    // Otherwise we match regular expressions ourselves, on *all* requests.
+    const regex = matches instanceof RegExp && matches;
+    const urls = regex ? ["*://*/*"] : matches;
     const listener = details => {
-      for (const header of details.requestHeaders) {
-        if (header.name.toLowerCase() === "user-agent") {
-          header.value = uaTransformer(header.value);
+      if (!regex || details.url.match(regex)) {
+        // Don't actually override the UA for an experiment if the user is not
+        // part of the experiment (unless they force-enabed the override).
+        if (
+          !override.config.experiment ||
+          override.experimentActive ||
+          override.permanentPrefEnabled === true
+        ) {
+          if (telemetryKey && !details.frameId) {
+            // For now, we only care about Telemetry on Fennec, where telemetry
+            // is sent in Java code (as part of the core ping). That code must
+            // be aware of each key we send, which we send as a SharedPreference.
+            browser.sharedPreferences.setBoolPref(`${telemetryKey}Used`, true);
+          }
+
+          for (const header of details.requestHeaders) {
+            if (header.name.toLowerCase() === "user-agent") {
+              header.value = uaTransformer(header.value);
+            }
+          }
         }
       }
       return { requestHeaders: details.requestHeaders };
     };
 
-    browser.webRequest.onBeforeSendHeaders.addListener(
-      listener,
-      { urls: matches },
-      ["blocking", "requestHeaders"]
-    );
+    browser.webRequest.onBeforeSendHeaders.addListener(listener, { urls }, [
+      "blocking",
+      "requestHeaders",
+    ]);
+
+    const listeners = { onBeforeSendHeaders: listener };
+    if (blocks) {
+      const bregex = blocks instanceof RegExp && blocks;
+      const burls = bregex ? ["*://*/*"] : blocks;
+
+      const blistener = details => {
+        const cancel = !bregex || !!details.url.match(bregex);
+        return { cancel };
+      };
+
+      browser.webRequest.onBeforeRequest.addListener(
+        blistener,
+        { urls: burls },
+        ["blocking"]
+      );
+
+      listeners.onBeforeRequest = blistener;
+    }
+    this._activeListeners.set(override, listeners);
+    override.active = true;
+
+    // If collecting telemetry on the override, note that it was activated.
+    if (telemetryKey) {
+      browser.sharedPreferences.setBoolPref(`${telemetryKey}Ready`, true);
+    }
+  }
+
+  onOverrideConfigChanged(override) {
+    // Check whether the override should be hidden from about:compat.
+    override.hidden = override.config.hidden;
 
-    this._activeListeners.set(override, listener);
-    override.active = true;
+    // Also hide if the override is in an experiment the user is not part of.
+    if (override.config.experiment && !override.experimentActive) {
+      override.hidden = true;
+    }
+
+    // Setting the override's permanent pref overrules whether it is hidden.
+    if (override.permanentPrefEnabled !== undefined) {
+      override.hidden = !override.permanentPrefEnabled;
+    }
+
+    // Also check whether the override should be active.
+    let shouldBeActive = true;
+
+    // Overrides can be force-deactivated by their permanent preference.
+    if (override.permanentPrefEnabled === false) {
+      shouldBeActive = false;
+    }
+
+    // Overrides gated behind an experiment the user is not part of do not
+    // have to be activated, unless they are gathering telemetry, or the
+    // user has force-enabled them with their permanent pref.
+    if (
+      override.config.experiment &&
+      !override.experimentActive &&
+      !override.config.telemetryKey &&
+      override.permanentPrefEnabled !== true
+    ) {
+      shouldBeActive = false;
+    }
+
+    if (shouldBeActive) {
+      this.enableOverride(override);
+    } else {
+      this.disableOverride(override);
+    }
+
+    if (this._overridesEnabled) {
+      this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
+        overridesChanged: this._aboutCompatBroker.filterOverrides(
+          this._availableOverrides
+        ),
+      });
+    }
   }
 
   async registerUAOverrides() {
     const platformMatches = ["all"];
     let platformInfo = await browser.runtime.getPlatformInfo();
     platformMatches.push(platformInfo.os == "android" ? "android" : "desktop");
 
     for (const override of this._availableOverrides) {
       if (platformMatches.includes(override.platform)) {
         override.availableOnPlatform = true;
-        this.enableOverride(override);
+
+        // If there is a specific experiment running for
+        // this override, get its state.
+        const experiment = override.config.experiment;
+        override.experimentActive =
+          experiment && (await browser.experiments.isActive(experiment));
+
+        // If there is a specific about:config preference governing
+        // this override, monitor its state.
+        const pref = override.config.permanentPref;
+        override.permanentPrefEnabled =
+          pref && (await browser.aboutConfigPrefs.getPref(pref));
+        if (pref) {
+          const checkOverridePref = () => {
+            browser.aboutConfigPrefs.getPref(pref).then(value => {
+              override.permanentPrefEnabled = value;
+              this.onOverrideConfigChanged(override);
+            });
+          };
+          browser.aboutConfigPrefs.onPrefChange.addListener(
+            checkOverridePref,
+            pref
+          );
+        }
+
+        this.onOverrideConfigChanged(override);
       }
     }
 
     this._overridesEnabled = true;
     this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
       overridesChanged: this._aboutCompatBroker.filterOverrides(
         this._availableOverrides
       ),
@@ -103,17 +220,18 @@ class UAOverrides {
     });
   }
 
   disableOverride(override) {
     if (!override.active) {
       return;
     }
 
-    browser.webRequest.onBeforeSendHeaders.removeListener(
-      this._activeListeners.get(override)
-    );
+    const listeners = this._activeListeners.get(override);
+    for (const [name, listener] of Object.entries(listeners)) {
+      browser.webRequest[name].removeListener(listener);
+    }
     override.active = false;
     this._activeListeners.delete(override);
   }
 }
 
 module.exports = UAOverrides;
--- a/browser/extensions/webcompat/manifest.json
+++ b/browser/extensions/webcompat/manifest.json
@@ -1,13 +1,13 @@
 {
   "manifest_version": 2,
   "name": "Web Compat",
   "description": "Urgent post-release fixes for web compatibility.",
-  "version": "5.0.2",
+  "version": "6.0.0",
 
   "applications": {
     "gecko": {
       "id": "webcompat@mozilla.org",
       "strict_min_version": "59.0b5"
     }
   },
 
@@ -22,16 +22,32 @@
     },
     "aboutPage": {
       "schema": "about-compat/aboutPage.json",
       "parent": {
         "scopes": ["addon_parent"],
         "script": "about-compat/aboutPage.js",
         "events": ["startup"]
       }
+    },
+    "experiments": {
+      "schema": "experiment-apis/experiments.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experiment-apis/experiments.js",
+        "paths": [["experiments"]]
+      }
+    },
+    "sharedPreferences": {
+      "schema": "experiment-apis/sharedPreferences.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experiment-apis/sharedPreferences.js",
+        "paths": [["sharedPreferences"]]
+      }
     }
   },
 
   "content_security_policy": "script-src 'self' 'sha256-MmZkN2QaIHhfRWPZ8TVRjijTn5Ci1iEabtTEWrt9CCo='; default-src 'self'; base-uri moz-extension://*;",
 
   "permissions": [
     "webRequest",
     "webRequestBlocking",
--- a/browser/extensions/webcompat/moz.build
+++ b/browser/extensions/webcompat/moz.build
@@ -4,61 +4,65 @@
 # 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/.
 
 DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org'] += [
   'manifest.json',
-  'run.js'
+  'run.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['about-compat'] += [
   'about-compat/aboutCompat.css',
   'about-compat/aboutCompat.html',
   'about-compat/aboutCompat.js',
   'about-compat/AboutCompat.jsm',
   'about-compat/aboutPage.js',
   'about-compat/aboutPage.json',
-  'about-compat/aboutPageProcessScript.js'
+  'about-compat/aboutPageProcessScript.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['data'] += [
   'data/injections.js',
-  'data/ua_overrides.js'
+  'data/ua_overrides.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['experiment-apis'] += [
   'experiment-apis/aboutConfigPrefs.js',
-  'experiment-apis/aboutConfigPrefs.json'
+  'experiment-apis/aboutConfigPrefs.json',
+  'experiment-apis/experiments.js',
+  'experiment-apis/experiments.json',
+  'experiment-apis/sharedPreferences.js',
+  'experiment-apis/sharedPreferences.json',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['injections']['css'] += [
   'injections/css/bug0000000-testbed-css-injection.css',
   'injections/css/bug1305028-gaming.youtube.com-webkit-scrollbar.css',
   'injections/css/bug1432935-breitbart.com-webkit-scrollbar.css',
   'injections/css/bug1432935-discordapp.com-webkit-scorllbar-white-line.css',
   'injections/css/bug1518781-twitch.tv-webkit-scrollbar.css',
   'injections/css/bug1526977-sreedharscce.in-login-fix.css',
   'injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css',
   'injections/css/bug1567610-dns.google.com-moz-fit-content.css',
-  'injections/css/bug1568256-zertifikate.commerzbank.de-flex.css'
+  'injections/css/bug1568256-zertifikate.commerzbank.de-flex.css',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['injections']['js'] += [
   'injections/js/bug0000000-testbed-js-injection.js',
   'injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js',
   'injections/js/bug1457335-histography.io-ua-change.js',
   'injections/js/bug1472075-bankofamerica.com-ua-change.js',
   'injections/js/bug1472081-election.gov.np-window.sidebar-shim.js',
-  'injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js'
+  'injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['lib'] += [
   'lib/about_compat_broker.js',
   'lib/injections.js',
   'lib/module_shim.js',
-  'lib/ua_overrides.js'
+  'lib/ua_overrides.js',
 ]
 
 with Files('**'):
   BUG_COMPONENT = ('Web Compatibility Tools', 'Go Faster')
--- a/mobile/android/base/java/org/mozilla/gecko/Experiments.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Experiments.java
@@ -17,16 +17,19 @@ import java.util.List;
 
 /**
  * This class should reflect the experiment names found in the Switchboard experiments config here:
  * https://github.com/mozilla-services/switchboard-experiments
  */
 public class Experiments {
     private static final String LOGTAG = "GeckoExperiments";
 
+    // Enable the Enhanced Search Experiment.
+    public static final String ENHANCED_SEARCH = "enhanced-search";
+
     // Show a system notification linking to a "What's New" page on app update.
     public static final String WHATSNEW_NOTIFICATION = "whatsnew-notification";
 
     // Synchronizing the catalog of downloadable content from Kinto
     public static final String DOWNLOAD_CONTENT_CATALOG_SYNC = "download-content-catalog-sync";
 
     // Promotion to bookmark reader-view items after entering reader view three times (Bug 1247689)
     public static final String TRIPLE_READERVIEW_BOOKMARK_PROMPT = "triple-readerview-bookmark-prompt";
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -144,16 +144,18 @@ public abstract class GeckoApp extends G
     public static final String EXTRA_STATE_BUNDLE          = "stateBundle";
 
     public static final String PREFS_ALLOW_STATE_BUNDLE    = "allowStateBundle";
     public static final String PREFS_FLASH_USAGE           = "playFlashCount";
     public static final String PREFS_VERSION_CODE          = "versionCode";
     public static final String PREFS_WAS_STOPPED           = "wasStopped";
     public static final String PREFS_CRASHED_COUNT         = "crashedCount";
     public static final String PREFS_CLEANUP_TEMP_FILES    = "cleanupTempFiles";
+    public static final String PREFS_ENHANCED_SEARCH_USAGE = "enhancedSearchUsed";
+    public static final String PREFS_ENHANCED_SEARCH_READY = "enhancedSearchReady";
 
     /**
      * Used with SharedPreferences, per profile, to determine if this is the first run of
      * the application. When accessing SharedPreferences, the default value of true should be used.
 
      * Originally, this was only used for the telemetry core ping logic. To avoid
      * having to write custom migration logic, we just keep the original pref key.
      * Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils#GECKO_PREFS_IS_FIRST_RUN}.
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
@@ -105,16 +105,21 @@ public class TelemetryCorePingBuilder ex
         payload.put(TIMEZONE_OFFSET, DateUtil.getTimezoneOffsetInMinutesForGivenDate(nowCalendar));
         payload.put(DISPLAY_VERSION, AppConstants.MOZ_APP_VERSION_DISPLAY);
         payload.putArray(EXPERIMENTS, Experiments.getActiveExperiments(context));
         synchronized (this) {
             SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
             final int count = prefs.getInt(GeckoApp.PREFS_FLASH_USAGE, 0);
             payload.put(FLASH_USAGE, count);
             prefs.edit().putInt(GeckoApp.PREFS_FLASH_USAGE, 0).apply();
+
+            final boolean searchUsed = prefs.getBoolean(GeckoApp.PREFS_ENHANCED_SEARCH_USAGE, false);
+            payload.put(GeckoApp.PREFS_ENHANCED_SEARCH_USAGE, searchUsed);
+            final boolean searchReady = prefs.getBoolean(GeckoApp.PREFS_ENHANCED_SEARCH_READY, false);
+            payload.put(GeckoApp.PREFS_ENHANCED_SEARCH_READY, searchReady);
         }
     }
 
     @Override
     public String getDocType() {
         return NAME;
     }
 
--- a/mobile/android/extensions/webcompat/data/ua_overrides.js
+++ b/mobile/android/extensions/webcompat/data/ua_overrides.js
@@ -13,29 +13,50 @@
  * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
  */
 const AVAILABLE_UA_OVERRIDES = [
   {
     id: "testbed-override",
     platform: "all",
     domain: "webcompat-addon-testbed.herokuapp.com",
     bug: "0000000",
-    hidden: true,
     config: {
+      hidden: true,
       matches: ["*://webcompat-addon-testbed.herokuapp.com/*"],
       uaTransformer: originalUA => {
         return (
           UAHelpers.getPrefix(originalUA) +
           " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36 for WebCompat"
         );
       },
     },
   },
   {
     /*
+     * Bug 1564594 - Create UA override for Enhanced Search on Firefox Android
+     *
+     * Enables the Chrome Google Search experience for Fennec users.
+     */
+    id: "bug1564594",
+    platform: "android",
+    domain: "Enhanced Search",
+    bug: "1567945",
+    config: {
+      matches: /^https?:\/\/(www|encrypted|maps|news|images)\.google(\.com?)?\.([a-z])+?($|\/)/,
+      blocks: /^https?:\/\/www\.google(\.com?)?\.([a-z])+?\/serviceworker($|\/)/,
+      permanentPref: "enable_enhanced_search",
+      telemetryKey: "enhancedSearch",
+      experiment: "enhanced-search-experiment",
+      uaTransformer: originalUA => {
+        return UAHelpers.getDeviceAppropriateChromeUA();
+      },
+    },
+  },
+  {
+    /*
      * Bug 1563839 - rolb.santanderbank.com - Build UA override
      * WebCompat issue #33462 - https://webcompat.com/issues/33462
      *
      * santanderbank expects UA to have 'like Gecko', otherwise it runs
      * xmlDoc.onload whose support has been dropped. It results in missing labels in forms
      * and some other issues.  Adding 'like Gecko' fixes those issues.
      */
     id: "bug1563839",
@@ -314,14 +335,31 @@ const AVAILABLE_UA_OVERRIDES = [
       uaTransformer: originalUA => {
         return originalUA.replace(/Firefox.+$/, "");
       },
     },
   },
 ];
 
 const UAHelpers = {
+  getDeviceAppropriateChromeUA() {
+    if (!UAHelpers._deviceAppropriateChromeUA) {
+      const RunningFirefoxVersion = (navigator.userAgent.match(
+        /Firefox\/([0-9.]+)/
+      ) || ["", "58.0"])[1];
+      const RunningAndroidVersion =
+        navigator.userAgent.match(/Android\/[0-9.]+/) || "Android 6.0";
+      const ChromeVersionToMimic = "76.0.3809.111";
+      const ChromePhoneUA = `Mozilla/5.0 (Linux; ${RunningAndroidVersion}; Nexus 5 Build/MRA58N) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ChromeVersionToMimic} Mobile Safari/537.36`;
+      const ChromeTabletUA = `Mozilla/5.0 (Linux; ${RunningAndroidVersion}; Nexus 7 Build/JSS15Q) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ChromeVersionToMimic} Safari/537.36`;
+      const IsPhone = navigator.userAgent.includes("Mobile");
+      UAHelpers._deviceAppropriateChromeUA = IsPhone
+        ? ChromePhoneUA
+        : ChromeTabletUA;
+    }
+    return UAHelpers._deviceAppropriateChromeUA;
+  },
   getPrefix(originalUA) {
     return originalUA.substr(0, originalUA.indexOf(")") + 1);
   },
 };
 
 module.exports = AVAILABLE_UA_OVERRIDES;
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/experiment-apis/experiments.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, Services, XPCOMUtils */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  EventDispatcher: "resource://gre/modules/Messaging.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+});
+
+this.experiments = class extends ExtensionAPI {
+  getAPI(context) {
+    function promiseActiveExperiments() {
+      return EventDispatcher.instance.sendRequestForResult({
+        type: "Experiments:GetActive",
+      });
+    }
+    return {
+      experiments: {
+        async isActive(name) {
+          if (!Services.androidBridge || !Services.androidBridge.isFennec) {
+            return undefined;
+          }
+          return promiseActiveExperiments().then(experiments => {
+            return experiments.includes(name);
+          });
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/experiment-apis/experiments.json
@@ -0,0 +1,21 @@
+[
+  {
+    "namespace": "experiments",
+    "description": "experimental API extension to allow checking the status of Fennec experiments via Switchboard",
+    "functions": [
+      {
+        "name": "isActive",
+        "type": "function",
+        "description": "Determine if a given experiment is active.",
+        "parameters": [
+          {
+            "name": "name",
+            "type": "string",
+            "description": "The experiment's name"
+          }
+        ],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/experiment-apis/sharedPreferences.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, Services, XPCOMUtils */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+  SharedPreferences: "resource://gre/modules/SharedPreferences.jsm",
+});
+
+this.sharedPreferences = class extends ExtensionAPI {
+  getAPI(context) {
+    const extensionIDBase = context.extension.id.split("@")[0];
+    const extensionBaseKey = `extensions.${extensionIDBase}`;
+
+    return {
+      sharedPreferences: {
+        async setBoolPref(name, value) {
+          if (!Services.androidBridge || !Services.androidBridge.isFennec) {
+            return;
+          }
+          SharedPreferences.forApp().setBoolPref(
+            `${extensionBaseKey}.${value}`,
+            value
+          );
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/experiment-apis/sharedPreferences.json
@@ -0,0 +1,26 @@
+[
+  {
+    "namespace": "sharedPreferences",
+    "description": "experimental API extension to allow setting SharedPreferences on Fennec",
+    "functions": [
+      {
+        "name": "setBoolPref",
+        "type": "function",
+        "description": "Set the value of a boolean Fennec SharedPreference",
+        "parameters": [
+          {
+            "name": "name",
+            "type": "string",
+            "description": "The key name"
+          },
+          {
+            "name": "value",
+            "type": "boolean",
+            "description": "The new value"
+          }
+        ],
+        "async": true
+      }
+    ]
+  }
+]
--- a/mobile/android/extensions/webcompat/lib/ua_overrides.js
+++ b/mobile/android/extensions/webcompat/lib/ua_overrides.js
@@ -47,45 +47,162 @@ class UAOverrides {
     return this._overridesEnabled;
   }
 
   enableOverride(override) {
     if (override.active) {
       return;
     }
 
-    const { matches, uaTransformer } = override.config;
+    const { blocks, matches, telemetryKey, uaTransformer } = override.config;
+    // Where standard extension match-patterns do the job, we use them.
+    // Otherwise we match regular expressions ourselves, on *all* requests.
+    const regex = matches instanceof RegExp && matches;
+    const urls = regex ? ["*://*/*"] : matches;
     const listener = details => {
-      for (const header of details.requestHeaders) {
-        if (header.name.toLowerCase() === "user-agent") {
-          header.value = uaTransformer(header.value);
+      if (!regex || details.url.match(regex)) {
+        // Don't actually override the UA for an experiment if the user is not
+        // part of the experiment (unless they force-enabed the override).
+        if (
+          !override.config.experiment ||
+          override.experimentActive ||
+          override.permanentPrefEnabled === true
+        ) {
+          if (telemetryKey && !details.frameId) {
+            // For now, we only care about Telemetry on Fennec, where telemetry
+            // is sent in Java code (as part of the core ping). That code must
+            // be aware of each key we send, which we send as a SharedPreference.
+            browser.sharedPreferences.setBoolPref(`${telemetryKey}Used`, true);
+          }
+
+          for (const header of details.requestHeaders) {
+            if (header.name.toLowerCase() === "user-agent") {
+              header.value = uaTransformer(header.value);
+            }
+          }
         }
       }
       return { requestHeaders: details.requestHeaders };
     };
 
-    browser.webRequest.onBeforeSendHeaders.addListener(
-      listener,
-      { urls: matches },
-      ["blocking", "requestHeaders"]
-    );
+    browser.webRequest.onBeforeSendHeaders.addListener(listener, { urls }, [
+      "blocking",
+      "requestHeaders",
+    ]);
+
+    const listeners = { onBeforeSendHeaders: listener };
+    if (blocks) {
+      const bregex = blocks instanceof RegExp && blocks;
+      const burls = bregex ? ["*://*/*"] : blocks;
+
+      const blistener = details => {
+        const cancel = !bregex || !!details.url.match(bregex);
+        return { cancel };
+      };
+
+      browser.webRequest.onBeforeRequest.addListener(
+        blistener,
+        { urls: burls },
+        ["blocking"]
+      );
+
+      listeners.onBeforeRequest = blistener;
+    }
+    this._activeListeners.set(override, listeners);
+    override.active = true;
+
+    // If collecting telemetry on the override, note that it was activated.
+    if (telemetryKey) {
+      browser.sharedPreferences.setBoolPref(`${telemetryKey}Ready`, true);
+    }
+  }
+
+  onOverrideConfigChanged(override) {
+    // Check whether the override should be hidden from about:compat.
+    override.hidden = override.config.hidden;
 
-    this._activeListeners.set(override, listener);
-    override.active = true;
+    // Also hide if the override is in an experiment the user is not part of.
+    if (override.config.experiment && !override.experimentActive) {
+      override.hidden = true;
+    }
+
+    // Setting the override's permanent pref overrules whether it is hidden.
+    if (override.permanentPrefEnabled !== undefined) {
+      override.hidden = !override.permanentPrefEnabled;
+    }
+
+    // Also check whether the override should be active.
+    let shouldBeActive = true;
+
+    // Overrides can be force-deactivated by their permanent preference.
+    if (override.permanentPrefEnabled === false) {
+      shouldBeActive = false;
+    }
+
+    // Overrides gated behind an experiment the user is not part of do not
+    // have to be activated, unless they are gathering telemetry, or the
+    // user has force-enabled them with their permanent pref.
+    if (
+      override.config.experiment &&
+      !override.experimentActive &&
+      !override.config.telemetryKey &&
+      override.permanentPrefEnabled !== true
+    ) {
+      shouldBeActive = false;
+    }
+
+    if (shouldBeActive) {
+      this.enableOverride(override);
+    } else {
+      this.disableOverride(override);
+    }
+
+    if (this._overridesEnabled) {
+      this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
+        overridesChanged: this._aboutCompatBroker.filterOverrides(
+          this._availableOverrides
+        ),
+      });
+    }
   }
 
   async registerUAOverrides() {
     const platformMatches = ["all"];
     let platformInfo = await browser.runtime.getPlatformInfo();
     platformMatches.push(platformInfo.os == "android" ? "android" : "desktop");
 
     for (const override of this._availableOverrides) {
       if (platformMatches.includes(override.platform)) {
         override.availableOnPlatform = true;
-        this.enableOverride(override);
+
+        // If there is a specific experiment running for
+        // this override, get its state.
+        const experiment = override.config.experiment;
+        override.experimentActive =
+          experiment && (await browser.experiments.isActive(experiment));
+
+        // If there is a specific about:config preference governing
+        // this override, monitor its state.
+        const pref = override.config.permanentPref;
+        override.permanentPrefEnabled =
+          pref && (await browser.aboutConfigPrefs.getPref(pref));
+        if (pref) {
+          const checkOverridePref = () => {
+            browser.aboutConfigPrefs.getPref(pref).then(value => {
+              override.permanentPrefEnabled = value;
+              this.onOverrideConfigChanged(override);
+            });
+          };
+          browser.aboutConfigPrefs.onPrefChange.addListener(
+            checkOverridePref,
+            pref
+          );
+        }
+
+        this.onOverrideConfigChanged(override);
       }
     }
 
     this._overridesEnabled = true;
     this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
       overridesChanged: this._aboutCompatBroker.filterOverrides(
         this._availableOverrides
       ),
@@ -103,17 +220,18 @@ class UAOverrides {
     });
   }
 
   disableOverride(override) {
     if (!override.active) {
       return;
     }
 
-    browser.webRequest.onBeforeSendHeaders.removeListener(
-      this._activeListeners.get(override)
-    );
+    const listeners = this._activeListeners.get(override);
+    for (const [name, listener] of Object.entries(listeners)) {
+      browser.webRequest[name].removeListener(listener);
+    }
     override.active = false;
     this._activeListeners.delete(override);
   }
 }
 
 module.exports = UAOverrides;
--- a/mobile/android/extensions/webcompat/manifest.json
+++ b/mobile/android/extensions/webcompat/manifest.json
@@ -1,13 +1,13 @@
 {
   "manifest_version": 2,
   "name": "Web Compat",
   "description": "Urgent post-release fixes for web compatibility.",
-  "version": "5.0.2",
+  "version": "6.0.0",
 
   "applications": {
     "gecko": {
       "id": "webcompat@mozilla.org",
       "strict_min_version": "59.0b5"
     }
   },
 
@@ -22,16 +22,32 @@
     },
     "aboutPage": {
       "schema": "about-compat/aboutPage.json",
       "parent": {
         "scopes": ["addon_parent"],
         "script": "about-compat/aboutPage.js",
         "events": ["startup"]
       }
+    },
+    "experiments": {
+      "schema": "experiment-apis/experiments.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experiment-apis/experiments.js",
+        "paths": [["experiments"]]
+      }
+    },
+    "sharedPreferences": {
+      "schema": "experiment-apis/sharedPreferences.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experiment-apis/sharedPreferences.js",
+        "paths": [["sharedPreferences"]]
+      }
     }
   },
 
   "content_security_policy": "script-src 'self' 'sha256-MmZkN2QaIHhfRWPZ8TVRjijTn5Ci1iEabtTEWrt9CCo='; default-src 'self'; base-uri moz-extension://*;",
 
   "permissions": [
     "webRequest",
     "webRequestBlocking",
--- a/mobile/android/extensions/webcompat/moz.build
+++ b/mobile/android/extensions/webcompat/moz.build
@@ -4,61 +4,65 @@
 # 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/.
 
 DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org'] += [
   'manifest.json',
-  'run.js'
+  'run.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['about-compat'] += [
   'about-compat/aboutCompat.css',
   'about-compat/aboutCompat.html',
   'about-compat/aboutCompat.js',
   'about-compat/AboutCompat.jsm',
   'about-compat/aboutPage.js',
   'about-compat/aboutPage.json',
-  'about-compat/aboutPageProcessScript.js'
+  'about-compat/aboutPageProcessScript.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['data'] += [
   'data/injections.js',
-  'data/ua_overrides.js'
+  'data/ua_overrides.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['experiment-apis'] += [
   'experiment-apis/aboutConfigPrefs.js',
-  'experiment-apis/aboutConfigPrefs.json'
+  'experiment-apis/aboutConfigPrefs.json',
+  'experiment-apis/experiments.js',
+  'experiment-apis/experiments.json',
+  'experiment-apis/sharedPreferences.js',
+  'experiment-apis/sharedPreferences.json',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['injections']['css'] += [
   'injections/css/bug0000000-testbed-css-injection.css',
   'injections/css/bug1305028-gaming.youtube.com-webkit-scrollbar.css',
   'injections/css/bug1432935-breitbart.com-webkit-scrollbar.css',
   'injections/css/bug1432935-discordapp.com-webkit-scorllbar-white-line.css',
   'injections/css/bug1518781-twitch.tv-webkit-scrollbar.css',
   'injections/css/bug1526977-sreedharscce.in-login-fix.css',
   'injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css',
   'injections/css/bug1567610-dns.google.com-moz-fit-content.css',
-  'injections/css/bug1568256-zertifikate.commerzbank.de-flex.css'
+  'injections/css/bug1568256-zertifikate.commerzbank.de-flex.css',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['injections']['js'] += [
   'injections/js/bug0000000-testbed-js-injection.js',
   'injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js',
   'injections/js/bug1457335-histography.io-ua-change.js',
   'injections/js/bug1472075-bankofamerica.com-ua-change.js',
   'injections/js/bug1472081-election.gov.np-window.sidebar-shim.js',
-  'injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js'
+  'injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['lib'] += [
   'lib/about_compat_broker.js',
   'lib/injections.js',
   'lib/module_shim.js',
-  'lib/ua_overrides.js'
+  'lib/ua_overrides.js',
 ]
 
 with Files('**'):
   BUG_COMPONENT = ('Web Compatibility Tools', 'Go Faster')