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

--HG--
extra : rebase_source : dbe9815127c1aa620bbc2f1623aa4726438d3285
This commit is contained in:
Sebastian Kaspari 2017-03-20 19:46:39 +01:00
Родитель c188818aea
Коммит d60800bd78
5 изменённых файлов: 171 добавлений и 35 удалений

Просмотреть файл

@ -59,6 +59,12 @@ public class Experiments {
// 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";

Просмотреть файл

@ -6,6 +6,7 @@
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;
@ -48,35 +49,98 @@ public class ActivityStream {
"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;
}
return GeckoSharedPrefs.forApp(context)
.getBoolean(GeckoPreferences.PREFS_ACTIVITY_STREAM, 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);
}
/**
* Is the user eligible to use activity stream or should we hide it from settings etc.?
* Set the user's decision: Enable or disable Activity Stream.
*/
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 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");
}
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;
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 Activity Stream enabled? Either actively by the user or by an experiment?
*/
public static boolean isEnabled(Context context) {
// (1) Can Activity Steam be enabled on this device?
if (!canBeEnabled(context)) {
return false;
}
// For everyone else activity stream is not available yet.
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;
}
// (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;
}
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;
}
/**

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -168,7 +168,7 @@ public class GeckoPreferences
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";
@ -715,8 +715,8 @@ public class GeckoPreferences
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;
@ -916,7 +916,8 @@ public class GeckoPreferences
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;
@ -1242,13 +1243,6 @@ public class GeckoPreferences
}
} 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);

Просмотреть файл

@ -84,10 +84,10 @@
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"