Bug 1348820 - Setup A/B experiment for enabling Activity Stream in Nightly. r?grisha
This is a bit complicated. But most of that code should go away again as soon as
we can stop shipping the opt-out preference.
With this patch we have three flags that can be controlled via Switchboard:
* activity-stream: This is our global kill switch and allows us to pull the feature
if needed. A user has to be in this experiment to ever see activity stream. The
goal is to enable this experiment for 100% of the Nightly audience.
* activity-stream-opt-out: This is experiment will enable the Activity Stream by
default. The goal is to enable this experiment for 50% of the Nightly audience.
* activity-stream-settings: This experiment controls the visibility of a setting
to enable/disable activity stream (settings -> advanced -> experimental features).
This allows us to control whether users can opt-in or opt-out of the activity
experiment. The goal is to enable this for 100% of the Nightly audience.
MozReview-Commit-ID: BwEoTK6QMQx
--- a/mobile/android/base/java/org/mozilla/gecko/Experiments.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Experiments.java
@@ -54,16 +54,22 @@ public class Experiments {
public static final String URLBAR_SHOW_EV_CERT_OWNER = "urlbar-show-ev-cert-owner";
// Play HLS videos in a VideoView (Bug 1313391)
public static final String HLS_VIDEO_PLAYBACK = "hls-video-playback";
// Make new activity stream panel available (to replace top sites) (Bug 1313316)
public static final String ACTIVITY_STREAM = "activity-stream";
+ // Show a setting in "experimental features" for enabling/disabling activity stream.
+ public static final String ACTIVITY_STREAM_SETTING = "activity-stream-setting";
+
+ // Enable Activity stream by default for users in the "opt out" group.
+ public static final String ACTIVITY_STREAM_OPT_OUT = "activity-stream-opt-out";
+
// Tabs tray: Arrange tabs in two columns in portrait mode
public static final String COMPACT_TABS = "compact-tabs";
/**
* Returns if a user is in certain local experiment.
* @param experiment Name of experiment to look up
* @return returns value for experiment or false if experiment does not exist.
*/
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java
@@ -1,16 +1,17 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.activitystream;
import android.content.Context;
+import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.text.TextUtils;
import org.mozilla.gecko.switchboard.SwitchBoard;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Experiments;
@@ -43,45 +44,108 @@ public class ActivityStream {
* - https://github.com/mozilla/activity-stream/issues/1311
*/
private static final List<String> UNDESIRED_LABELS = Arrays.asList(
"render",
"login",
"edit"
);
- public static boolean isEnabled(Context context) {
- if (!isUserEligible(context)) {
- // If the user is not eligible then disable activity stream. Even if it has been
- // enabled before.
- return false;
+ /**
+ * Returns true if the user has made an active decision: Enabling or disabling Activity Stream.
+ */
+ public static boolean hasUserEnabledOrDisabled(Context context) {
+ final SharedPreferences preferences = GeckoSharedPrefs.forApp(context);
+ return preferences.contains(GeckoPreferences.PREFS_ACTIVITY_STREAM);
+ }
+
+ /**
+ * Set the user's decision: Enable or disable Activity Stream.
+ */
+ public static void setUserEnabled(Context context, boolean value) {
+ GeckoSharedPrefs.forApp(context).edit()
+ .putBoolean(GeckoPreferences.PREFS_ACTIVITY_STREAM, value)
+ .apply();
+ }
+
+ /**
+ * Returns true if Activity Stream has been enabled by the user. Before calling this method
+ * hasUserEnabledOrDisabled() should be used to determine whether the user actually has made
+ * a decision.
+ */
+ public static boolean isEnabledByUser(Context context) {
+ final SharedPreferences preferences = GeckoSharedPrefs.forApp(context);
+ if (!preferences.contains(GeckoPreferences.PREFS_ACTIVITY_STREAM)) {
+ throw new IllegalStateException("User hasn't made a decision. Call hasUserEnabledOrDisabled() before calling this method");
}
- return GeckoSharedPrefs.forApp(context)
- .getBoolean(GeckoPreferences.PREFS_ACTIVITY_STREAM, false);
+ return preferences.getBoolean(GeckoPreferences.PREFS_ACTIVITY_STREAM, /* should not be used */ false);
+ }
+
+ /**
+ * Is Activity Stream enabled by an A/B experiment?
+ */
+ public static boolean isEnabledByExperiment(Context context) {
+ // For users in the "opt out" group Activity Stream is enabled by default.
+ return SwitchBoard.isInExperiment(context, Experiments.ACTIVITY_STREAM_OPT_OUT);
}
/**
- * Is the user eligible to use activity stream or should we hide it from settings etc.?
+ * Is Activity Stream enabled? Either actively by the user or by an experiment?
*/
- public static boolean isUserEligible(Context context) {
- if (AppConstants.MOZ_ANDROID_ACTIVITY_STREAM) {
- // If the build flag is enabled then just show the option to the user.
- return true;
+ public static boolean isEnabled(Context context) {
+ // (1) Can Activity Steam be enabled on this device?
+ if (!canBeEnabled(context)) {
+ return false;
+ }
+
+ // (2) Has Activity Stream be enabled/disabled by the user?
+ if (hasUserEnabledOrDisabled(context)) {
+ return isEnabledByUser(context);
+ }
+
+ // (3) Is Activity Stream enabled by an experiment?
+ return isEnabledByExperiment(context);
+ }
+
+ /**
+ * Can the user enable/disable Activity Stream (Returns true) or is this completely controlled by us?
+ */
+ public static boolean isUserSwitchable(Context context) {
+ // (1) Can Activity Steam be enabled on this device?
+ if (!canBeEnabled(context)) {
+ return false;
}
- if (AppConstants.NIGHTLY_BUILD && SwitchBoard.isInExperiment(context, Experiments.ACTIVITY_STREAM)) {
- // If this is a nightly build and the user is part of the activity stream experiment then
- // the option should be visible too. The experiment is limited to Nightly too but I want
- // to make really sure that this isn't riding the trains accidentally.
- return true;
+ // (2) Is the user part of the experiment for showing the settings UI?
+ return SwitchBoard.isInExperiment(context, Experiments.ACTIVITY_STREAM_SETTING);
+ }
+
+ /**
+ * This method returns true if Activity Stream can be enabled - by the user or an experiment.
+ * Whether a setting shows up or whether the user is in an experiment group is evaluated
+ * separately from this method. However if this methods returns false then Activity Stream
+ * should never be visible/enabled - no matter what build or what experiments are active.
+ */
+ public static boolean canBeEnabled(Context context) {
+ if (!AppConstants.NIGHTLY_BUILD) {
+ // If this is not a Nightly build then hide Activity Stream completely. We can control
+ // this via the Switchboard experiment too but I want to make really sure that this
+ // isn't riding the trains accidentally.
+ return false;
}
- // For everyone else activity stream is not available yet.
- return false;
+ if (!SwitchBoard.isInExperiment(context, Experiments.ACTIVITY_STREAM)) {
+ // This is our kill switch. If the user is not part of this experiment then show no
+ // Activity Stream UI.
+ return false;
+ }
+
+ // Activity stream can be enabled. Whether it is depends on other experiments and settings.
+ return true;
}
/**
* Query whether we want to display Activity Stream as a Home Panel (within the HomePager),
* or as a HomePager replacement.
*/
public static boolean isHomePanel() {
return true;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStreamPreference.java
@@ -0,0 +1,72 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.activitystream;
+
+import android.content.Context;
+import android.preference.SwitchPreference;
+import android.util.AttributeSet;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * A custom switch preference that is used while we allow users to opt-out from using Activity Stream.
+ */
+public class ActivityStreamPreference extends SwitchPreference {
+ @SuppressWarnings("unused") // Used from XML
+ public ActivityStreamPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context);
+ }
+
+ @SuppressWarnings("unused") // Used from XML
+ public ActivityStreamPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ @SuppressWarnings("unused") // Used from XML
+ public ActivityStreamPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ @SuppressWarnings("unused") // Used from XML
+ public ActivityStreamPreference(Context context) {
+ super(context);
+ init(context);
+ }
+
+ private void init(Context context) {
+ // The SwitchPreference shouldn't do any persistence itself. We want to avoid that a value
+ // is written that is not set by the user but set from an experiment.
+ setPersistent(false);
+
+ setChecked(ActivityStream.isEnabled(context));
+ }
+
+ @Override
+ public boolean isPersistent() {
+ // Just be absolutely sure that no one re-sets this value since calling init().
+ return false;
+ }
+
+ @Override
+ protected void onClick() {
+ super.onClick();
+
+ ActivityStream.setUserEnabled(getContext(), isChecked());
+
+ // We require a restart for this change to take effect. This is not nice, but this setting
+ // is not something we want to ship outside of Nightly anyways.
+ ThreadUtils.postDelayedToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoAppShell.scheduleRestart();
+ }
+ }, 1000);
+ }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -163,17 +163,17 @@ public class GeckoPreferences
private static final String PREFS_FEEDBACK_LINK = NON_PREF_PREFIX + "feedback.link";
public static final String PREFS_NOTIFICATIONS_CONTENT = NON_PREF_PREFIX + "notifications.content";
public static final String PREFS_NOTIFICATIONS_CONTENT_LEARN_MORE = NON_PREF_PREFIX + "notifications.content.learn_more";
public static final String PREFS_NOTIFICATIONS_WHATS_NEW = NON_PREF_PREFIX + "notifications.whats_new";
public static final String PREFS_APP_UPDATE_LAST_BUILD_ID = "app.update.last_build_id";
public static final String PREFS_READ_PARTNER_CUSTOMIZATIONS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_customizations_provider";
public static final String PREFS_READ_PARTNER_BOOKMARKS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_bookmarks_provider";
public static final String PREFS_CUSTOM_TABS = NON_PREF_PREFIX + "customtabs";
- public static final String PREFS_ACTIVITY_STREAM = NON_PREF_PREFIX + "activitystream";
+ public static final String PREFS_ACTIVITY_STREAM = NON_PREF_PREFIX + "experiments.activitystream";
public static final String PREFS_CATEGORY_EXPERIMENTAL_FEATURES = NON_PREF_PREFIX + "category_experimental";
public static final String PREFS_COMPACT_TABS = NON_PREF_PREFIX + "compact_tabs";
public static final String PREFS_SHOW_QUIT_MENU = NON_PREF_PREFIX + "distribution.show_quit_menu";
public static final String PREFS_SEARCH_SUGGESTIONS_ENABLED = "browser.search.suggest.enabled";
public static final String PREFS_DEFAULT_BROWSER = NON_PREF_PREFIX + "default_browser.link";
private static final String ACTION_STUMBLER_UPLOAD_PREF = "STUMBLER_PREF";
@@ -709,18 +709,18 @@ public class GeckoPreferences
continue;
}
} else if (PREFS_SCREEN_ADVANCED.equals(key) &&
!Restrictions.isAllowed(this, Restrictable.ADVANCED_SETTINGS)) {
preferences.removePreference(pref);
i--;
continue;
} else if (PREFS_CATEGORY_EXPERIMENTAL_FEATURES.equals(key)
- && !AppConstants.MOZ_ANDROID_ACTIVITY_STREAM
- && !AppConstants.MOZ_ANDROID_CUSTOM_TABS) {
+ && !AppConstants.MOZ_ANDROID_CUSTOM_TABS
+ && !ActivityStream.isUserSwitchable(this)) {
preferences.removePreference(pref);
i--;
continue;
}
setupPreferences((PreferenceGroup) pref, prefs);
} else {
if (HANDLERS.containsKey(key)) {
PrefHandler handler = HANDLERS.get(key);
@@ -910,17 +910,18 @@ public class GeckoPreferences
preferences.removePreference(pref);
i--;
continue;
}
} else if (PREFS_CUSTOM_TABS.equals(key) && !AppConstants.MOZ_ANDROID_CUSTOM_TABS) {
preferences.removePreference(pref);
i--;
continue;
- } else if (PREFS_ACTIVITY_STREAM.equals(key) && !ActivityStream.isUserEligible(this)) {
+ } else if (PREFS_ACTIVITY_STREAM.equals(key)
+ && !ActivityStream.isUserSwitchable(this)) {
preferences.removePreference(pref);
i--;
continue;
} else if (PREFS_COMPACT_TABS.equals(key)) {
if (HardwareUtils.isTablet()) {
preferences.removePreference(pref);
i--;
continue;
@@ -1236,23 +1237,16 @@ public class GeckoPreferences
} else if (PREFS_TAB_QUEUE.equals(prefName)) {
if ((Boolean) newValue && !TabQueueHelper.canDrawOverlays(this)) {
Intent promptIntent = new Intent(this, TabQueuePrompt.class);
startActivityForResult(promptIntent, REQUEST_CODE_TAB_QUEUE);
return false;
}
} else if (PREFS_NOTIFICATIONS_CONTENT.equals(prefName)) {
FeedService.setup(this);
- } else if (PREFS_ACTIVITY_STREAM.equals(prefName)) {
- ThreadUtils.postDelayedToUiThread(new Runnable() {
- @Override
- public void run() {
- GeckoAppShell.scheduleRestart();
- }
- }, 1000);
} else if (HANDLERS.containsKey(prefName)) {
PrefHandler handler = HANDLERS.get(prefName);
handler.onChange(this, preference, newValue);
} else if (PREFS_SEARCH_SUGGESTIONS_ENABLED.equals(prefName)) {
// Tell Gecko to transmit the current search engine data again, so
// BrowserSearch is notified immediately about the new enabled state.
EventDispatcher.getInstance().dispatch("SearchEngines:GetVisible", null);
}
--- a/mobile/android/base/resources/xml/preferences_advanced.xml
+++ b/mobile/android/base/resources/xml/preferences_advanced.xml
@@ -79,20 +79,20 @@
android:persistent="false"
url="https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_Firefox_for_Android_with_WebIDE" />
</PreferenceCategory>
<PreferenceCategory
android:key="android.not_a_preference.category_experimental"
android:title="@string/pref_category_experimental">
- <SwitchPreference android:key="android.not_a_preference.activitystream"
+ <org.mozilla.gecko.activitystream.ActivityStreamPreference
+ android:key="android.not_a_preference.experiments.activitystream"
android:title="@string/pref_activity_stream"
- android:summary="@string/pref_activity_stream_summary"
- android:defaultValue="false" />
+ android:summary="@string/pref_activity_stream_summary" />
<SwitchPreference android:key="android.not_a_preference.customtabs"
android:title="@string/pref_custom_tabs"
android:summary="@string/pref_custom_tabs_summary"
android:defaultValue="false" />
</PreferenceCategory>