| // 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.content.Context; |
| import android.net.Uri; |
| import android.telephony.TelephonyManager; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| |
| import org.chromium.base.CollectionUtil; |
| import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel; |
| import org.chromium.chrome.browser.contextualsearch.ContextualSearchFieldTrial.ContextualSearchSetting; |
| import org.chromium.chrome.browser.contextualsearch.ContextualSearchFieldTrial.ContextualSearchSwitch; |
| import org.chromium.chrome.browser.contextualsearch.ContextualSearchSelectionController.SelectionType; |
| import org.chromium.chrome.browser.flags.ChromeFeatureList; |
| import org.chromium.chrome.browser.preferences.ChromePreferenceKeys; |
| import org.chromium.chrome.browser.preferences.SharedPreferencesManager; |
| import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory; |
| import org.chromium.chrome.browser.signin.services.UnifiedConsentServiceBridge; |
| import org.chromium.chrome.browser.version.ChromeVersionInfo; |
| import org.chromium.components.embedder_support.util.UrlConstants; |
| |
| import java.net.URL; |
| import java.util.HashSet; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Handles business decision policy for the {@code ContextualSearchManager}. |
| */ |
| class ContextualSearchPolicy { |
| private static final Pattern CONTAINS_WHITESPACE_PATTERN = Pattern.compile("\\s"); |
| private static final String DOMAIN_GOOGLE = "google"; |
| private static final String PATH_AMP = "/amp/"; |
| private static final int REMAINING_NOT_APPLICABLE = -1; |
| private static final int TAP_TRIGGERED_PROMO_LIMIT = 50; |
| // Related Searches "stamp" building and accessing details. |
| private static final String RELATED_SEARCHES_STAMP_VERSION = "1"; |
| private static final String RELATED_SEARCHES_EXPERIMENT_RECIPE_STAGE = "R"; |
| private static final String RELATED_SEARCHES_NO_EXPERIMENT = "n"; |
| private static final String RELATED_SEARCHES_LANGUAGE_RESTRICTION = "l"; |
| private static final String RELATED_SEARCHES_DARK_LAUNCH = "d"; |
| private static final String NO_EXPERIMENT_STAMP = RELATED_SEARCHES_STAMP_VERSION |
| + RELATED_SEARCHES_EXPERIMENT_RECIPE_STAGE + RELATED_SEARCHES_NO_EXPERIMENT; |
| |
| // TODO(donnd): remove -- deprecated. |
| private static final HashSet<String> PREDOMINENTLY_ENGLISH_SPEAKING_COUNTRIES = |
| CollectionUtil.newHashSet("GB", "US"); |
| |
| private final SharedPreferencesManager mPreferencesManager; |
| private final ContextualSearchSelectionController mSelectionController; |
| private ContextualSearchNetworkCommunicator mNetworkCommunicator; |
| private ContextualSearchPanel mSearchPanel; |
| |
| // Members used only for testing purposes. |
| private boolean mDidOverrideDecidedStateForTesting; |
| private boolean mDecidedStateForTesting; |
| private Integer mTapTriggeredPromoLimitForTesting; |
| |
| /** |
| * ContextualSearchPolicy constructor. |
| */ |
| public ContextualSearchPolicy(ContextualSearchSelectionController selectionController, |
| ContextualSearchNetworkCommunicator networkCommunicator) { |
| mPreferencesManager = SharedPreferencesManager.getInstance(); |
| |
| mSelectionController = selectionController; |
| mNetworkCommunicator = networkCommunicator; |
| if (selectionController != null) selectionController.setPolicy(this); |
| } |
| |
| /** |
| * Sets the handle to the ContextualSearchPanel. |
| * @param panel The ContextualSearchPanel. |
| */ |
| public void setContextualSearchPanel(ContextualSearchPanel panel) { |
| mSearchPanel = panel; |
| } |
| |
| /** |
| * @return The number of additional times to show the promo on tap, 0 if it should not be shown, |
| * or a negative value if the counter has been disabled or the user has accepted |
| * the promo. |
| */ |
| int getPromoTapsRemaining() { |
| if (!isUserUndecided()) return REMAINING_NOT_APPLICABLE; |
| |
| // Return a non-negative value if opt-out promo counter is enabled, and there's a limit. |
| DisableablePromoTapCounter counter = getPromoTapCounter(); |
| if (counter.isEnabled()) { |
| int limit = getPromoTapTriggeredLimit(); |
| if (limit >= 0) return Math.max(0, limit - counter.getCount()); |
| } |
| |
| return REMAINING_NOT_APPLICABLE; |
| } |
| |
| private int getPromoTapTriggeredLimit() { |
| return mTapTriggeredPromoLimitForTesting != null |
| ? mTapTriggeredPromoLimitForTesting.intValue() |
| : TAP_TRIGGERED_PROMO_LIMIT; |
| } |
| |
| /** |
| * @return the {@link DisableablePromoTapCounter}. |
| */ |
| DisableablePromoTapCounter getPromoTapCounter() { |
| return DisableablePromoTapCounter.getInstance(mPreferencesManager); |
| } |
| |
| /** |
| * @return Whether a Tap gesture is currently supported as a trigger for the feature. |
| */ |
| boolean isTapSupported() { |
| if (isTapDisabledDueToLongpress()) return false; |
| |
| return (!isUserUndecided() |
| || ContextualSearchFieldTrial.getSwitch( |
| ContextualSearchSwitch |
| .IS_CONTEXTUAL_SEARCH_TAP_DISABLE_OVERRIDE_ENABLED)) |
| ? true |
| : (getPromoTapsRemaining() != 0); |
| } |
| |
| /** |
| * @return whether or not the Contextual Search Result should be preloaded before the user |
| * explicitly interacts with the feature. |
| */ |
| boolean shouldPrefetchSearchResult() { |
| if (isMandatoryPromoAvailable() |
| || !PrivacyPreferencesManagerImpl.getInstance().getNetworkPredictionEnabled()) { |
| return false; |
| } |
| |
| // We never preload unless we have sent page context (done through a Resolve request). |
| // Only some gestures can resolve, and only when resolve privacy rules are met. |
| return (mSelectionController.getSelectionType() == SelectionType.TAP |
| || mSelectionController.getSelectionType() |
| == SelectionType.RESOLVING_LONG_PRESS) |
| && shouldPreviousGestureResolve(); |
| } |
| |
| /** |
| * Determines whether the current gesture can trigger a resolve request to use page context. |
| * This only checks the gesture, not privacy status -- {@see #shouldPreviousGestureResolve}. |
| */ |
| boolean isResolvingGesture() { |
| return (mSelectionController.getSelectionType() == SelectionType.TAP |
| && !isLiteralSearchTapEnabled()) |
| || mSelectionController.getSelectionType() == SelectionType.RESOLVING_LONG_PRESS; |
| } |
| |
| /** |
| * Determines whether the gesture being processed is allowed to resolve. |
| * TODO(donnd): rename to be more descriptive. Maybe isGestureAllowedToResolve? |
| * @return Whether the previous gesture should resolve. |
| */ |
| boolean shouldPreviousGestureResolve() { |
| if (isMandatoryPromoAvailable() |
| || ContextualSearchFieldTrial.getSwitch( |
| ContextualSearchSwitch.IS_SEARCH_TERM_RESOLUTION_DISABLED)) { |
| return false; |
| } |
| |
| // The user must have decided on privacy to resolve page content on HTTPS. |
| return !isUserUndecided() || doesLegacyHttpPolicyApply(); |
| } |
| |
| /** @return Whether a long-press gesture can resolve. */ |
| boolean canResolveLongpress() { |
| return ChromeFeatureList.isEnabled(ChromeFeatureList.CONTEXTUAL_SEARCH_LONGPRESS_RESOLVE) |
| || ChromeFeatureList.isEnabled(ChromeFeatureList.CONTEXTUAL_SEARCH_TRANSLATIONS); |
| } |
| |
| /** |
| * Returns whether surrounding context can be accessed by other systems or not. |
| * @return Whether surroundings are available. |
| */ |
| boolean canSendSurroundings() { |
| if (mDidOverrideDecidedStateForTesting) return mDecidedStateForTesting; |
| |
| // The user must have decided on privacy to send page content on HTTPS. |
| return !isUserUndecided() || doesLegacyHttpPolicyApply(); |
| } |
| |
| /** |
| * @return Whether the Mandatory Promo is enabled. |
| */ |
| boolean isMandatoryPromoAvailable() { |
| if (!isUserUndecided() |
| || !ContextualSearchFieldTrial.getSwitch( |
| ContextualSearchSwitch.IS_MANDATORY_PROMO_ENABLED)) { |
| return false; |
| } |
| |
| return getPromoOpenCount() >= ContextualSearchFieldTrial.getValue( |
| ContextualSearchSetting.MANDATORY_PROMO_LIMIT); |
| } |
| |
| /** |
| * @return Whether the Opt-out promo is available to be shown in any panel. |
| */ |
| boolean isPromoAvailable() { |
| return isUserUndecided(); |
| } |
| |
| /** |
| * Registers that a tap has taken place by incrementing tap-tracking counters. |
| */ |
| void registerTap() { |
| if (isPromoAvailable()) { |
| DisableablePromoTapCounter promoTapCounter = getPromoTapCounter(); |
| // Bump the counter only when it is still enabled. |
| if (promoTapCounter.isEnabled()) promoTapCounter.increment(); |
| } |
| int tapsSinceOpen = mPreferencesManager.incrementInt( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_TAP_SINCE_OPEN_COUNT); |
| if (isUserUndecided()) { |
| ContextualSearchUma.logTapsSinceOpenForUndecided(tapsSinceOpen); |
| } else { |
| ContextualSearchUma.logTapsSinceOpenForDecided(tapsSinceOpen); |
| } |
| mPreferencesManager.incrementInt(ChromePreferenceKeys.CONTEXTUAL_SEARCH_ALL_TIME_TAP_COUNT); |
| } |
| |
| /** |
| * Updates all the counters to account for an open-action on the panel. |
| */ |
| void updateCountersForOpen() { |
| // Always completely reset the tap counters that accumulate only since the last open. |
| mPreferencesManager.writeInt( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_TAP_SINCE_OPEN_COUNT, 0); |
| mPreferencesManager.writeInt( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_TAP_SINCE_OPEN_QUICK_ANSWER_COUNT, 0); |
| |
| // Disable the "promo tap" counter, but only if we're using the Opt-out onboarding. |
| // For Opt-in, we never disable the promo tap counter. |
| if (isPromoAvailable()) { |
| getPromoTapCounter().disable(); |
| |
| // Bump the total-promo-opens counter. |
| int count = mPreferencesManager.incrementInt( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_PROMO_OPEN_COUNT); |
| ContextualSearchUma.logPromoOpenCount(count); |
| } |
| mPreferencesManager.incrementInt( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_ALL_TIME_OPEN_COUNT); |
| } |
| |
| /** |
| * Updates Tap counters to account for a quick-answer caption shown on the panel. |
| * @param wasActivatedByTap Whether the triggering gesture was a Tap or not. |
| * @param doesAnswer Whether the caption is considered an answer rather than just |
| * informative. |
| */ |
| void updateCountersForQuickAnswer(boolean wasActivatedByTap, boolean doesAnswer) { |
| if (wasActivatedByTap && doesAnswer) { |
| mPreferencesManager.incrementInt( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_TAP_SINCE_OPEN_QUICK_ANSWER_COUNT); |
| mPreferencesManager.incrementInt( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_ALL_TIME_TAP_QUICK_ANSWER_COUNT); |
| } |
| } |
| |
| /** |
| * @return Whether a verbatim request should be made for the given base page, assuming there |
| * is no existing request. |
| */ |
| boolean shouldCreateVerbatimRequest() { |
| @SelectionType |
| int selectionType = mSelectionController.getSelectionType(); |
| return (mSelectionController.getSelectedText() != null |
| && (selectionType == SelectionType.LONG_PRESS || !shouldPreviousGestureResolve())); |
| } |
| |
| /** |
| * @return whether the experiment that causes a tap gesture to trigger a literal search for the |
| * selection (rather than sending context to resolve a search term) is enabled. |
| */ |
| boolean isLiteralSearchTapEnabled() { |
| return ChromeFeatureList.isEnabled(ChromeFeatureList.CONTEXTUAL_SEARCH_LITERAL_SEARCH_TAP); |
| } |
| |
| /** @return whether Tap is disabled due to the longpress experiment. */ |
| private boolean isTapDisabledDueToLongpress() { |
| return canResolveLongpress() && !isLiteralSearchTapEnabled(); |
| } |
| |
| /** |
| * Determines the policy for sending page content when on plain HTTP pages. |
| * Checks a Feature to use our legacy HTTP policy instead of treating HTTP just like HTTPS. |
| * See https://crbug.com/1129969 for details. |
| * @return whether the legacy policy for plain HTTP pages currently applies. |
| */ |
| private boolean doesLegacyHttpPolicyApply() { |
| if (!isBasePageHTTP(mNetworkCommunicator.getBasePageUrl())) return false; |
| |
| // Check if the legacy behavior is enabled through a feature. |
| return ChromeFeatureList.isEnabled(ChromeFeatureList.CONTEXTUAL_SEARCH_LEGACY_HTTP_POLICY); |
| } |
| |
| /** |
| * Determines whether an error from a search term resolution request should |
| * be shown to the user, or not. |
| */ |
| boolean shouldShowErrorCodeInBar() { |
| // Builds with lots of real users should not see raw error codes. |
| return !(ChromeVersionInfo.isStableBuild() || ChromeVersionInfo.isBetaBuild()); |
| } |
| |
| /** |
| * Logs the current user's state, including preference, tap and open counters, etc. |
| */ |
| void logCurrentState() { |
| ContextualSearchUma.logPreferenceState(); |
| ContextualSearchUma.logRelatedSearchesPermissionsForAllUsers( |
| hasSendUrlPermissions(), canSendSurroundings()); |
| |
| // Log the number of promo taps remaining. |
| int promoTapsRemaining = getPromoTapsRemaining(); |
| if (promoTapsRemaining >= 0) ContextualSearchUma.logPromoTapsRemaining(promoTapsRemaining); |
| |
| // Also log the total number of taps before opening the promo, even for those |
| // that are no longer tap limited. That way we'll know the distribution of the |
| // number of taps needed before opening the promo. |
| DisableablePromoTapCounter promoTapCounter = getPromoTapCounter(); |
| boolean wasOpened = !promoTapCounter.isEnabled(); |
| int count = promoTapCounter.getCount(); |
| if (wasOpened) { |
| ContextualSearchUma.logPromoTapsBeforeFirstOpen(count); |
| } else { |
| ContextualSearchUma.logPromoTapsForNeverOpened(count); |
| } |
| } |
| |
| /** |
| * Logs details about the Search Term Resolution. |
| * Should only be called when a search term has been resolved. |
| * @param searchTerm The Resolved Search Term. |
| */ |
| void logSearchTermResolutionDetails(String searchTerm) { |
| // Only log for decided users so the data reflect fully-enabled behavior. |
| // Otherwise we'll get skewed data; more HTTP pages than HTTPS (since those don't resolve), |
| // and it's also possible that public pages, e.g. news, have more searches for multi-word |
| // entities like people. |
| if (!isUserUndecided()) { |
| URL url = mNetworkCommunicator.getBasePageUrl(); |
| ContextualSearchUma.logBasePageProtocol(isBasePageHTTP(url)); |
| boolean isSingleWord = !CONTAINS_WHITESPACE_PATTERN.matcher(searchTerm.trim()).find(); |
| ContextualSearchUma.logSearchTermResolvedWords(isSingleWord); |
| } |
| } |
| |
| /** |
| * Logs whether the current user is qualified to do Related Searches requests. This does not |
| * check if Related Searches is actually enabled for the current user, only whether they are |
| * qualified. We use this to gauge whether each group has a balanced number of qualified users. |
| * Can be logged multiple times since we'll just look at the user-count of this histogram. |
| * @param basePageLanguage The language of the page, to check if supported by the server. |
| */ |
| void logRelatedSearchesQualifiedUsers(String basePageLanguage) { |
| if (isQualifiedForRelatedSearches(basePageLanguage)) { |
| ContextualSearchUma.logRelatedSearchesQualifiedUsers(); |
| } |
| } |
| |
| /** |
| * Whether this request should include sending the URL of the base page to the server. |
| * Several conditions are checked to make sure it's OK to send the URL, but primarily this is |
| * based on whether the user has checked the setting for "Make searches and browsing better". |
| * @return {@code true} if the URL should be sent. |
| */ |
| boolean doSendBasePageUrl() { |
| if (isUserUndecided()) return false; |
| |
| // Check whether there is a Field Trial setting preventing us from sending the page URL. |
| if (ContextualSearchFieldTrial.getSwitch( |
| ContextualSearchSwitch.IS_SEND_BASE_PAGE_URL_DISABLED)) { |
| return false; |
| } |
| |
| // Ensure that the default search provider is Google. |
| if (!TemplateUrlServiceFactory.get().isDefaultSearchEngineGoogle()) return false; |
| |
| // Only allow HTTP or HTTPS URLs. |
| URL url = mNetworkCommunicator.getBasePageUrl(); |
| String urlProtocol = url != null ? url.getProtocol() : ""; |
| if (!(urlProtocol.equals(UrlConstants.HTTP_SCHEME) |
| || urlProtocol.equals(UrlConstants.HTTPS_SCHEME))) { |
| return false; |
| } |
| |
| return hasSendUrlPermissions(); |
| } |
| |
| /** |
| * Determines whether the user has given permission to send URLs through the "Make searches and |
| * browsing better" user setting. |
| * @return Whether we can send a URL. |
| */ |
| private boolean hasSendUrlPermissions() { |
| // Check whether the user has enabled anonymous URL-keyed data collection. |
| // This is surfaced on the relatively new "Make searches and browsing better" user setting. |
| // In case an experiment is active for the legacy UI call through the unified consent |
| // service. |
| return UnifiedConsentServiceBridge.isUrlKeyedAnonymizedDataCollectionEnabled( |
| Profile.getLastUsedRegularProfile()); |
| } |
| |
| /** |
| * The search provider icon is animated every time on long press if the user has never opened |
| * the panel before and once a day on tap. |
| * |
| * @return Whether the search provider icon should be animated. |
| */ |
| boolean shouldAnimateSearchProviderIcon() { |
| if (mSearchPanel.isShowing()) return false; |
| |
| @SelectionType |
| int selectionType = mSelectionController.getSelectionType(); |
| if (selectionType == SelectionType.TAP) { |
| long currentTimeMillis = System.currentTimeMillis(); |
| long lastAnimatedTimeMillis = mPreferencesManager.readLong( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_LAST_ANIMATION_TIME); |
| if (Math.abs(currentTimeMillis - lastAnimatedTimeMillis) > DateUtils.DAY_IN_MILLIS) { |
| mPreferencesManager.writeLong( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_LAST_ANIMATION_TIME, |
| currentTimeMillis); |
| return true; |
| } else { |
| return false; |
| } |
| } else if (selectionType == SelectionType.LONG_PRESS) { |
| // If the panel has never been opened before, getPromoOpenCount() will be 0. |
| // Once the panel has been opened, regardless of whether or not the user has opted-in or |
| // opted-out, the promo open count will be greater than zero. |
| return isUserUndecided() && getPromoOpenCount() == 0; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * @return Whether the given URL is used for Accelerated Mobile Pages by Google. |
| */ |
| boolean isAmpUrl(String url) { |
| Uri uri = Uri.parse(url); |
| if (uri == null || uri.getHost() == null || uri.getPath() == null) return false; |
| |
| return uri.getHost().contains(DOMAIN_GOOGLE) && uri.getPath().startsWith(PATH_AMP); |
| } |
| |
| // -------------------------------------------------------------------------------------------- |
| // Testing support. |
| // -------------------------------------------------------------------------------------------- |
| |
| /** |
| * Overrides the decided/undecided state for the user preference. |
| * @param decidedState Whether the user has decided or not. |
| */ |
| @VisibleForTesting |
| void overrideDecidedStateForTesting(boolean decidedState) { |
| mDidOverrideDecidedStateForTesting = true; |
| mDecidedStateForTesting = decidedState; |
| } |
| |
| /** |
| * @return count of times the panel with the promo has been opened. |
| */ |
| @VisibleForTesting |
| int getPromoOpenCount() { |
| return mPreferencesManager.readInt(ChromePreferenceKeys.CONTEXTUAL_SEARCH_PROMO_OPEN_COUNT); |
| } |
| |
| /** |
| * @return The number of times the user has tapped since the last panel open. |
| */ |
| @VisibleForTesting |
| int getTapCount() { |
| return mPreferencesManager.readInt( |
| ChromePreferenceKeys.CONTEXTUAL_SEARCH_TAP_SINCE_OPEN_COUNT); |
| } |
| |
| // -------------------------------------------------------------------------------------------- |
| // Additional considerations. |
| // -------------------------------------------------------------------------------------------- |
| |
| /** |
| * @return The ISO country code for the user's home country, or an empty string if not |
| * available or privacy-enabled. |
| */ |
| @NonNull |
| String getHomeCountry(Context context) { |
| if (ContextualSearchFieldTrial.getSwitch( |
| ContextualSearchSwitch.IS_SEND_HOME_COUNTRY_DISABLED)) { |
| return ""; |
| } |
| |
| TelephonyManager telephonyManager = |
| (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); |
| if (telephonyManager == null) return ""; |
| |
| String simCountryIso = telephonyManager.getSimCountryIso(); |
| return TextUtils.isEmpty(simCountryIso) ? "" : simCountryIso; |
| } |
| |
| /** |
| * @return Whether a promo is needed because the user is still undecided |
| * on enabling or disabling the feature. |
| */ |
| boolean isUserUndecided() { |
| if (mDidOverrideDecidedStateForTesting) return !mDecidedStateForTesting; |
| |
| return ContextualSearchManager.isContextualSearchUninitialized(); |
| } |
| |
| /** |
| * @param url The URL of the base page. |
| * @return Whether the given content view is for an HTTP page. |
| */ |
| boolean isBasePageHTTP(@Nullable URL url) { |
| return url != null && UrlConstants.HTTP_SCHEME.equals(url.getProtocol()); |
| } |
| |
| // -------------------------------------------------------------------------------------------- |
| // Related Searches Support. |
| // -------------------------------------------------------------------------------------------- |
| |
| /** |
| * Gets the runtime processing stamp for Related Searches. This typically gets the value from |
| * a param from a Field Trial Feature. |
| * @param basePageLanguage The language of the page, to check for server support. |
| * @return A {@code String} whose value describes the schema version and current processing |
| * of Related Searches, or an empty string if the user is not qualified to request |
| * Related Searches or the feature is not enabled. |
| */ |
| String getRelatedSearchesStamp(String basePageLanguage) { |
| if (!isRelatedSearchesQualifiedAndEnabled(basePageLanguage)) return ""; |
| |
| boolean isLanguageRestricted = |
| !TextUtils.isEmpty(ContextualSearchFieldTrial.getRelatedSearchesParam( |
| ContextualSearchFieldTrial.RELATED_SEARCHES_LANGUAGE_ALLOWLIST_PARAM_NAME)); |
| return buildRelatedSearchesStamp(isLanguageRestricted); |
| } |
| |
| /** |
| * Checks if the current user is both qualified to do Related Searches and has the feature |
| * enabled. Qualifications may include restrictions on language during early development. |
| * @param basePageLanguage The language of the page, to check for server support. |
| * @return Whether the user is qualified to get Related Searches suggestions and the |
| * experimental feature is enabled. |
| */ |
| boolean isRelatedSearchesQualifiedAndEnabled(String basePageLanguage) { |
| return isQualifiedForRelatedSearches(basePageLanguage) |
| && ChromeFeatureList.isEnabled(ChromeFeatureList.RELATED_SEARCHES); |
| } |
| |
| /** |
| * Determines if the current user is qualified for Related Searches. There may be language |
| * and privacy restrictions on whether users can activate Related Searches, and some of these |
| * requirements are determined at runtime based on Variations params. |
| * @param basePageLanguage The language of the page, to check for server support. |
| * @return Whether the user could do a Related Searches request if Feature-enabled. |
| */ |
| private boolean isQualifiedForRelatedSearches(String basePageLanguage) { |
| return isLanguageQualified(basePageLanguage) && canSendUrlIfNeeded() |
| && canSendContentIfNeeded(); |
| } |
| |
| /** |
| * Checks if the language of the page qualifies for Related Searches. |
| * We check the Variations config for a parameter that lists allowed languages so we can know |
| * what the server currently supports. If there's no allow list then any language will work. |
| * @param basePageLanguage The language of the page, to check for server support. |
| * @return whether the supplied parameter satisfies the current language requirement. |
| */ |
| private boolean isLanguageQualified(String basePageLanguage) { |
| String allowedLanguages = ContextualSearchFieldTrial.getRelatedSearchesParam( |
| ContextualSearchFieldTrial.RELATED_SEARCHES_LANGUAGE_ALLOWLIST_PARAM_NAME); |
| return TextUtils.isEmpty(allowedLanguages) || allowedLanguages.contains(basePageLanguage); |
| } |
| |
| /** |
| * @return whether the user's privacy setting for URL sending satisfies the configured |
| * requirement. |
| */ |
| private boolean canSendUrlIfNeeded() { |
| return !isRelatedSearchesUrlNeeded() || hasSendUrlPermissions(); |
| } |
| |
| /** |
| * @return whether the user's privacy setting for page content sending satisfies the configured |
| * requirement. |
| */ |
| private boolean canSendContentIfNeeded() { |
| return !isRelatedSearchesContentNeeded() || !isUserUndecided(); |
| } |
| |
| /** @return whether the runtime configuration has a URL sending permissions requirement. */ |
| private boolean isRelatedSearchesUrlNeeded() { |
| return isRelatedSearchesParamEnabled( |
| ContextualSearchFieldTrial.RELATED_SEARCHES_NEEDS_URL_PARAM_NAME) |
| || isMissingRelatedSearchesConfiguration(); |
| } |
| |
| /** |
| * @return whether the runtime configuration has a page content sending permissions |
| * requirement. |
| */ |
| private boolean isRelatedSearchesContentNeeded() { |
| return isRelatedSearchesParamEnabled( |
| ContextualSearchFieldTrial.RELATED_SEARCHES_NEEDS_CONTENT_PARAM_NAME) |
| || isMissingRelatedSearchesConfiguration(); |
| } |
| |
| /** |
| * @return whether the given parameter is currently enabled in the Related Searches Variation |
| * configuration. |
| */ |
| private boolean isRelatedSearchesParamEnabled(String paramName) { |
| return ContextualSearchFieldTrial.isRelatedSearchesParamEnabled(paramName); |
| } |
| |
| /** @return whether we're missing the Related Searches configuration stamp. */ |
| private boolean isMissingRelatedSearchesConfiguration() { |
| return TextUtils.isEmpty( |
| ContextualSearchFieldTrial.getRelatedSearchesExperiementConfigurationStamp()); |
| } |
| |
| /** |
| * Builds the "stamp" that tracks the processing of Related Searches and describes what was |
| * done at each stage using a shorthand notation. The notation is described in go/rsearches-dd |
| * here: http://doc/1DryD8NAP5LQAo326LnxbqkIDCNfiCOB7ak3gAYaNWAM#bookmark=id.nx7ivu2upqw |
| * <p>The first stage is built here: "1" for schema version one, "R" for the configuration |
| * Recipe which has a character describing how we'll formulate the search. Typically all of |
| * this comes from the Variations config at runtime. We programmatically append an "l" that |
| * indicates a language restriction (when present), and currently a "d" for "dark launch" so |
| * the server knows to return normal Contextual Search results for this older client. |
| * @param isLanguageRestricted Whether there are any language restrictions needed by the |
| * server. |
| * @return A string that represents and encoded description of the current request processing. |
| */ |
| private String buildRelatedSearchesStamp(boolean isLanguageRestricted) { |
| String experimentConfigStamp = |
| ContextualSearchFieldTrial.getRelatedSearchesExperiementConfigurationStamp(); |
| if (TextUtils.isEmpty(experimentConfigStamp)) experimentConfigStamp = NO_EXPERIMENT_STAMP; |
| // TODO(donnd): Consider supporting URL-only requests -- for now content is required. |
| StringBuilder stampBuilder = new StringBuilder().append(experimentConfigStamp); |
| if (isLanguageRestricted) stampBuilder.append(RELATED_SEARCHES_LANGUAGE_RESTRICTION); |
| // Hard code a tag so the server knows this version of the client is doing a dark launch |
| // and cannot decode Related Searches. |
| stampBuilder.append(RELATED_SEARCHES_DARK_LAUNCH); |
| return stampBuilder.toString(); |
| } |
| |
| // -------------------------------------------------------------------------------------------- |
| // Testing helpers. |
| // -------------------------------------------------------------------------------------------- |
| |
| /** |
| * Sets the {@link ContextualSearchNetworkCommunicator} to use for server requests. |
| * @param networkCommunicator The communicator for all future requests. |
| */ |
| @VisibleForTesting |
| public void setNetworkCommunicator(ContextualSearchNetworkCommunicator networkCommunicator) { |
| mNetworkCommunicator = networkCommunicator; |
| } |
| } |