// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.contextualsearch;
import android.text.TextUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.SysUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.components.variations.VariationsAssociatedData;
/**
* Provides Field Trial support for the Contextual Search application within Chrome for Android.
*/
public class ContextualSearchFieldTrial {
private static final String FIELD_TRIAL_NAME = "ContextualSearch";
private static final String DISABLED_PARAM = "disabled";
private static final String ENABLED_VALUE = "true";
static final String MANDATORY_PROMO_ENABLED = "mandatory_promo_enabled";
static final String MANDATORY_PROMO_LIMIT = "mandatory_promo_limit";
static final int MANDATORY_PROMO_DEFAULT_LIMIT = 10;
private static final String PEEK_PROMO_FORCED = "peek_promo_forced";
@VisibleForTesting
static final String PEEK_PROMO_ENABLED = "peek_promo_enabled";
private static final String PEEK_PROMO_MAX_SHOW_COUNT = "peek_promo_max_show_count";
private static final int PEEK_PROMO_DEFAULT_MAX_SHOW_COUNT = 10;
private static final String DISABLE_SEARCH_TERM_RESOLUTION = "disable_search_term_resolution";
private static final String ENABLE_BLACKLIST = "enable_blacklist";
// Translation. All these members are private, except for usage by testing.
// Master switch, needed to enable any translate code for Contextual Search.
@VisibleForTesting
static final String ENABLE_TRANSLATION = "enable_translation";
// Switch to disable translation, but not logging, used for experiment comparison.
@VisibleForTesting
static final String DISABLE_FORCE_TRANSLATION_ONEBOX = "disable_force_translation_onebox";
// Disables translation when we need to auto-detect the source language (when we don't resolve).
@VisibleForTesting
static final String DISABLE_AUTO_DETECT_TRANSLATION_ONEBOX =
"disable_auto_detect_translation_onebox";
// Disables using the keyboard languages to determine the target language.
private static final String DISABLE_KEYBOARD_LANGUAGES_FOR_TRANSLATION =
"disable_keyboard_languages_for_translation";
// Disables using the accept-languages list to determine the target language.
private static final String DISABLE_ACCEPT_LANGUAGES_FOR_TRANSLATION =
"disable_accept_languages_for_translation";
// Enables usage of English as the target language even when it's the primary UI language.
private static final String ENABLE_ENGLISH_TARGET_TRANSLATION =
"enable_english_target_translation";
// Enables relying on the server to control whether the onebox is actually shown, rather
// than checking if translation is needed client-side based on source/target languages.
@VisibleForTesting
static final String ENABLE_SERVER_CONTROLLED_ONEBOX = "enable_server_controlled_onebox";
/** Hide Contextual Cards data.*/
private static final String HIDE_CONTEXTUAL_CARDS_DATA = "hide_contextual_cards_data";
// Quick Answers.
private static final String ENABLE_QUICK_ANSWERS = "enable_quick_answers";
// Tap triggering suppression.
static final String SUPPRESSION_TAPS = "suppression_taps";
// Enables collection of recent scroll seen/unseen histograms.
// TODO(donnd): remove all supporting code once short-lived data collection is done.
private static final String ENABLE_RECENT_SCROLL_COLLECTION = "enable_recent_scroll_collection";
// Set non-zero to establish an recent scroll suppression threshold for taps.
private static final String RECENT_SCROLL_DURATION_MS = "recent_scroll_duration_ms";
// TODO(donnd): remove all supporting code once short-lived data collection is done.
private static final String SCREEN_TOP_SUPPRESSION_DPS = "screen_top_suppression_dps";
private static final String ENABLE_BAR_OVERLAP_COLLECTION = "enable_bar_overlap_collection";
private static final String BAR_OVERLAP_SUPPRESSION_ENABLED = "enable_bar_overlap_suppression";
// Safety switch for disabling online-detection. Also used to disable detection when running
// tests.
@VisibleForTesting
static final String ONLINE_DETECTION_DISABLED = "disable_online_detection";
private static final String ENABLE_AMP_AS_SEPARATE_TAB = "enable_amp_as_separate_tab";
// Cached values to avoid repeated and redundant JNI operations.
private static Boolean sEnabled;
private static Boolean sDisableSearchTermResolution;
private static Boolean sIsMandatoryPromoEnabled;
private static Integer sMandatoryPromoLimit;
private static Boolean sIsPeekPromoEnabled;
private static Integer sPeekPromoMaxCount;
private static Boolean sIsTranslationEnabled;
private static Boolean sIsForceTranslationOneboxDisabled;
private static Boolean sIsAutoDetectTranslationOneboxDisabled;
private static Boolean sIsAcceptLanguagesForTranslationDisabled;
private static Boolean sIsKeyboardLanguagesForTranslationDisabled;
private static Boolean sIsEnglishTargetTranslationEnabled;
private static Boolean sIsServerControlledOneboxEnabled;
private static Boolean sIsQuickAnswersEnabled;
private static Boolean sIsRecentScrollCollectionEnabled;
private static Integer sRecentScrollDurationMs;
private static Integer sScreenTopSuppressionDps;
private static Boolean sIsBarOverlapCollectionEnabled;
private static Boolean sIsBarOverlapSuppressionEnabled;
private static Integer sSuppressionTaps;
private static Boolean sShouldHideContextualCardsData;
private static Boolean sIsContextualCardsBarIntegrationEnabled;
private static Boolean sIsOnlineDetectionDisabled;
private static Boolean sIsAmpAsSeparateTabEnabled;
/**
* Don't instantiate.
*/
private ContextualSearchFieldTrial() {}
/**
* Checks the current Variations parameters associated with the active group as well as the
* Chrome preference to determine if the service is enabled.
* @return Whether Contextual Search is enabled or not.
*/
public static boolean isEnabled() {
if (sEnabled == null) {
sEnabled = detectEnabled();
}
return sEnabled.booleanValue();
}
private static boolean detectEnabled() {
if (SysUtils.isLowEndDevice()) {
return false;
}
// Allow this user-flippable flag to disable the feature.
if (CommandLine.getInstance().hasSwitch(ChromeSwitches.DISABLE_CONTEXTUAL_SEARCH)) {
return false;
}
// Allow this user-flippable flag to enable the feature.
if (CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CONTEXTUAL_SEARCH)) {
return true;
}
// Allow disabling the feature remotely.
if (getBooleanParam(DISABLED_PARAM)) {
return false;
}
return true;
}
/**
* @return Whether the search term resolution is enabled.
*/
static boolean isSearchTermResolutionEnabled() {
if (sDisableSearchTermResolution == null) {
sDisableSearchTermResolution = getBooleanParam(DISABLE_SEARCH_TERM_RESOLUTION);
}
if (sDisableSearchTermResolution.booleanValue()) {
return false;
}
return true;
}
/**
* @return Whether the Mandatory Promo is enabled.
*/
static boolean isMandatoryPromoEnabled() {
if (sIsMandatoryPromoEnabled == null) {
sIsMandatoryPromoEnabled = getBooleanParam(MANDATORY_PROMO_ENABLED);
}
return sIsMandatoryPromoEnabled.booleanValue();
}
/**
* @return The number of times the Promo should be seen before it becomes mandatory.
*/
static int getMandatoryPromoLimit() {
if (sMandatoryPromoLimit == null) {
sMandatoryPromoLimit = getIntParamValueOrDefault(
MANDATORY_PROMO_LIMIT,
MANDATORY_PROMO_DEFAULT_LIMIT);
}
return sMandatoryPromoLimit.intValue();
}
/**
* @return Whether the Peek Promo is forcibly enabled (used for testing).
*/
static boolean isPeekPromoForced() {
return CommandLine.getInstance().hasSwitch(PEEK_PROMO_FORCED);
}
/**
* @return Whether the Peek Promo is enabled.
*/
static boolean isPeekPromoEnabled() {
if (sIsPeekPromoEnabled == null) {
sIsPeekPromoEnabled = getBooleanParam(PEEK_PROMO_ENABLED);
}
return sIsPeekPromoEnabled.booleanValue();
}
/**
* @return Whether the blacklist is enabled.
*/
static boolean isBlacklistEnabled() {
return getBooleanParam(ENABLE_BLACKLIST);
}
/**
* @return The maximum number of times the Peek Promo should be displayed.
*/
static int getPeekPromoMaxShowCount() {
if (sPeekPromoMaxCount == null) {
sPeekPromoMaxCount = getIntParamValueOrDefault(
PEEK_PROMO_MAX_SHOW_COUNT,
PEEK_PROMO_DEFAULT_MAX_SHOW_COUNT);
}
return sPeekPromoMaxCount.intValue();
}
/**
* @return Whether any translate code is enabled.
*/
static boolean isTranslationEnabled() {
if (sIsTranslationEnabled == null) {
sIsTranslationEnabled = getBooleanParam(ENABLE_TRANSLATION);
}
return sIsTranslationEnabled.booleanValue();
}
/**
* @return Whether forcing a translation Onebox is disabled.
*/
static boolean isForceTranslationOneboxDisabled() {
if (sIsForceTranslationOneboxDisabled == null) {
sIsForceTranslationOneboxDisabled = getBooleanParam(DISABLE_FORCE_TRANSLATION_ONEBOX);
}
return sIsForceTranslationOneboxDisabled.booleanValue();
}
/**
* @return Whether forcing a translation Onebox based on auto-detection of the source language
* is disabled.
*/
static boolean isAutoDetectTranslationOneboxDisabled() {
if (sIsAutoDetectTranslationOneboxDisabled == null) {
sIsAutoDetectTranslationOneboxDisabled = getBooleanParam(
DISABLE_AUTO_DETECT_TRANSLATION_ONEBOX);
}
return sIsAutoDetectTranslationOneboxDisabled.booleanValue();
}
/**
* @return Whether considering accept-languages for translation is disabled.
*/
static boolean isAcceptLanguagesForTranslationDisabled() {
if (sIsAcceptLanguagesForTranslationDisabled == null) {
sIsAcceptLanguagesForTranslationDisabled = getBooleanParam(
DISABLE_ACCEPT_LANGUAGES_FOR_TRANSLATION);
}
return sIsAcceptLanguagesForTranslationDisabled.booleanValue();
}
/**
* @return Whether considering keyboards for translation is disabled.
*/
static boolean isKeyboardLanguagesForTranslationDisabled() {
if (sIsKeyboardLanguagesForTranslationDisabled == null) {
sIsKeyboardLanguagesForTranslationDisabled =
getBooleanParam(DISABLE_KEYBOARD_LANGUAGES_FOR_TRANSLATION);
}
return sIsKeyboardLanguagesForTranslationDisabled.booleanValue();
}
/**
* @return Whether English-target translation should be enabled (default is disabled for 'en').
*/
static boolean isEnglishTargetTranslationEnabled() {
if (sIsEnglishTargetTranslationEnabled == null) {
sIsEnglishTargetTranslationEnabled = getBooleanParam(ENABLE_ENGLISH_TARGET_TRANSLATION);
}
return sIsEnglishTargetTranslationEnabled.booleanValue();
}
/**
* @return Whether relying on server-control of showing the translation one-box is enabled.
*/
static boolean isServerControlledOneboxEnabled() {
if (sIsServerControlledOneboxEnabled == null) {
sIsServerControlledOneboxEnabled = getBooleanParam(ENABLE_SERVER_CONTROLLED_ONEBOX);
}
return sIsServerControlledOneboxEnabled.booleanValue();
}
/**
* @return Whether showing "quick answers" in the Bar is enabled.
*/
static boolean isQuickAnswersEnabled() {
if (sIsQuickAnswersEnabled == null) {
sIsQuickAnswersEnabled = getBooleanParam(ENABLE_QUICK_ANSWERS);
}
return sIsQuickAnswersEnabled.booleanValue();
}
/**
* @return Whether collecting metrics for tap triggering after a scroll is enabled.
*/
static boolean isRecentScrollCollectionEnabled() {
if (sIsRecentScrollCollectionEnabled == null) {
sIsRecentScrollCollectionEnabled = getBooleanParam(ENABLE_RECENT_SCROLL_COLLECTION);
}
return sIsRecentScrollCollectionEnabled.booleanValue();
}
/**
* Gets the duration to use for suppressing Taps after a recent scroll, or {@code 0} if no
* suppression is configured.
* @return The period of time after a scroll when tap triggering is suppressed.
*/
static int getRecentScrollSuppressionDurationMs() {
if (sRecentScrollDurationMs == null) {
sRecentScrollDurationMs = getIntParamValueOrDefault(RECENT_SCROLL_DURATION_MS, 0);
}
return sRecentScrollDurationMs.intValue();
}
/**
* Gets a Y value limit that will suppress a Tap near the top of the screen.
* Any Y value less than the limit will suppress the Tap trigger.
* @return The Y value triggering limit in DPs, a value of zero will not limit.
*/
static int getScreenTopSuppressionDps() {
if (sScreenTopSuppressionDps == null) {
sScreenTopSuppressionDps = getIntParamValueOrDefault(SCREEN_TOP_SUPPRESSION_DPS, 0);
}
return sScreenTopSuppressionDps.intValue();
}
/**
* @return Whether collecting data on Bar overlap is enabled.
*/
static boolean isBarOverlapCollectionEnabled() {
if (sIsBarOverlapCollectionEnabled == null) {
sIsBarOverlapCollectionEnabled = getBooleanParam(ENABLE_BAR_OVERLAP_COLLECTION);
}
return sIsBarOverlapCollectionEnabled.booleanValue();
}
/**
* @return Whether triggering is suppressed by a selection nearly overlapping the normal
* Bar peeking location.
*/
static boolean isBarOverlapSuppressionEnabled() {
if (sIsBarOverlapSuppressionEnabled == null) {
sIsBarOverlapSuppressionEnabled = getBooleanParam(BAR_OVERLAP_SUPPRESSION_ENABLED);
}
return sIsBarOverlapSuppressionEnabled.booleanValue();
}
/**
* @return Whether triggering by Tap is suppressed (through a combination of various signals).
*/
static boolean isTapSuppressionEnabled() {
return getSuppressionTaps() > 0;
}
/**
* @return The suppression threshold, expressed as the number of Taps since the last open where
* we start suppressing the UX on Tap.
*/
static int getSuppressionTaps() {
if (sSuppressionTaps == null) {
sSuppressionTaps = getIntParamValueOrDefault(SUPPRESSION_TAPS, 0);
}
return sSuppressionTaps.intValue();
}
/**
* @return Whether to auto-promote clicks in the AMP carousel into a separate Tab.
*/
static boolean isAmpAsSeparateTabEnabled() {
if (sIsAmpAsSeparateTabEnabled == null) {
sIsAmpAsSeparateTabEnabled = getBooleanParam(ENABLE_AMP_AS_SEPARATE_TAB);
}
return sIsAmpAsSeparateTabEnabled;
}
// TODO(donnd): Remove once bar-integration is fully landed if still unused (native only).
static boolean isContextualCardsBarIntegrationEnabled() {
if (sIsContextualCardsBarIntegrationEnabled == null) {
sIsContextualCardsBarIntegrationEnabled = getBooleanParam(
ChromeSwitches.CONTEXTUAL_SEARCH_CONTEXTUAL_CARDS_BAR_INTEGRATION);
}
return sIsContextualCardsBarIntegrationEnabled;
}
static boolean shouldHideContextualCardsData() {
if (sShouldHideContextualCardsData == null) {
sShouldHideContextualCardsData = getBooleanParam(HIDE_CONTEXTUAL_CARDS_DATA);
}
return sShouldHideContextualCardsData;
}
// --------------------------------------------------------------------------------------------
// Helpers.
// --------------------------------------------------------------------------------------------
/**
* Gets a boolean Finch parameter, assuming the <paramName>="true" format. Also checks for a
* command-line switch with the same name, for easy local testing.
* @param paramName The name of the Finch parameter (or command-line switch) to get a value for.
* @return Whether the Finch param is defined with a value "true", if there's a command-line
* flag present with any value.
*/
private static boolean getBooleanParam(String paramName) {
if (CommandLine.getInstance().hasSwitch(paramName)) {
return true;
}
return TextUtils.equals(ENABLED_VALUE,
VariationsAssociatedData.getVariationParamValue(FIELD_TRIAL_NAME, paramName));
}
/**
* Returns an integer value for a Finch parameter, or the default value if no parameter exists
* in the current configuration. Also checks for a command-line switch with the same name.
* @param paramName The name of the Finch parameter (or command-line switch) to get a value for.
* @param defaultValue The default value to return when there's no param or switch.
* @return An integer value -- either the param or the default.
*/
private static int getIntParamValueOrDefault(String paramName, int defaultValue) {
String value = CommandLine.getInstance().getSwitchValue(paramName);
if (TextUtils.isEmpty(value)) {
value = VariationsAssociatedData.getVariationParamValue(FIELD_TRIAL_NAME, paramName);
}
if (!TextUtils.isEmpty(value)) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
return defaultValue;
}
}