Bug 1270929 - (Part 2) Create Experiments.setOverride/clearOverride API to allow add-ons to force enable/disable experiments. r=sebastian, a=ritu
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Sat, 07 May 2016 16:44:00 -0400
changeset 332825 f5f954dcd9aa9db058cf1761a71289d7f5557fa1
parent 332824 521f93a84891d00a4e860d7281cf8925d23440f2
child 332826 115883bbe33fcd1d71046c4f02b66450fbe4259f
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian, ritu
bugs1270929
milestone48.0a2
Bug 1270929 - (Part 2) Create Experiments.setOverride/clearOverride API to allow add-ons to force enable/disable experiments. r=sebastian, a=ritu MozReview-Commit-ID: 1tCstZwVl9T
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
mobile/android/chrome/content/browser.js
mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java
mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java
mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -736,16 +736,18 @@ public class BrowserApp extends GeckoApp
             "Search:Keyword",
             "Prompt:ShowTop");
 
         EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this,
             "CharEncoding:Data",
             "CharEncoding:State",
             "Download:AndroidDownloadManager",
             "Experiments:GetActive",
+            "Experiments:SetOverride",
+            "Experiments:ClearOverride",
             "Favicon:CacheLoad",
             "Feedback:MaybeLater",
             "Menu:Add",
             "Menu:Remove",
             "Sanitize:ClearHistory",
             "Sanitize:ClearSyncedTabs",
             "Settings:Show",
             "Telemetry:Gather",
@@ -1507,16 +1509,18 @@ public class BrowserApp extends GeckoApp
             "Search:Keyword",
             "Prompt:ShowTop");
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
             "CharEncoding:Data",
             "CharEncoding:State",
             "Download:AndroidDownloadManager",
             "Experiments:GetActive",
+            "Experiments:SetOverride",
+            "Experiments:ClearOverride",
             "Favicon:CacheLoad",
             "Feedback:MaybeLater",
             "Menu:Add",
             "Menu:Remove",
             "Sanitize:ClearHistory",
             "Sanitize:ClearSyncedTabs",
             "Settings:Show",
             "Telemetry:Gather",
@@ -1778,16 +1782,20 @@ public class BrowserApp extends GeckoApp
                     }
                 }
             });
 
         } else if ("Experiments:GetActive".equals(event)) {
             final List<String> experiments = SwitchBoard.getActiveExperiments(this);
             final JSONArray json = new JSONArray(experiments);
             callback.sendSuccess(json.toString());
+        } else if ("Experiments:SetOverride".equals(event)) {
+            Experiments.setOverride(getContext(), message.getString("name"), message.getBoolean("isEnabled"));
+        } else if ("Experiments:ClearOverride".equals(event)) {
+            Experiments.clearOverride(getContext(), message.getString("name"));
         } else if ("Favicon:CacheLoad".equals(event)) {
             final String url = message.getString("url");
             getFaviconFromCache(callback, url);
         } else if ("Feedback:MaybeLater".equals(event)) {
             resetFeedbackLaunchCount();
         } else if ("Menu:Add".equals(event)) {
             final MenuItemInfo info = new MenuItemInfo();
             info.label = message.getString("name");
--- a/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
@@ -4,16 +4,18 @@
 
 package org.mozilla.gecko.util;
 
 import android.content.Context;
 
 import android.util.Log;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import android.text.TextUtils;
+
+import com.keepsafe.switchboard.Preferences;
 import com.keepsafe.switchboard.SwitchBoard;
 import org.mozilla.gecko.GeckoSharedPrefs;
 
 import java.util.LinkedList;
 import java.util.List;
 
 /**
  * This class should reflect the experiment names found in the Switchboard experiments config here:
@@ -106,9 +108,33 @@ public class Experiments {
         // Add onboarding version.
         final String onboardingExperiment = GeckoSharedPrefs.forProfile(c).getString(Experiments.PREF_ONBOARDING_VERSION, null);
         if (!TextUtils.isEmpty(onboardingExperiment)) {
             experiments.add(onboardingExperiment);
         }
 
         return experiments;
     }
+
+    /**
+     * Sets an override to force an experiment to be enabled or disabled. This value
+     * will be read and used before reading the switchboard server configuration.
+     *
+     * @param c Context
+     * @param experimentName Experiment name
+     * @param isEnabled Whether or not the experiment should be enabled
+     */
+    public static void setOverride(Context c, String experimentName, boolean isEnabled) {
+        Log.d(LOGTAG, "setOverride: " + experimentName + " = " + isEnabled);
+        Preferences.setOverrideValue(c, experimentName, isEnabled);
+    }
+
+    /**
+     * Clears the override value for an experiment.
+     *
+     * @param c Context
+     * @param experimentName Experiment name
+     */
+    public static void clearOverride(Context c, String experimentName) {
+        Log.d(LOGTAG, "clearOverride: " + experimentName);
+        Preferences.clearOverrideValue(c, experimentName);
+    }
 }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -6793,16 +6793,31 @@ var Experiments = {
           case this.OFFLINE_CACHE: {
             let defaults = Services.prefs.getDefaultBranch(null);
             defaults.setBoolPref("browser.tabs.useCache", true);
             continue;
           }
         }
       }
     });
