1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.contextualsearch;
6 
7 import android.text.TextUtils;
8 
9 import androidx.annotation.IntDef;
10 import androidx.annotation.VisibleForTesting;
11 
12 import org.chromium.base.CommandLine;
13 import org.chromium.base.SysUtils;
14 import org.chromium.chrome.browser.flags.ChromeFeatureList;
15 import org.chromium.chrome.browser.flags.ChromeSwitches;
16 import org.chromium.components.variations.VariationsAssociatedData;
17 
18 import java.lang.annotation.Retention;
19 import java.lang.annotation.RetentionPolicy;
20 
21 /**
22  * Provides Field Trial support for the Contextual Search application within Chrome for Android.
23  */
24 public class ContextualSearchFieldTrial {
25     private static final String FIELD_TRIAL_NAME = "ContextualSearch";
26     private static final String DISABLED_PARAM = "disabled";
27     private static final String ENABLED_VALUE = "true";
28 
29     //==========================================================================================
30     // Related Searches FieldTrial and parameter names.
31     //==========================================================================================
32     // Params used elsewhere but gathered here since they may be present in FieldTrial configs.
33     static final String RELATED_SEARCHES_NEEDS_URL_PARAM_NAME = "needs_url";
34     static final String RELATED_SEARCHES_NEEDS_CONTENT_PARAM_NAME = "needs_content";
35     // A comma-separated list of lower-case ISO 639 language codes.
36     static final String RELATED_SEARCHES_LANGUAGE_ALLOWLIST_PARAM_NAME = "language_allowlist";
37     private static final String RELATED_SEARCHES_CONFIG_STAMP_PARAM_NAME = "stamp";
38 
39     // Deprecated.
40     private static final int MANDATORY_PROMO_DEFAULT_LIMIT = 10;
41 
42     // Cached values to avoid repeated and redundant JNI operations.
43     private static Boolean sEnabled;
44     private static Boolean[] sSwitches = new Boolean[ContextualSearchSwitch.NUM_ENTRIES];
45     private static Integer[] sSettings = new Integer[ContextualSearchSetting.NUM_ENTRIES];
46 
47     // SWITCHES
48     // TODO(donnd): remove all supporting code once short-lived data collection is done.
49     @IntDef({ContextualSearchSwitch.IS_TRANSLATION_DISABLED,
50             ContextualSearchSwitch.IS_ONLINE_DETECTION_DISABLED,
51             ContextualSearchSwitch.IS_SEARCH_TERM_RESOLUTION_DISABLED,
52             ContextualSearchSwitch.IS_MANDATORY_PROMO_ENABLED,
53             ContextualSearchSwitch.IS_ENGLISH_TARGET_TRANSLATION_ENABLED,
54             ContextualSearchSwitch.IS_BAR_OVERLAP_COLLECTION_ENABLED,
55             ContextualSearchSwitch.IS_BAR_OVERLAP_SUPPRESSION_ENABLED,
56             ContextualSearchSwitch.IS_WORD_EDGE_SUPPRESSION_ENABLED,
57             ContextualSearchSwitch.IS_SHORT_WORD_SUPPRESSION_ENABLED,
58             ContextualSearchSwitch.IS_NOT_LONG_WORD_SUPPRESSION_ENABLED,
59             ContextualSearchSwitch.IS_NOT_AN_ENTITY_SUPPRESSION_ENABLED,
60             ContextualSearchSwitch.IS_ENGAGEMENT_SUPPRESSION_ENABLED,
61             ContextualSearchSwitch.IS_SHORT_TEXT_RUN_SUPPRESSION_ENABLED,
62             ContextualSearchSwitch.IS_SMALL_TEXT_SUPPRESSION_ENABLED,
63             ContextualSearchSwitch.IS_AMP_AS_SEPARATE_TAB_DISABLED,
64             ContextualSearchSwitch.IS_SEND_HOME_COUNTRY_DISABLED,
65             ContextualSearchSwitch.IS_PAGE_CONTENT_NOTIFICATION_DISABLED,
66             ContextualSearchSwitch.IS_UKM_RANKER_LOGGING_DISABLED,
67             ContextualSearchSwitch.IS_CONTEXTUAL_SEARCH_ML_TAP_SUPPRESSION_ENABLED,
68             ContextualSearchSwitch.IS_CONTEXTUAL_SEARCH_SECOND_TAP_ML_OVERRIDE_ENABLED,
69             ContextualSearchSwitch.IS_CONTEXTUAL_SEARCH_TAP_DISABLE_OVERRIDE_ENABLED,
70             ContextualSearchSwitch.IS_SEND_BASE_PAGE_URL_DISABLED})
71     @Retention(RetentionPolicy.SOURCE)
72     /**
73      * Boolean Switch values that are backed by either a Feature or a Variations parameter.
74      * Values are used for indexing ContextualSearchSwitchNames - should start from 0 and can't
75      * have gaps.
76      */
77     @interface ContextualSearchSwitch {
78         /**
79          * @deprecated
80          * Whether all translate code is disabled (master switch, needed to disable all translate
81          * code for Contextual Search in case of an emergency).
82          */
83         int IS_TRANSLATION_DISABLED = 0;
84         /**
85          * Whether detection of device-online should be disabled (default false).
86          * (safety switch for disabling online-detection also used to disable detection when
87          * running tests).
88          */
89         // TODO(donnd): Convert to test-only after launch and we have confidence it's robust.
90         int IS_ONLINE_DETECTION_DISABLED = 1;
91 
92         int IS_SEARCH_TERM_RESOLUTION_DISABLED = 2;
93         int IS_MANDATORY_PROMO_ENABLED = 3;
94 
95         /**
96          * Whether English-target translation should be enabled (default is disabled for 'en').
97          * Enables usage of English as the target language even when it's the primary UI language.
98          */
99         int IS_ENGLISH_TARGET_TRANSLATION_ENABLED = 4;
100         /** Whether collecting data on Bar overlap is enabled. */
101         int IS_BAR_OVERLAP_COLLECTION_ENABLED = 5;
102         /**
103          * Whether triggering is suppressed by a selection nearly overlapping the normal
104          * Bar peeking location.
105          */
106         int IS_BAR_OVERLAP_SUPPRESSION_ENABLED = 6;
107         /** Whether triggering is suppressed by a tap that's near the edge of a word. */
108         int IS_WORD_EDGE_SUPPRESSION_ENABLED = 7;
109         /** Whether triggering is suppressed by a tap that's in a short word. */
110         int IS_SHORT_WORD_SUPPRESSION_ENABLED = 8;
111         /** Whether triggering is suppressed by a tap that's not in a long word. */
112         int IS_NOT_LONG_WORD_SUPPRESSION_ENABLED = 9;
113         /** Whether triggering is suppressed for a tap that's not on an entity. */
114         int IS_NOT_AN_ENTITY_SUPPRESSION_ENABLED = 10;
115         /** Whether triggering is suppressed due to lack of engagement with the feature. */
116         int IS_ENGAGEMENT_SUPPRESSION_ENABLED = 11;
117         /** Whether triggering is suppressed for a tap that has a short element run-length. */
118         int IS_SHORT_TEXT_RUN_SUPPRESSION_ENABLED = 12;
119         /** Whether triggering is suppressed for a tap on small-looking text. */
120         int IS_SMALL_TEXT_SUPPRESSION_ENABLED = 13;
121         /**
122          * Whether to disable auto-promotion of clicks in the AMP carousel into a
123          * separate Tab.
124          */
125         int IS_AMP_AS_SEPARATE_TAB_DISABLED = 14;
126         /** Whether sending the "home country" to Google is disabled. */
127         int IS_SEND_HOME_COUNTRY_DISABLED = 15;
128         /**
129          * Whether sending the page content notifications to observers (e.g. icing for
130          * conversational search) is disabled.
131          */
132         int IS_PAGE_CONTENT_NOTIFICATION_DISABLED = 16;
133         /** Whether logging for Machine Learning is disabled. */
134         int IS_UKM_RANKER_LOGGING_DISABLED = 17;
135         /** Whether or not ML-based Tap suppression is enabled. */
136         int IS_CONTEXTUAL_SEARCH_ML_TAP_SUPPRESSION_ENABLED = 18;
137         /** Whether or not to override an ML-based Tap suppression on a second tap. */
138         int IS_CONTEXTUAL_SEARCH_SECOND_TAP_ML_OVERRIDE_ENABLED = 19;
139         /**
140          * Whether or not to override tap-disable for users that have never opened the
141          * panel.
142          */
143         int IS_CONTEXTUAL_SEARCH_TAP_DISABLE_OVERRIDE_ENABLED = 20;
144         /** Whether sending the URL of the page viewed by the user is disabled. */
145         int IS_SEND_BASE_PAGE_URL_DISABLED = 21;
146 
147         int NUM_ENTRIES = 22;
148     }
149 
150     @VisibleForTesting
151     static final String ONLINE_DETECTION_DISABLED = "disable_online_detection";
152     @VisibleForTesting
153     static final String TRANSLATION_DISABLED = "disable_translation";
154 
155     // Indexed by ContextualSearchSwitch
156     private static final String[] ContextualSearchSwitchNames = {
157             TRANSLATION_DISABLED, // IS_TRANSLATION_DISABLED
158             ONLINE_DETECTION_DISABLED, // IS_ONLINE_DETECTION_DISABLED
159             "disable_search_term_resolution", // DISABLE_SEARCH_TERM_RESOLUTION
160             "mandatory_promo_enabled", // IS_MANDATORY_PROMO_ENABLED
161             "enable_english_target_translation", // IS_ENGLISH_TARGET_TRANSLATION_ENABLED
162             "enable_bar_overlap_collection", // IS_BAR_OVERLAP_COLLECTION_ENABLED
163             "enable_bar_overlap_suppression", // IS_BAR_OVERLAP_SUPPRESSION_ENABLED
164             "enable_word_edge_suppression", // IS_WORD_EDGE_SUPPRESSION_ENABLED
165             "enable_short_word_suppression", //  IS_SHORT_WORD_SUPPRESSION_ENABLED
166             "enable_not_long_word_suppression", // IS_NOT_LONG_WORD_SUPPRESSION_ENABLED
167             "enable_not_an_entity_suppression", //  IS_NOT_AN_ENTITY_SUPPRESSION_ENABLED
168             "enable_engagement_suppression", // IS_ENGAGEMENT_SUPPRESSION_ENABLED
169             "enable_short_text_run_suppression", // IS_SHORT_TEXT_RUN_SUPPRESSION_ENABLED
170             "enable_small_text_suppression", // IS_SMALL_TEXT_SUPPRESSION_ENABLED
171             "disable_amp_as_separate_tab", // IS_AMP_AS_SEPARATE_TAB_DISABLED
172             "disable_send_home_country", //  IS_SEND_HOME_COUNTRY_DISABLED
173             "disable_page_content_notification", // IS_PAGE_CONTENT_NOTIFICATION_DISABLED
174             "disable_ukm_ranker_logging", // IS_UKM_RANKER_LOGGING_DISABLED
175             ChromeFeatureList.CONTEXTUAL_SEARCH_ML_TAP_SUPPRESSION, // (related to Chrome Feature)
176             ChromeFeatureList.CONTEXTUAL_SEARCH_SECOND_TAP, // (related to Chrome Feature)
177             ChromeFeatureList.CONTEXTUAL_SEARCH_TAP_DISABLE_OVERRIDE, // (related to Chrome Feature)
178             "disable_send_url" // IS_SEND_BASE_PAGE_URL_DISABLED
179     };
180 
181     @IntDef({ContextualSearchSetting.MANDATORY_PROMO_LIMIT,
182             ContextualSearchSetting.SCREEN_TOP_SUPPRESSION_DPS,
183             ContextualSearchSetting.MINIMUM_SELECTION_LENGTH,
184             ContextualSearchSetting.WAIT_AFTER_TAP_DELAY_MS,
185             ContextualSearchSetting.TAP_DURATION_THRESHOLD_MS,
186             ContextualSearchSetting.RECENT_SCROLL_DURATION_MS})
187     @Retention(RetentionPolicy.SOURCE)
188     /**
189      * These are integer Setting values that are backed by a Variation Param.
190      * Values are used for indexing ContextualSearchSwitchStrings - should start from 0 and can't
191      * have gaps.
192      */
193     @interface ContextualSearchSetting {
194         /** The number of times the Promo should be seen before it becomes mandatory. */
195         int MANDATORY_PROMO_LIMIT = 0;
196         /**
197          * A Y value limit that will suppress a Tap near the top of the screen.
198          * (any Y value less than the limit will suppress the Tap trigger).
199          */
200         int SCREEN_TOP_SUPPRESSION_DPS = 1;
201         /** The minimum valid selection length. */
202         int MINIMUM_SELECTION_LENGTH = 2;
203         /**
204          * An amount to delay after a Tap gesture is recognized, in case some user gesture
205          * immediately follows that would prevent the UI from showing.
206          * The classic example is a scroll, which might be a signal that the previous tap was
207          * accidental.
208          */
209         int WAIT_AFTER_TAP_DELAY_MS = 3;
210         /**
211          * A threshold for the duration of a tap gesture for categorization as brief or
212          * lengthy (the maximum amount of time in milliseconds for a tap gesture that's still
213          * considered a very brief duration tap).
214          */
215         int TAP_DURATION_THRESHOLD_MS = 4;
216         /**
217          * The duration to use for suppressing Taps after a recent scroll, or {@code 0} if no
218          * suppression is configured (the period of time after a scroll when tap triggering is
219          * suppressed).
220          */
221         int RECENT_SCROLL_DURATION_MS = 5;
222 
223         int NUM_ENTRIES = 6;
224     }
225 
226     // Indexed by ContextualSearchSetting
227     private static final String[] ContextualSearchSettingNames = {
228             "mandatory_promo_limit", // MANDATORY_PROMO_LIMIT
229             "screen_top_suppression_dps", // SCREEN_TOP_SUPPRESSION_DPS
230             "minimum_selection_length", // MINIMUM_SELECTION_LENGTH
231             "wait_after_tap_delay_ms", // WAIT_AFTER_TAP_DELAY_MS
232             "tap_duration_threshold_ms", // TAP_DURATION_THRESHOLD_MS
233             "recent_scroll_duration_ms" // RECENT_SCROLL_DURATION_MS
234     };
235 
ContextualSearchFieldTrial()236     private ContextualSearchFieldTrial() {
237         assert ContextualSearchSwitchNames.length == ContextualSearchSwitch.NUM_ENTRIES;
238         assert ContextualSearchSettingNames.length == ContextualSearchSetting.NUM_ENTRIES;
239     }
240 
241     /**
242      * Current Variations parameters associated with the ContextualSearch Field Trial or a
243      * Chrome Feature to determine if the service is enabled
244      * (whether Contextual Search is enabled or not).
245      */
isEnabled()246     public static boolean isEnabled() {
247         if (sEnabled == null) sEnabled = detectEnabled();
248         return sEnabled.booleanValue();
249     }
250 
getSwitch(@ontextualSearchSwitch int value)251     static boolean getSwitch(@ContextualSearchSwitch int value) {
252         if (sSwitches[value] == null) {
253             switch (value) {
254                 case ContextualSearchSwitch.IS_CONTEXTUAL_SEARCH_ML_TAP_SUPPRESSION_ENABLED:
255                 case ContextualSearchSwitch.IS_CONTEXTUAL_SEARCH_SECOND_TAP_ML_OVERRIDE_ENABLED:
256                 case ContextualSearchSwitch.IS_CONTEXTUAL_SEARCH_TAP_DISABLE_OVERRIDE_ENABLED:
257                     sSwitches[value] =
258                             ChromeFeatureList.isEnabled(ContextualSearchSwitchNames[value]);
259                     break;
260                 default:
261                     assert !TextUtils.isEmpty(ContextualSearchSwitchNames[value]);
262                     sSwitches[value] = getBooleanParam(ContextualSearchSwitchNames[value]);
263             }
264         }
265         return sSwitches[value].booleanValue();
266     }
267 
getValue(@ontextualSearchSetting int value)268     static int getValue(@ContextualSearchSetting int value) {
269         if (sSettings[value] == null) {
270             sSettings[value] = getIntParamValueOrDefault(ContextualSearchSettingNames[value],
271                     value == ContextualSearchSetting.MANDATORY_PROMO_LIMIT
272                             ? MANDATORY_PROMO_DEFAULT_LIMIT
273                             : 0);
274         }
275         return sSettings[value].intValue();
276     }
277 
278     /**
279      * Gets the "stamp" parameter from the RelatedSearches FieldTrial feature.
280      * @return The stamp parameter from the feature. If no stamp param is present then an empty
281      *         string is returned.
282      */
getRelatedSearchesExperiementConfigurationStamp()283     static String getRelatedSearchesExperiementConfigurationStamp() {
284         return getRelatedSearchesParam(RELATED_SEARCHES_CONFIG_STAMP_PARAM_NAME);
285     }
286 
287     /**
288      * Gets the given parameter from the RelatedSearches FieldTrial feature.
289      * @param paramName The name of the parameter to get.
290      * @return The value of the parameter from the feature. If no param is present then an empty
291      *         string is returned.
292      */
getRelatedSearchesParam(String paramName)293     static String getRelatedSearchesParam(String paramName) {
294         return ChromeFeatureList.getFieldTrialParamByFeature(
295                 ChromeFeatureList.RELATED_SEARCHES, paramName);
296     }
297 
298     /**
299      * Determines whether the specified parameter is present and enabled in the RelatedSearches
300      * Feature.
301      * @param relatedSearchesParamName The name of the param to get from the Feature.
302      * @return Whether the given parameter is enabled or not (has a value of "true").
303      */
isRelatedSearchesParamEnabled(String relatedSearchesParamName)304     static boolean isRelatedSearchesParamEnabled(String relatedSearchesParamName) {
305         return ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
306                 ChromeFeatureList.RELATED_SEARCHES, relatedSearchesParamName, false);
307     }
308 
309     // --------------------------------------------------------------------------------------------
310     // Helpers.
311     // --------------------------------------------------------------------------------------------
312 
detectEnabled()313     private static boolean detectEnabled() {
314         if (SysUtils.isLowEndDevice()) return false;
315 
316         // Allow this user-flippable flag to disable the feature.
317         if (CommandLine.getInstance().hasSwitch(ChromeSwitches.DISABLE_CONTEXTUAL_SEARCH)) {
318             return false;
319         }
320 
321         // Allow this user-flippable flag to enable the feature.
322         if (CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CONTEXTUAL_SEARCH)) {
323             return true;
324         }
325 
326         // Allow disabling the feature remotely.
327         if (getBooleanParam(DISABLED_PARAM)) return false;
328 
329         return true;
330     }
331 
332     /**
333      * Gets a boolean Finch parameter, assuming the <paramName>="true" format.  Also checks for
334      * a command-line switch with the same name, for easy local testing.
335      * @param paramName The name of the Finch parameter (or command-line switch) to get a value
336      *                  for.
337      * @return Whether the Finch param is defined with a value "true", if there's a command-line
338      *         flag present with any value.
339      */
getBooleanParam(String paramName)340     private static boolean getBooleanParam(String paramName) {
341         if (CommandLine.getInstance().hasSwitch(paramName)) {
342             return true;
343         }
344         return TextUtils.equals(ENABLED_VALUE,
345                 VariationsAssociatedData.getVariationParamValue(FIELD_TRIAL_NAME, paramName));
346     }
347 
348     /**
349      * Returns an integer value for a Finch parameter, or the default value if no parameter
350      * exists in the current configuration.  Also checks for a command-line switch with the same
351      * name.
352      * @param paramName The name of the Finch parameter (or command-line switch) to get a value
353      *                  for.
354      * @param defaultValue The default value to return when there's no param or switch.
355      * @return An integer value -- either the param or the default.
356      */
getIntParamValueOrDefault(String paramName, int defaultValue)357     private static int getIntParamValueOrDefault(String paramName, int defaultValue) {
358         String value = CommandLine.getInstance().getSwitchValue(paramName);
359         if (TextUtils.isEmpty(value)) {
360             value = VariationsAssociatedData.getVariationParamValue(FIELD_TRIAL_NAME, paramName);
361         }
362         if (!TextUtils.isEmpty(value)) {
363             try {
364                 return Integer.parseInt(value);
365             } catch (NumberFormatException e) {
366                 return defaultValue;
367             }
368         }
369 
370         return defaultValue;
371     }
372 }
373