+  },
+
+  setOverride(name, isEnabled) {
+    Messaging.sendRequest({
+      type: "Experiments:SetOverride",
+      name: name,
+      isEnabled: isEnabled
+    });
+  },
+
+  clearOverride(name) {
+    Messaging.sendRequest({
+      type: "Experiments:ClearOverride",
+      name: name
+    });
   }
 };
 
 var ExternalApps = {
   _contextMenuId: null,
 
   // extend _getLink to pickup html5 media links.
   _getMediaLink: function(aElement) {
--- a/mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java
+++ b/mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java
@@ -3,16 +3,17 @@ package com.keepsafe.switchboard;
 import android.content.Context;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.util.Experiments;
 import org.robolectric.RuntimeEnvironment;
 
 import java.io.IOException;
 import java.util.List;
 import java.util.UUID;
 
 import static org.junit.Assert.*;
 
@@ -57,13 +58,34 @@ public class TestSwitchboard {
     }
 
     @Test
     public void testGetActiveExperiments() {
         final Context c = RuntimeEnvironment.application;
         final List<String> experiments = SwitchBoard.getActiveExperiments(c);
         assertNotNull("List of active experiments is not null", experiments);
 
-        assertTrue("List of active experiments contains active-experiemnt", experiments.contains("active-experiment"));
-        assertFalse("List of active experiments does not contain inactive-experiemnt", experiments.contains("inactive-experiment"));
+        assertTrue("List of active experiments contains active-experiment", experiments.contains("active-experiment"));
+        assertFalse("List of active experiments does not contain inactive-experiment", experiments.contains("inactive-experiment"));
+    }
+
+    @Test
+    public void testOverride() {
+        final Context c = RuntimeEnvironment.application;
+
+        Experiments.setOverride(c, "active-experiment", false);
+        assertFalse("active-experiment is not active because of override", SwitchBoard.isInExperiment(c, "active-experiment"));
+        assertFalse("List of active experiments does not contain active-experiment", SwitchBoard.getActiveExperiments(c).contains("active-experiment"));
+
+        Experiments.clearOverride(c, "active-experiment");
+        assertTrue("active-experiment is active after override is cleared", SwitchBoard.isInExperiment(c, "active-experiment"));
+        assertTrue("List of active experiments contains active-experiment again", SwitchBoard.getActiveExperiments(c).contains("active-experiment"));
+
+        Experiments.setOverride(c, "inactive-experiment", true);
+        assertTrue("inactive-experiment is active because of override", SwitchBoard.isInExperiment(c, "inactive-experiment"));
+        assertTrue("List of active experiments contains inactive-experiment", SwitchBoard.getActiveExperiments(c).contains("inactive-experiment"));
+
+        Experiments.clearOverride(c, "inactive-experiment");
+        assertFalse("inactive-experiment is inactive after override is cleared", SwitchBoard.isInExperiment(c, "inactive-experiment"));
+        assertFalse("List of active experiments does not contain inactive-experiment again", SwitchBoard.getActiveExperiments(c).contains("inactive-experiment"));
     }
 
 }
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java
@@ -27,16 +27,18 @@ import android.support.annotation.Nullab
  */
 public class Preferences {
 
     private static final String switchBoardSettings = "com.keepsafe.switchboard.settings";
 
     private static final String kDynamicConfigServerUrl = "dynamic-config-server-url";
     private static final String kDynamicConfig = "dynamic-config";
 
+    private static final String OVERRIDE_PREFIX = "experiment.override.";
+
     /**
      * Returns the stored config server URL.
      * @param c Context
      * @return URL for config endpoint.
      */
     @Nullable public static String getDynamicConfigServerUrl(Context c) {
         final SharedPreferences prefs = c.getApplicationContext().getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
         return prefs.getString(kDynamicConfigServerUrl, null);
@@ -70,9 +72,57 @@ public class Preferences {
      * @param configJson Config JSON
      */
     public static void setDynamicConfigJson(Context c, String configJson) {
         final SharedPreferences.Editor editor = c.getApplicationContext().
                 getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
         editor.putString(kDynamicConfig, configJson);
         editor.apply();
     }
+
+    /**
+     * Gets the override value for an experiment.
+     *
+     * @param c Context
+     * @param experimentName Experiment name
+     * @return Whether or not the experiment should be enabled, or null if there is no override.
+     */
+    @Nullable public static Boolean getOverrideValue(Context c, String experimentName) {
+        final SharedPreferences prefs = c.getApplicationContext().
+                getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
+
+        final String key = OVERRIDE_PREFIX + experimentName;
+        if (prefs.contains(key)) {
+            // This will never fall back to the default value.
+            return prefs.getBoolean(key, false);
+        }
+
+        // Default to returning null if no override was found.
+        return null;
+    }
+
+    /**
+     * Saves an override value for an experiment.
+     *
+     * @param c Context
+     * @param experimentName Experiment name
+     * @param isEnabled Whether or not to enable the experiment
+     */
+    public static void setOverrideValue(Context c, String experimentName, boolean isEnabled) {
+        final SharedPreferences.Editor editor = c.getApplicationContext().
+                getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+        editor.putBoolean(OVERRIDE_PREFIX + experimentName, isEnabled);
+        editor.apply();
+    }
+
+    /**
+     * Clears the override value for an experiment.
+     *
+     * @param c Context
+     * @param experimentName Experiment name
+     */
+    public static void clearOverrideValue(Context c, String experimentName) {
+        final SharedPreferences.Editor editor = c.getApplicationContext().
+                getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+        editor.remove(OVERRIDE_PREFIX + experimentName);
+        editor.apply();
+    }
 }
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java
@@ -163,54 +163,61 @@ public class SwitchBoard {
     /**
      * Looks up in config if user is in certain experiment. Returns false as a default value when experiment
      * does not exist.
      * Experiment names are defined server side as Key in array for return values.
      * @param experimentName Name of the experiment to lookup
      * @return returns value for experiment or false if experiment does not exist.
      */
     public static boolean isInExperiment(Context c, String experimentName) {
+        final Boolean override = Preferences.getOverrideValue(c, experimentName);
+        if (override != null) {
+            return override;
+        }
+
         final String config = Preferences.getDynamicConfigJson(c);
-
         if (config == null) {
             return false;
         }
 
         try {
             final JSONObject experiment = new JSONObject(config).getJSONObject(experimentName);
             return experiment != null && experiment.getBoolean(IS_EXPERIMENT_ACTIVE);
         } catch (JSONException e) {
             // If the experiment name is not found in the JSON, just return false.
             // There is no need to log an error, since we don't really care if an
             // inactive experiment is missing from the config.
             return false;
         }
     }
 
     /**
-     * @returns a list of all active experiments.
+     * @return a list of all active experiments.
      */
     public static List<String> getActiveExperiments(Context c) {
-        ArrayList<String> returnList = new ArrayList<String>();
+        final ArrayList<String> returnList = new ArrayList<>();
 
-        // lookup experiment in config
-        String config = Preferences.getDynamicConfigJson(c);
-
-        // if it does not exist
+        final String config = Preferences.getDynamicConfigJson(c);
         if (config == null) {
             return returnList;
         }
 
         try {
-            JSONObject experiments = new JSONObject(config);
+            final JSONObject experiments = new JSONObject(config);
             Iterator<?> iter = experiments.keys();
             while (iter.hasNext()) {
-                String key = (String)iter.next();
-                JSONObject experiment = experiments.getJSONObject(key);
-                if (experiment.getBoolean(IS_EXPERIMENT_ACTIVE)) {
+                final String key = (String) iter.next();
+
+                // Check override value before reading saved JSON.
+                Boolean isActive = Preferences.getOverrideValue(c, key);
+                if (isActive == null) {
+                    final JSONObject experiment = experiments.getJSONObject(key);
+                    isActive = experiment.getBoolean(IS_EXPERIMENT_ACTIVE);
+                }
+                if (isActive) {
                     returnList.add(key);
                 }
             }
         } catch (JSONException e) {
             // Something went wrong!
         }
 
         return returnList;