| // 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.os.Handler; |
| import android.text.TextUtils; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver; |
| import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; |
| |
| import androidx.annotation.Nullable; |
| |
| import org.chromium.base.Log; |
| import org.chromium.base.ObserverList; |
| import org.chromium.base.SysUtils; |
| import org.chromium.base.TimeUtils; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.annotations.NativeMethods; |
| import org.chromium.base.metrics.RecordUserAction; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.ChromeFeatureList; |
| import org.chromium.chrome.browser.compositor.bottombar.OverlayContentDelegate; |
| import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.PanelState; |
| import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.StateChangeReason; |
| import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelContentViewDelegate; |
| 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.ContextualSearchInternalStateController.InternalState; |
| import org.chromium.chrome.browser.contextualsearch.ContextualSearchSelectionController.SelectionType; |
| import org.chromium.chrome.browser.contextualsearch.ResolvedSearchTerm.CardTag; |
| import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler; |
| import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler.OverrideUrlLoadingResult; |
| import org.chromium.chrome.browser.externalnav.ExternalNavigationParams; |
| import org.chromium.chrome.browser.feature_engagement.TrackerFactory; |
| import org.chromium.chrome.browser.gsa.GSAContextDisplaySelection; |
| import org.chromium.chrome.browser.infobar.InfoBarContainer; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.tab.SadTab; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.tab.TabRedirectHandler; |
| import org.chromium.chrome.browser.tabmodel.TabLaunchType; |
| import org.chromium.chrome.browser.tabmodel.TabModelSelector; |
| import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver; |
| import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver; |
| import org.chromium.chrome.browser.tabmodel.TabSelectionType; |
| import org.chromium.components.feature_engagement.EventConstants; |
| import org.chromium.components.feature_engagement.FeatureConstants; |
| import org.chromium.components.feature_engagement.Tracker; |
| import org.chromium.components.feature_engagement.TriggerState; |
| import org.chromium.components.navigation_interception.NavigationParams; |
| import org.chromium.content_public.browser.GestureStateListener; |
| import org.chromium.content_public.browser.LoadUrlParams; |
| import org.chromium.content_public.browser.NavigationEntry; |
| import org.chromium.content_public.browser.SelectionClient; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.content_public.common.BrowserControlsState; |
| import org.chromium.content_public.common.ContentUrlConstants; |
| import org.chromium.contextual_search.mojom.OverlayPosition; |
| import org.chromium.net.NetworkChangeNotifier; |
| import org.chromium.ui.touch_selection.SelectionEventType; |
| |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Manager for the Contextual Search feature. This class keeps track of the status of Contextual |
| * Search and coordinates the control with the layout. |
| */ |
| public class ContextualSearchManager |
| implements ContextualSearchManagementDelegate, ContextualSearchTranslateInterface, |
| ContextualSearchNetworkCommunicator, ContextualSearchSelectionHandler { |
| /** A delegate for reporting selected context to GSA for search quality. */ |
| public interface ContextReporterDelegate { |
| /** |
| * Reports that the given display selection has been established for the current tab. |
| * @param displaySelection The information about the selection being displayed. |
| */ |
| void reportDisplaySelection(@Nullable GSAContextDisplaySelection displaySelection); |
| } |
| |
| // TODO(donnd): provide an inner class that implements some of these interfaces (like the |
| // ContextualSearchTranslateInterface) rather than having the manager itself implement the |
| // interface because that exposes all the public methods of that interface at the manager level. |
| |
| private static final String TAG = "ContextualSearch"; |
| |
| private static final String INTENT_URL_PREFIX = "intent:"; |
| |
| // The animation duration of a URL being promoted to a tab when triggered by an |
| // intercept navigation. This is faster than the standard tab promotion animation |
| // so that it completes before the navigation. |
| private static final long INTERCEPT_NAVIGATION_PROMOTION_ANIMATION_DURATION_MS = 40; |
| |
| // We blacklist this URL because malformed URLs may bring up this page. |
| private static final String BLACKLISTED_URL = ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL; |
| |
| private static final Pattern CONTAINS_WHITESPACE_PATTERN = Pattern.compile("\\s"); |
| |
| // When we don't need to send any "home country" code we can just pass the empty string. |
| private static final String NO_HOME_COUNTRY = ""; |
| |
| // How long to wait for a tap near a previous tap before hiding the UI or showing a re-Tap. |
| // This setting is not critical: in practice it determines how long to wait after an invalid |
| // tap for the page to respond before hiding the UI. Specifically this setting just needs to be |
| // long enough for Blink's decisions before calling handleShowUnhandledTapUIIfNeeded (which |
| // probably are page-dependent), and short enough that the Bar goes away fairly quickly after a |
| // tap on non-text or whitespace: We currently do not get notification in these cases (hence the |
| // timer). |
| private static final int TAP_NEAR_PREVIOUS_DETECTION_DELAY_MS = 100; |
| |
| // How long to wait for a Tap to be converted to a Long-press gesture when the user taps on |
| // an existing tap-selection. |
| private static final int TAP_ON_TAP_SELECTION_DELAY_MS = 100; |
| |
| private final ObserverList<ContextualSearchObserver> mObservers = |
| new ObserverList<ContextualSearchObserver>(); |
| |
| private final ChromeActivity mActivity; |
| private final ContextualSearchTabPromotionDelegate mTabPromotionDelegate; |
| private final ViewTreeObserver.OnGlobalFocusChangeListener mOnFocusChangeListener; |
| |
| /** |
| * The {@link ContextualSearchInteractionRecorder} to use to record user interactions and apply |
| * ML, etc. |
| */ |
| private final ContextualSearchInteractionRecorder mInteractionRecorder; |
| |
| private final ContextualSearchSelectionClient mContextualSearchSelectionClient; |
| |
| private ContextualSearchSelectionController mSelectionController; |
| private ContextualSearchNetworkCommunicator mNetworkCommunicator; |
| private ContextualSearchPolicy mPolicy; |
| private ContextualSearchInternalStateController mInternalStateController; |
| |
| @VisibleForTesting |
| protected ContextualSearchTranslation mTranslateController; |
| |
| // The Overlay panel. |
| private ContextualSearchPanel mSearchPanel; |
| |
| // The native manager associated with this object. |
| private long mNativeContextualSearchManagerPtr; |
| |
| private ViewGroup mParentView; |
| private TabRedirectHandler mTabRedirectHandler; |
| private OverlayPanelContentViewDelegate mSearchContentViewDelegate; |
| private TabModelSelectorTabModelObserver mTabModelObserver; |
| private TabModelSelectorTabObserver mTabModelSelectorTabObserver; |
| private ContextualSearchIPH mInProductHelp; |
| |
| private boolean mDidStartLoadingResolvedSearchRequest; |
| private long mLoadedSearchUrlTimeMs; |
| private boolean mWereSearchResultsSeen; |
| private boolean mWereInfoBarsHidden; |
| private boolean mDidPromoteSearchNavigation; |
| |
| private boolean mWasActivatedByTap; |
| private boolean mIsInitialized; |
| private boolean mReceivedContextualCardsEntityData; |
| |
| // The current search context, or null. |
| private ContextualSearchContext mContext; |
| |
| /** |
| * This boolean is used for loading content after a long-press when content is not immediately |
| * loaded. |
| */ |
| private boolean mShouldLoadDelayedSearch; |
| |
| private boolean mIsShowingPromo; |
| private boolean mIsMandatoryPromo; |
| private boolean mDidLogPromoOutcome; |
| |
| /** |
| * Whether contextual search manager is currently promoting a tab. We should be ignoring hide |
| * requests when mIsPromotingTab is set to true. |
| */ |
| private boolean mIsPromotingToTab; |
| |
| private ContextualSearchRequest mSearchRequest; |
| private ContextualSearchRequest mLastSearchRequestLoaded; |
| |
| /** Whether the Accessibility Mode is enabled. */ |
| private boolean mIsAccessibilityModeEnabled; |
| |
| /** Tap Experiments and other variable behavior. */ |
| private QuickAnswersHeuristic mQuickAnswersHeuristic; |
| |
| // Counter for how many times we've called SelectWordAroundCaret without an ACK returned. |
| // TODO(donnd): replace with a more systematic approach using the InternalStateController. |
| private int mSelectWordAroundCaretCounter; |
| |
| /** An observer that reports selected context to GSA for search quality. */ |
| private ContextualSearchObserver mContextReportingObserver; |
| |
| /** |
| * The delegate that is responsible for promoting a {@link WebContents} to a {@link Tab} |
| * when necessary. |
| */ |
| public interface ContextualSearchTabPromotionDelegate { |
| /** |
| * Called when {@link WebContents} for contextual search should be promoted to a {@link |
| * Tab}. |
| * @param searchUrl The Search URL to be promoted. |
| */ |
| void createContextualSearchTab(String searchUrl); |
| } |
| |
| /** |
| * Constructs the manager for the given activity, and will attach views to the given parent. |
| * @param activity The {@code ChromeActivity} in use. |
| * @param tabPromotionDelegate The {@link ContextualSearchTabPromotionDelegate} that is |
| * responsible for building tabs from contextual search {@link WebContents}. |
| */ |
| public ContextualSearchManager( |
| ChromeActivity activity, ContextualSearchTabPromotionDelegate tabPromotionDelegate) { |
| mActivity = activity; |
| mTabPromotionDelegate = tabPromotionDelegate; |
| |
| final View controlContainer = mActivity.findViewById(R.id.control_container); |
| mOnFocusChangeListener = new OnGlobalFocusChangeListener() { |
| @Override |
| public void onGlobalFocusChanged(View oldFocus, View newFocus) { |
| if (controlContainer != null && controlContainer.hasFocus()) { |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| } |
| } |
| }; |
| |
| mSelectionController = new ContextualSearchSelectionController(activity, this); |
| mNetworkCommunicator = this; |
| mPolicy = new ContextualSearchPolicy(mSelectionController, mNetworkCommunicator); |
| mTranslateController = |
| ContextualSearchTranslateController.getContextualSearchTranslation(mPolicy, this); |
| mInternalStateController = new ContextualSearchInternalStateController( |
| mPolicy, getContextualSearchInternalStateHandler()); |
| mInteractionRecorder = new ContextualSearchRankerLoggerImpl(); |
| mContextualSearchSelectionClient = new ContextualSearchSelectionClient(); |
| mInProductHelp = new ContextualSearchIPH(); |
| } |
| |
| /** |
| * Initializes this manager. |
| * @param parentView The parent view to attach Contextual Search UX to. |
| */ |
| public void initialize(ViewGroup parentView) { |
| mNativeContextualSearchManagerPtr = ContextualSearchManagerJni.get().init(this); |
| |
| mParentView = parentView; |
| mParentView.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnFocusChangeListener); |
| |
| mInProductHelp.setParentView(parentView); |
| |
| mTabRedirectHandler = TabRedirectHandler.create(); |
| |
| mIsShowingPromo = false; |
| mDidLogPromoOutcome = false; |
| mDidStartLoadingResolvedSearchRequest = false; |
| mWereSearchResultsSeen = false; |
| mIsInitialized = true; |
| |
| mInternalStateController.reset(StateChangeReason.UNKNOWN); |
| |
| listenForTabModelSelectorNotifications(); |
| } |
| |
| /** |
| * Destroys the native Contextual Search Manager. |
| * Call this method before orphaning this object to allow it to be garbage collected. |
| */ |
| public void destroy() { |
| if (!mIsInitialized) return; |
| |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| mParentView.getViewTreeObserver().removeOnGlobalFocusChangeListener(mOnFocusChangeListener); |
| ContextualSearchManagerJni.get().destroy(mNativeContextualSearchManagerPtr, this); |
| stopListeningForHideNotifications(); |
| mTabRedirectHandler.clear(); |
| mInternalStateController.enter(InternalState.UNDEFINED); |
| } |
| |
| @Override |
| public void setContextualSearchPanel(ContextualSearchPanel panel) { |
| assert panel != null; |
| mSearchPanel = panel; |
| mPolicy.setContextualSearchPanel(panel); |
| mInProductHelp.setSearchPanel(panel); |
| } |
| |
| @Override |
| public ChromeActivity getChromeActivity() { |
| return mActivity; |
| } |
| |
| /** @return Whether the Search Panel is opened. That is, whether it is EXPANDED or MAXIMIZED. */ |
| public boolean isSearchPanelOpened() { |
| return mSearchPanel != null && mSearchPanel.isPanelOpened(); |
| } |
| |
| /** @return Whether the {@code mSearchPanel} is not {@code null} and is showing. */ |
| boolean isSearchPanelShowing() { |
| return mSearchPanel != null && mSearchPanel.isShowing(); |
| } |
| |
| /** @return Whether the {@code mSearchPanel} is not {@code null} and is currently active. */ |
| boolean isSearchPanelActive() { |
| return mSearchPanel != null && mSearchPanel.isActive(); |
| } |
| |
| /** |
| * @return the {@link WebContents} of the {@code mSearchPanel} or {@code null} if |
| * {@code mSearchPanel} is null or the search panel doesn't currently hold one. |
| */ |
| private @Nullable WebContents getSearchPanelWebContents() { |
| return mSearchPanel == null ? null : mSearchPanel.getWebContents(); |
| } |
| |
| /** @return The Base Page's {@link WebContents}. */ |
| @Nullable |
| private WebContents getBaseWebContents() { |
| return mSelectionController.getBaseWebContents(); |
| } |
| |
| /** @return The Base Page's {@link URL}. */ |
| @Nullable |
| private URL getBasePageURL() { |
| WebContents baseWebContents = mSelectionController.getBaseWebContents(); |
| if (baseWebContents == null) return null; |
| try { |
| return new URL(baseWebContents.getVisibleUrl()); |
| } catch (MalformedURLException e) { |
| return null; |
| } |
| } |
| |
| /** Notifies that the base page has started loading a page. */ |
| public void onBasePageLoadStarted() { |
| mSelectionController.onBasePageLoadStarted(); |
| } |
| |
| /** Notifies that a Context Menu has been shown. */ |
| void onContextMenuShown() { |
| mSelectionController.onContextMenuShown(); |
| } |
| |
| @Override |
| public void hideContextualSearch(@StateChangeReason int reason) { |
| mInternalStateController.reset(reason); |
| } |
| |
| @Override |
| public void onCloseContextualSearch(@StateChangeReason int reason) { |
| if (mSearchPanel == null) return; |
| |
| mSelectionController.onSearchEnded(reason); |
| |
| // Show the infobar container if it was visible before Contextual Search was shown. |
| if (mWereInfoBarsHidden) { |
| mWereInfoBarsHidden = false; |
| InfoBarContainer container = getInfoBarContainer(); |
| if (container != null) { |
| container.setHidden(false); |
| } |
| } |
| |
| if (!mWereSearchResultsSeen && mLoadedSearchUrlTimeMs != 0L) { |
| removeLastSearchVisit(); |
| } |
| |
| // Clear the timestamp. This is to avoid future calls to hideContextualSearch clearing |
| // the current URL. |
| mLoadedSearchUrlTimeMs = 0L; |
| mWereSearchResultsSeen = false; |
| |
| mSearchRequest = null; |
| |
| mInProductHelp.dismiss(); |
| |
| if (mIsShowingPromo && !mDidLogPromoOutcome && mSearchPanel.wasPromoInteractive()) { |
| ContextualSearchUma.logPromoOutcome(mWasActivatedByTap, mIsMandatoryPromo); |
| mDidLogPromoOutcome = true; |
| } |
| |
| mIsShowingPromo = false; |
| mSearchPanel.setIsPromoActive(false, false); |
| notifyHideContextualSearch(); |
| } |
| |
| /** |
| * Shows the Contextual Search UX. |
| * @param stateChangeReason The reason explaining the change of state. |
| */ |
| private void showContextualSearch(@StateChangeReason int stateChangeReason) { |
| assert mSearchPanel != null; |
| |
| // Dismiss the undo SnackBar if present by committing all tab closures. |
| mActivity.getTabModelSelector().commitAllTabClosures(); |
| |
| if (!mSearchPanel.isShowing()) { |
| // If visible, hide the infobar container before showing the Contextual Search panel. |
| InfoBarContainer container = getInfoBarContainer(); |
| if (container != null && container.getVisibility() == View.VISIBLE) { |
| mWereInfoBarsHidden = true; |
| container.setHidden(true); |
| } |
| } |
| |
| // If the user is jumping from one unseen search to another search, remove the last search |
| // from history. |
| @PanelState |
| int state = mSearchPanel.getPanelState(); |
| if (!mWereSearchResultsSeen && mLoadedSearchUrlTimeMs != 0L |
| && state != PanelState.UNDEFINED && state != PanelState.CLOSED) { |
| removeLastSearchVisit(); |
| } |
| |
| mSearchPanel.destroyContent(); |
| mReceivedContextualCardsEntityData = false; |
| |
| String selection = mSelectionController.getSelectedText(); |
| boolean canResolve = mSelectionController.getSelectionType() == SelectionType.TAP |
| || mSelectionController.getSelectionType() == SelectionType.RESOLVING_LONG_PRESS; |
| if (canResolve) { |
| // If the user action was not a long-press, we should not delay before loading content. |
| mShouldLoadDelayedSearch = false; |
| } |
| if (canResolve && mPolicy.shouldPreviousGestureResolve()) { |
| // For a resolving gestures we'll figure out translation need after the Resolve. |
| } else if (!TextUtils.isEmpty(selection)) { |
| boolean shouldPrefetch = mPolicy.shouldPrefetchSearchResult(); |
| mSearchRequest = new ContextualSearchRequest(selection, shouldPrefetch); |
| mTranslateController.forceAutoDetectTranslateUnlessDisabled(mSearchRequest); |
| mDidStartLoadingResolvedSearchRequest = false; |
| mSearchPanel.setSearchTerm(selection); |
| if (shouldPrefetch) loadSearchUrl(); |
| |
| // Record metrics for manual refinement of the search term from long-press. |
| // TODO(donnd): remove this section once metrics have been analyzed. |
| if (!canResolve && mSearchPanel.isPeeking()) { |
| boolean isSingleWord = |
| !CONTAINS_WHITESPACE_PATTERN.matcher(selection.trim()).find(); |
| RecordUserAction.record(isSingleWord ? "ContextualSearch.ManualRefineSingleWord" |
| : "ContextualSearch.ManualRefineMultiWord"); |
| } |
| } else { |
| // The selection is no longer valid, so we can't build a request. Don't show the UX. |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| return; |
| } |
| mWereSearchResultsSeen = false; |
| |
| // Note: now that the contextual search has properly started, set the promo involvement. |
| if (mPolicy.isPromoAvailable()) { |
| mIsShowingPromo = true; |
| mIsMandatoryPromo = mPolicy.isMandatoryPromoAvailable(); |
| mDidLogPromoOutcome = false; |
| mSearchPanel.setIsPromoActive(true, mIsMandatoryPromo); |
| mSearchPanel.setDidSearchInvolvePromo(); |
| } |
| |
| mSearchPanel.requestPanelShow(stateChangeReason); |
| |
| assert mSelectionController.getSelectionType() != SelectionType.UNDETERMINED; |
| mWasActivatedByTap = mSelectionController.getSelectionType() == SelectionType.TAP; |
| |
| Tracker tracker = |
| TrackerFactory.getTrackerForProfile(mActivity.getActivityTab().getProfile()); |
| tracker.notifyEvent(mWasActivatedByTap |
| ? EventConstants.CONTEXTUAL_SEARCH_TRIGGERED_BY_TAP |
| : EventConstants.CONTEXTUAL_SEARCH_TRIGGERED_BY_LONGPRESS); |
| |
| // Log whether IPH for tapping has been shown before. |
| if (mWasActivatedByTap) { |
| ContextualSearchUma.logTapIPH( |
| tracker.getTriggerState(FeatureConstants.CONTEXTUAL_SEARCH_PROMOTE_TAP_FEATURE) |
| == TriggerState.HAS_BEEN_DISPLAYED); |
| } |
| } |
| |
| @Override |
| public void startSearchTermResolutionRequest(String selection, boolean isRestrictedResolve) { |
| WebContents baseWebContents = getBaseWebContents(); |
| if (baseWebContents != null && mContext != null && mContext.canResolve()) { |
| if (isRestrictedResolve) mContext.setRestrictedResolve(); |
| ContextualSearchManagerJni.get().startSearchTermResolutionRequest( |
| mNativeContextualSearchManagerPtr, this, mContext, getBaseWebContents()); |
| } else { |
| // Something went wrong and we couldn't resolve. |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| } |
| } |
| |
| @Override |
| @Nullable |
| public URL getBasePageUrl() { |
| WebContents baseWebContents = getBaseWebContents(); |
| if (baseWebContents == null) return null; |
| |
| try { |
| return new URL(baseWebContents.getLastCommittedUrl()); |
| } catch (MalformedURLException e) { |
| return null; |
| } |
| } |
| |
| /** Accessor for the {@code InfoBarContainer} currently attached to the {@code Tab}. */ |
| private InfoBarContainer getInfoBarContainer() { |
| Tab tab = mActivity.getActivityTab(); |
| return tab == null ? null : InfoBarContainer.get(tab); |
| } |
| |
| /** Listens for notifications that should hide the Contextual Search bar. */ |
| private void listenForTabModelSelectorNotifications() { |
| TabModelSelector selector = mActivity.getTabModelSelector(); |
| mTabModelObserver = new TabModelSelectorTabModelObserver(selector) { |
| @Override |
| public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) { |
| if ((!mIsPromotingToTab && tab.getId() != lastId) |
| || mActivity.getTabModelSelector().isIncognitoSelected()) { |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| mSelectionController.onTabSelected(); |
| } |
| } |
| |
| @Override |
| public void didAddTab(Tab tab, @TabLaunchType int type) { |
| // If we're in the process of promoting this tab, just return and don't mess with |
| // this state. |
| if (tab.getWebContents() == getSearchPanelWebContents()) return; |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| } |
| }; |
| mTabModelSelectorTabObserver = new TabModelSelectorTabObserver(selector) { |
| @Override |
| public void onPageLoadStarted(Tab tab, String url) { |
| // Detects navigation of the base page for crbug.com/428368 (navigation-detection). |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| } |
| |
| @Override |
| public void onCrash(Tab tab) { |
| if (SadTab.isShowing(tab)) { |
| // Hide contextual search if the foreground tab crashed |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| } |
| } |
| |
| @Override |
| public void onClosingStateChanged(Tab tab, boolean closing) { |
| if (closing) hideContextualSearch(StateChangeReason.UNKNOWN); |
| } |
| }; |
| } |
| |
| /** Stops listening for notifications that should hide the Contextual Search bar. */ |
| private void stopListeningForHideNotifications() { |
| if (mTabModelObserver != null) mTabModelObserver.destroy(); |
| if (mTabModelSelectorTabObserver != null) mTabModelSelectorTabObserver.destroy(); |
| mTabModelObserver = null; |
| mTabModelSelectorTabObserver = null; |
| } |
| |
| /** Clears our private member referencing the native manager. */ |
| @CalledByNative |
| public void clearNativeManager() { |
| assert mNativeContextualSearchManagerPtr != 0; |
| mNativeContextualSearchManagerPtr = 0; |
| } |
| |
| /** |
| * Sets our private member referencing the native manager. |
| * @param nativeManager The pointer to the native Contextual Search manager. |
| */ |
| @CalledByNative |
| public void setNativeManager(long nativeManager) { |
| assert mNativeContextualSearchManagerPtr == 0; |
| mNativeContextualSearchManagerPtr = nativeManager; |
| } |
| |
| /** |
| * Called by native code when the surrounding text and selection range are available. |
| * This is done for both Tap and Long-press gestures. |
| * @param encoding The original encoding used on the base page. |
| * @param surroundingText The Text surrounding the selection. |
| * @param startOffset The start offset of the selection. |
| * @param endOffset The end offset of the selection. |
| */ |
| @CalledByNative |
| @VisibleForTesting |
| void onTextSurroundingSelectionAvailable( |
| final String encoding, final String surroundingText, int startOffset, int endOffset) { |
| if (mInternalStateController.isStillWorkingOn(InternalState.GATHERING_SURROUNDINGS)) { |
| assert mContext != null; |
| // Sometimes Blink returns empty surroundings and 0 offsets so reset in that case. |
| // See crbug.com/393100. |
| if (surroundingText.length() == 0) { |
| mInternalStateController.reset(StateChangeReason.UNKNOWN); |
| } else { |
| mContext.setSurroundingText(encoding, surroundingText, startOffset, endOffset); |
| mInternalStateController.notifyFinishedWorkOn(InternalState.GATHERING_SURROUNDINGS); |
| } |
| } |
| } |
| |
| /** |
| * Called in response to the {@link ContextualSearchManagerJni#startSearchTermResolutionRequest} |
| * method. If {@code startSearchTermResolutionRequest} is called with a previous request sill |
| * pending our native delegate is supposed to cancel all previous requests. So this code should |
| * only be called with data corresponding to the most recent request. |
| * @param isNetworkUnavailable Indicates if the network is unavailable, in which case all other |
| * parameters should be ignored. |
| * @param responseCode The HTTP response code. If the code is not OK, the query should be |
| * ignored. |
| * @param searchTerm The term to use in our subsequent search. |
| * @param displayText The text to display in our UX. |
| * @param alternateTerm The alternate term to display on the results page. |
| * @param mid the MID for an entity to use to trigger a Knowledge Panel, or an empty string. |
| * A MID is a unique identifier for an entity in the Search Knowledge Graph. |
| * @param selectionStartAdjust A positive number of characters that the start of the existing |
| * selection should be expanded by. |
| * @param selectionEndAdjust A positive number of characters that the end of the existing |
| * selection should be expanded by. |
| * @param contextLanguage The language of the original search term, or an empty string. |
| * @param thumbnailUrl The URL of the thumbnail to display in our UX. |
| * @param caption The caption to display. |
| * @param quickActionUri The URI for the intent associated with the quick action. |
| * @param quickActionCategory The {@link QuickActionCategory} for the quick action. |
| * @param loggedEventId The EventID logged by the server, which should be recorded and sent back |
| * to the server along with user action results in a subsequent request. |
| * @param searchUrlFull The URL for the full search to present in the overlay, or empty. |
| * @param searchUrlPreload The URL for the search to preload into the overlay, or empty. |
| * @param cocaCardTag The primary internal Coca card tag for the response, or {@code 0} if none. |
| */ |
| @CalledByNative |
| public void onSearchTermResolutionResponse(boolean isNetworkUnavailable, int responseCode, |
| final String searchTerm, final String displayText, final String alternateTerm, |
| final String mid, boolean doPreventPreload, int selectionStartAdjust, |
| int selectionEndAdjust, final String contextLanguage, final String thumbnailUrl, |
| final String caption, final String quickActionUri, |
| @QuickActionCategory final int quickActionCategory, final long loggedEventId, |
| final String searchUrlFull, final String searchUrlPreload, |
| @CardTag final int cocaCardTag) { |
| ResolvedSearchTerm resolvedSearchTerm = new ResolvedSearchTerm(isNetworkUnavailable, |
| responseCode, searchTerm, displayText, alternateTerm, mid, doPreventPreload, |
| selectionStartAdjust, selectionEndAdjust, contextLanguage, thumbnailUrl, caption, |
| quickActionUri, quickActionCategory, loggedEventId, searchUrlFull, searchUrlPreload, |
| cocaCardTag); |
| mNetworkCommunicator.handleSearchTermResolutionResponse(resolvedSearchTerm); |
| } |
| |
| @Override |
| public void handleSearchTermResolutionResponse(ResolvedSearchTerm resolvedSearchTerm) { |
| if (!mInternalStateController.isStillWorkingOn(InternalState.RESOLVING)) return; |
| |
| // Show an appropriate message for what to search for. |
| String message; |
| boolean doLiteralSearch = false; |
| if (resolvedSearchTerm.isNetworkUnavailable()) { |
| // TODO(donnd): double-check that the network is really unavailable, maybe using |
| // NetworkChangeNotifier#isOnline. |
| message = mActivity.getResources().getString( |
| R.string.contextual_search_network_unavailable); |
| } else if (!isHttpFailureCode(resolvedSearchTerm.responseCode()) |
| && !TextUtils.isEmpty(resolvedSearchTerm.displayText())) { |
| message = resolvedSearchTerm.displayText(); |
| } else if (!mPolicy.shouldShowErrorCodeInBar()) { |
| message = mSelectionController.getSelectedText(); |
| doLiteralSearch = true; |
| } else { |
| message = mActivity.getResources().getString( |
| R.string.contextual_search_error, resolvedSearchTerm.responseCode()); |
| doLiteralSearch = true; |
| } |
| |
| boolean receivedCaptionOrThumbnail = !TextUtils.isEmpty(resolvedSearchTerm.caption()) |
| || !TextUtils.isEmpty(resolvedSearchTerm.thumbnailUrl()); |
| |
| assert mSearchPanel != null; |
| mSearchPanel.onSearchTermResolved(message, resolvedSearchTerm.thumbnailUrl(), |
| resolvedSearchTerm.quickActionUri(), resolvedSearchTerm.quickActionCategory(), |
| resolvedSearchTerm.cardTagEnum()); |
| if (!TextUtils.isEmpty(resolvedSearchTerm.caption())) { |
| // Call #onSetCaption() to set the caption. For entities, the caption should not be |
| // regarded as an answer. In the future, when quick actions are added, doesAnswer will |
| // need to be determined rather than always set to false. |
| boolean doesAnswer = false; |
| onSetCaption(resolvedSearchTerm.caption(), doesAnswer); |
| } |
| |
| boolean quickActionShown = |
| mSearchPanel.getSearchBarControl().getQuickActionControl().hasQuickAction(); |
| mReceivedContextualCardsEntityData = !quickActionShown && receivedCaptionOrThumbnail; |
| |
| if (mReceivedContextualCardsEntityData) { |
| Tracker tracker = TrackerFactory.getTrackerForProfile( |
| Profile.getLastUsedProfile().getOriginalProfile()); |
| tracker.notifyEvent(EventConstants.CONTEXTUAL_SEARCH_ENTITY_RESULT); |
| mInProductHelp.onEntityDataReceived( |
| mWasActivatedByTap, Profile.getLastUsedProfile().getOriginalProfile()); |
| } |
| |
| ContextualSearchUma.logContextualCardsDataShown(mReceivedContextualCardsEntityData); |
| mSearchPanel.getPanelMetrics().setWasContextualCardsDataShown( |
| mReceivedContextualCardsEntityData, resolvedSearchTerm.cardTagEnum()); |
| ContextualSearchUma.logQuickActionShown( |
| quickActionShown, resolvedSearchTerm.quickActionCategory()); |
| mSearchPanel.getPanelMetrics().setWasQuickActionShown( |
| quickActionShown, resolvedSearchTerm.quickActionCategory()); |
| |
| // If there was an error, fall back onto a literal search for the selection. |
| // Since we're showing the panel, there must be a selection. |
| String searchTerm = resolvedSearchTerm.searchTerm(); |
| String alternateTerm = resolvedSearchTerm.alternateTerm(); |
| boolean doPreventPreload = resolvedSearchTerm.doPreventPreload(); |
| if (doLiteralSearch) { |
| searchTerm = mSelectionController.getSelectedText(); |
| alternateTerm = null; |
| doPreventPreload = true; |
| } |
| if (!TextUtils.isEmpty(searchTerm)) { |
| // TODO(donnd): Instead of preloading, we should prefetch (ie the URL should not |
| // appear in the user's history until the user views it). See crbug.com/406446. |
| boolean shouldPreload = !doPreventPreload && mPolicy.shouldPrefetchSearchResult(); |
| mSearchRequest = new ContextualSearchRequest(searchTerm, alternateTerm, |
| resolvedSearchTerm.mid(), shouldPreload, resolvedSearchTerm.searchUrlFull(), |
| resolvedSearchTerm.searchUrlPreload()); |
| // Trigger translation, if enabled. |
| mTranslateController.forceTranslateIfNeeded( |
| mSearchRequest, resolvedSearchTerm.contextLanguage()); |
| mDidStartLoadingResolvedSearchRequest = false; |
| if (mSearchPanel.isContentShowing()) { |
| mSearchRequest.setNormalPriority(); |
| } |
| if (mSearchPanel.isContentShowing() || shouldPreload) { |
| loadSearchUrl(); |
| } |
| mPolicy.logSearchTermResolutionDetails(searchTerm); |
| } |
| |
| // Adjust the selection unless the user changed it since we initiated the search. |
| int selectionStartAdjust = resolvedSearchTerm.selectionStartAdjust(); |
| int selectionEndAdjust = resolvedSearchTerm.selectionEndAdjust(); |
| if ((selectionStartAdjust != 0 || selectionEndAdjust != 0) |
| && (mSelectionController.getSelectionType() == SelectionType.TAP |
| || mSelectionController.getSelectionType() |
| == SelectionType.RESOLVING_LONG_PRESS)) { |
| String originalSelection = mContext == null ? null : mContext.getInitialSelectedWord(); |
| String currentSelection = mSelectionController.getSelectedText(); |
| if (currentSelection != null) currentSelection = currentSelection.trim(); |
| if (originalSelection != null && originalSelection.trim().equals(currentSelection)) { |
| mSelectionController.adjustSelection(selectionStartAdjust, selectionEndAdjust); |
| mContext.onSelectionAdjusted(selectionStartAdjust, selectionEndAdjust); |
| } |
| } |
| |
| // Tell the Interaction Recorder about the current Event ID for persisted interaction. |
| mInteractionRecorder.persistInteraction(resolvedSearchTerm.loggedEventId()); |
| |
| mInternalStateController.notifyFinishedWorkOn(InternalState.RESOLVING); |
| } |
| |
| /** |
| * External entry point to determine if the device is currently online or not. |
| * Stubbed out when under test. |
| * @return Whether the device is currently online. |
| */ |
| boolean isDeviceOnline() { |
| return mNetworkCommunicator.isOnline(); |
| } |
| |
| /** Handles this {@link ContextualSearchNetworkCommunicator} vector when not under test. */ |
| @Override |
| public boolean isOnline() { |
| return NetworkChangeNotifier.isOnline(); |
| } |
| |
| /** Loads a Search Request in the Contextual Search's Content View. */ |
| private void loadSearchUrl() { |
| assert mSearchPanel != null; |
| mLoadedSearchUrlTimeMs = System.currentTimeMillis(); |
| mLastSearchRequestLoaded = mSearchRequest; |
| String searchUrl = mSearchRequest.getSearchUrl(); |
| ContextualSearchManagerJni.get().whitelistContextualSearchJsApiUrl( |
| mNativeContextualSearchManagerPtr, this, searchUrl); |
| mSearchPanel.loadUrlInPanel(searchUrl); |
| mDidStartLoadingResolvedSearchRequest = true; |
| |
| // TODO(donnd): If the user taps on a word and quickly after that taps on the |
| // peeking Search Bar, the Search Content View will not be displayed. It seems that |
| // calling WebContents.onShow() while it's being created has no effect. |
| // For now, we force the ContentView to be displayed by calling onShow() again |
| // when a URL is being loaded. See: crbug.com/398206 |
| if (mSearchPanel.isContentShowing() && getSearchPanelWebContents() != null) { |
| getSearchPanelWebContents().onShow(); |
| } |
| } |
| |
| /** |
| * Called to set a caption. The caption may either be included with the search term resolution |
| * response or set by the page through the CS JavaScript API used to notify CS that there is |
| * a caption available on the current overlay. |
| * @param caption The caption to display. |
| * @param doesAnswer Whether the caption should be regarded as an answer such |
| * that the user may not need to open the panel, or whether the caption |
| * is simply informative or descriptive of the answer in the full results. |
| */ |
| @CalledByNative |
| private void onSetCaption(String caption, boolean doesAnswer) { |
| if (TextUtils.isEmpty(caption) || mSearchPanel == null) return; |
| |
| // Notify the UI of the caption. |
| mSearchPanel.setCaption(caption); |
| if (mQuickAnswersHeuristic != null) { |
| mQuickAnswersHeuristic.setConditionSatisfied(true); |
| mQuickAnswersHeuristic.setDoesAnswer(doesAnswer); |
| } |
| |
| // Update Tap counters to account for a possible answer. |
| mPolicy.updateCountersForQuickAnswer(mWasActivatedByTap, doesAnswer); |
| } |
| |
| /** |
| * Called by JavaScript in the Overlay to change the position of the overlay. |
| * The panel cannot be changed to any opened position if it's not already opened. |
| * @param desiredPosition The desired position of the Overlay Panel expressed as an |
| * OverlayPosition int (defined in contextual_search_js_api_service.mojom). |
| */ |
| @CalledByNative |
| private void onChangeOverlayPosition(int desiredPosition) { |
| assert desiredPosition >= OverlayPosition.CLOSE |
| && desiredPosition <= OverlayPosition.MAXIMIZE; |
| // Ignore requests when the panel is not already open to prevent spam or abuse of the API. |
| if (!mSearchPanel.isShowing() || desiredPosition < OverlayPosition.CLOSE |
| || desiredPosition > OverlayPosition.MAXIMIZE) { |
| Log.w(TAG, "Unexpected request to set Overlay position to " + desiredPosition); |
| return; |
| } |
| |
| // Set the position. |
| switch (desiredPosition) { |
| case OverlayPosition.CLOSE: |
| mSearchPanel.closePanel(StateChangeReason.UNKNOWN, true); |
| break; |
| case OverlayPosition.PEEK: |
| mSearchPanel.peekPanel(StateChangeReason.UNKNOWN); |
| break; |
| case OverlayPosition.EXPAND: |
| mSearchPanel.expandPanel(StateChangeReason.UNKNOWN); |
| break; |
| case OverlayPosition.MAXIMIZE: |
| mSearchPanel.maximizePanel(StateChangeReason.UNKNOWN); |
| break; |
| } |
| } |
| |
| /** |
| * Notifies that the Accessibility Mode state has changed. |
| * |
| * @param enabled Whether the Accessibility Mode is enabled. |
| */ |
| public void onAccessibilityModeChanged(boolean enabled) { |
| mIsAccessibilityModeEnabled = enabled; |
| if (enabled) hideContextualSearch(StateChangeReason.UNKNOWN); |
| } |
| |
| /** |
| * Notifies that the preference state has changed. |
| * @param isEnabled Whether the feature is enabled. |
| */ |
| public void onContextualSearchPrefChanged(boolean isEnabled) { |
| // The pref may be automatically changed during application startup due to enterprise |
| // configuration settings, so we may not have a panel yet. |
| if (mSearchPanel != null) mSearchPanel.onContextualSearchPrefChanged(isEnabled); |
| } |
| |
| @Override |
| public void stopPanelContentsNavigation() { |
| if (getSearchPanelWebContents() == null) return; |
| |
| getSearchPanelWebContents().stop(); |
| } |
| |
| // ============================================================================================ |
| // Observers |
| // ============================================================================================ |
| |
| /** @param observer An observer to notify when the user performs a contextual search. */ |
| void addObserver(ContextualSearchObserver observer) { |
| mObservers.addObserver(observer); |
| } |
| |
| /** @param observer An observer to no longer notify when the user performs a contextual search. |
| */ |
| void removeObserver(ContextualSearchObserver observer) { |
| mObservers.removeObserver(observer); |
| } |
| |
| /** |
| * Notifies that a new selection has been established and available for Contextual Search. |
| * Should be called when the selection changes to notify listeners that care about the selection |
| * and surrounding text. |
| * Specifically this means we're showing the Contextual Search UX for the given selection. |
| * Notifies Icing of the current selection. |
| * Also notifies the panel whether the selection was part of a URL. |
| */ |
| private void notifyObserversOfContextSelectionChanged() { |
| assert mContext != null; |
| String surroundingText = mContext.getSurroundingText(); |
| assert surroundingText != null; |
| int startOffset = mContext.getSelectionStartOffset(); |
| int endOffset = mContext.getSelectionEndOffset(); |
| if (!ContextualSearchFieldTrial.getSwitch( |
| ContextualSearchSwitch.IS_PAGE_CONTENT_NOTIFICATION_DISABLED)) { |
| GSAContextDisplaySelection selection = new GSAContextDisplaySelection( |
| mContext.getEncoding(), surroundingText, startOffset, endOffset); |
| notifyShowContextualSearch(selection); |
| } |
| } |
| |
| /** |
| * Notifies all Contextual Search observers that a search has occurred. |
| * @param selectionContext The selection and context that triggered the search. |
| */ |
| private void notifyShowContextualSearch(GSAContextDisplaySelection selectionContext) { |
| if (!mPolicy.canSendSurroundings()) selectionContext = null; |
| |
| for (ContextualSearchObserver observer : mObservers) { |
| observer.onShowContextualSearch(selectionContext); |
| } |
| } |
| |
| /** Notifies all Contextual Search observers that a search ended and is no longer in effect. */ |
| private void notifyHideContextualSearch() { |
| for (ContextualSearchObserver observer : mObservers) { |
| observer.onHideContextualSearch(); |
| } |
| } |
| |
| // ============================================================================================ |
| // ContextualSearchTranslateInterface |
| // ============================================================================================ |
| |
| @Override |
| public String getAcceptLanguages() { |
| return ContextualSearchManagerJni.get().getAcceptLanguages( |
| mNativeContextualSearchManagerPtr, this); |
| } |
| |
| @Override |
| public String getTranslateServiceTargetLanguage() { |
| return ContextualSearchManagerJni.get().getTargetLanguage( |
| mNativeContextualSearchManagerPtr, this); |
| } |
| |
| // ============================================================================================ |
| // OverlayContentDelegate |
| // ============================================================================================ |
| |
| @Override |
| public OverlayContentDelegate getOverlayContentDelegate() { |
| return new SearchOverlayContentDelegate(); |
| } |
| |
| /** Implementation of OverlayContentDelegate. Made public for testing purposes. */ |
| public class SearchOverlayContentDelegate extends OverlayContentDelegate { |
| // Note: New navigation or changes to the WebContents are not advised in this class since |
| // the WebContents is being observed and navigation is already being performed. |
| |
| public SearchOverlayContentDelegate() {} |
| |
| @Override |
| public void onMainFrameLoadStarted(String url, boolean isExternalUrl) { |
| assert mSearchPanel != null; |
| mSearchPanel.updateBrowserControlsState(); |
| |
| if (isExternalUrl) { |
| onExternalNavigation(url); |
| } |
| } |
| |
| @Override |
| public void onMainFrameNavigation(String url, boolean isExternalUrl, boolean isFailure) { |
| assert mSearchPanel != null; |
| if (isExternalUrl) { |
| if (!ContextualSearchFieldTrial.getSwitch( |
| ContextualSearchSwitch.IS_AMP_AS_SEPARATE_TAB_DISABLED) |
| && mPolicy.isAmpUrl(url) && mSearchPanel.didTouchContent()) { |
| onExternalNavigation(url); |
| } |
| } else { |
| // Could be just prefetching, check if that failed. |
| onContextualSearchRequestNavigation(isFailure); |
| |
| // Record metrics for when the prefetched results became viewable. |
| if (mSearchRequest != null && mSearchRequest.wasPrefetch()) { |
| boolean didResolve = mPolicy.shouldPreviousGestureResolve(); |
| mSearchPanel.onPanelNavigatedToPrefetchedSearch(didResolve); |
| } |
| } |
| } |
| |
| @Override |
| public void onContentLoadStarted(String url) { |
| mDidPromoteSearchNavigation = false; |
| } |
| |
| @Override |
| public void onVisibilityChanged(boolean isVisible) { |
| if (isVisible) { |
| mWereSearchResultsSeen = true; |
| // If there's no current request, then either a search term resolution |
| // is in progress or we should do a verbatim search now. |
| if (mSearchRequest == null && mPolicy.shouldCreateVerbatimRequest() |
| && !TextUtils.isEmpty(mSelectionController.getSelectedText())) { |
| mSearchRequest = |
| new ContextualSearchRequest(mSelectionController.getSelectedText()); |
| mDidStartLoadingResolvedSearchRequest = false; |
| } |
| if (mSearchRequest != null |
| && (!mDidStartLoadingResolvedSearchRequest || mShouldLoadDelayedSearch)) { |
| // mShouldLoadDelayedSearch is used in the long-press case to load content. |
| // Since content is now created and destroyed for each request, was impossible |
| // to know if content was already loaded or recently needed to be; this is for |
| // the case where it needed to be. |
| mSearchRequest.setNormalPriority(); |
| loadSearchUrl(); |
| } |
| mShouldLoadDelayedSearch = true; |
| mPolicy.updateCountersForOpen(); |
| } |
| } |
| |
| @Override |
| public void onContentViewCreated() { |
| ContextualSearchManagerJni.get().enableContextualSearchJsApiForWebContents( |
| mNativeContextualSearchManagerPtr, ContextualSearchManager.this, |
| getSearchPanelWebContents()); |
| } |
| |
| @Override |
| public void onContentViewDestroyed() { |
| if (mSearchContentViewDelegate != null) { |
| mSearchContentViewDelegate.releaseOverlayPanelContent(); |
| } |
| } |
| |
| @Override |
| public void onContentViewSeen() { |
| assert mSearchPanel != null; |
| mSearchPanel.setWasSearchContentViewSeen(); |
| } |
| |
| @Override |
| public boolean shouldInterceptNavigation( |
| ExternalNavigationHandler externalNavHandler, NavigationParams navigationParams) { |
| assert mSearchPanel != null; |
| mTabRedirectHandler.updateNewUrlLoading(navigationParams.pageTransitionType, |
| navigationParams.isRedirect, |
| navigationParams.hasUserGesture || navigationParams.hasUserGestureCarryover, |
| mActivity.getLastUserInteractionTime(), TabRedirectHandler.INVALID_ENTRY_INDEX); |
| ExternalNavigationParams params = |
| new ExternalNavigationParams |
| .Builder(navigationParams.url, false, navigationParams.referrer, |
| navigationParams.pageTransitionType, |
| navigationParams.isRedirect) |
| .setApplicationMustBeInForeground(true) |
| .setRedirectHandler(mTabRedirectHandler) |
| .setIsMainFrame(navigationParams.isMainFrame) |
| .build(); |
| if (externalNavHandler.shouldOverrideUrlLoading(params) |
| != OverrideUrlLoadingResult.NO_OVERRIDE) { |
| mSearchPanel.maximizePanelThenPromoteToTab(StateChangeReason.TAB_PROMOTION, |
| INTERCEPT_NAVIGATION_PROMOTION_ANIMATION_DURATION_MS); |
| return false; |
| } |
| return !navigationParams.isExternalProtocol; |
| } |
| } |
| |
| // ============================================================================================ |
| // Search Content View |
| // ============================================================================================ |
| |
| /** |
| * Sets the {@code OverlayPanelContentViewDelegate} associated with the Content View. |
| * @param delegate |
| */ |
| public void setSearchContentViewDelegate(OverlayPanelContentViewDelegate delegate) { |
| mSearchContentViewDelegate = delegate; |
| } |
| |
| /** Removes the last resolved search URL from the Chrome history. */ |
| private void removeLastSearchVisit() { |
| assert mSearchPanel != null; |
| if (mLastSearchRequestLoaded != null) { |
| // TODO(pedrosimonetti): Consider having this feature builtin into OverlayPanelContent. |
| mSearchPanel.removeLastHistoryEntry( |
| mLastSearchRequestLoaded.getSearchUrl(), mLoadedSearchUrlTimeMs); |
| } |
| } |
| |
| /** |
| * Called when the Search content view navigates to a contextual search request URL. |
| * This navigation could be for a prefetch when the panel is still closed, or |
| * a load of a user-visible search result. |
| * @param isFailure Whether the navigation failed. |
| */ |
| private void onContextualSearchRequestNavigation(boolean isFailure) { |
| if (mSearchRequest == null) return; |
| |
| if (mSearchRequest.isUsingLowPriority()) { |
| ContextualSearchUma.logLowPrioritySearchRequestOutcome(isFailure); |
| } else { |
| ContextualSearchUma.logNormalPrioritySearchRequestOutcome(isFailure); |
| if (mSearchRequest.getHasFailed()) { |
| ContextualSearchUma.logFallbackSearchRequestOutcome(isFailure); |
| } |
| } |
| |
| if (isFailure && mSearchRequest.isUsingLowPriority()) { |
| // We're navigating to an error page, so we want to stop and retry. |
| // Stop loading the page that displays the error to the user. |
| if (getSearchPanelWebContents() != null) { |
| // When running tests the Content View might not exist. |
| mNetworkCommunicator.stopPanelContentsNavigation(); |
| } |
| mSearchRequest.setHasFailed(); |
| mSearchRequest.setNormalPriority(); |
| // If the content view is showing, load at normal priority now. |
| if (mSearchPanel != null && mSearchPanel.isContentShowing()) { |
| // NOTE: we must reuse the existing content view because we're called from within |
| // a WebContentsObserver. If we don't reuse the content view then the WebContents |
| // being observed will be deleted. We notify of the failure to trigger the reuse. |
| // See crbug.com/682953 for details. |
| mSearchPanel.onLoadUrlFailed(); |
| loadSearchUrl(); |
| } else { |
| mDidStartLoadingResolvedSearchRequest = false; |
| } |
| } |
| } |
| |
| @Override |
| public void logCurrentState() { |
| if (ContextualSearchFieldTrial.isEnabled()) mPolicy.logCurrentState(); |
| } |
| |
| /** @return Whether the given HTTP result code represents a failure or not. */ |
| private boolean isHttpFailureCode(int httpResultCode) { |
| return httpResultCode <= 0 || httpResultCode >= 400; |
| } |
| |
| /** @return whether a navigation in the search content view should promote to a separate tab. */ |
| private boolean shouldPromoteSearchNavigation() { |
| // A navigation can be due to us loading a URL, or a touch in the search content view. |
| // Require a touch, but no recent loading, in order to promote to a separate tab. |
| // Note that tapping the opt-in button requires checking for recent loading. |
| assert mSearchPanel != null; |
| return mSearchPanel.didTouchContent() && !mSearchPanel.isProcessingPendingNavigation(); |
| } |
| |
| /** |
| * Called to check if an external navigation is being done and take the appropriate action: |
| * Auto-promotes the panel into a separate tab if that's not already being done. |
| * @param url The URL we are navigating to. |
| */ |
| public void onExternalNavigation(String url) { |
| if (!mDidPromoteSearchNavigation && !BLACKLISTED_URL.equals(url) |
| && !url.startsWith(INTENT_URL_PREFIX) && shouldPromoteSearchNavigation() |
| && mSearchPanel != null) { |
| // Do not promote to a regular tab if we're loading our Resolved Search |
| // URL, otherwise we'll promote it when prefetching the Serp. |
| // Don't promote URLs when they are navigating to an intent - this is |
| // handled by the InterceptNavigationDelegate which uses a faster |
| // maximizing animation. |
| mDidPromoteSearchNavigation = true; |
| mSearchPanel.maximizePanelThenPromoteToTab(StateChangeReason.SERP_NAVIGATION); |
| } |
| } |
| |
| @Override |
| public void openResolvedSearchUrlInNewTab() { |
| if (mSearchRequest != null && mSearchRequest.getSearchUrlForPromotion() != null) { |
| TabModelSelector tabModelSelector = mActivity.getTabModelSelector(); |
| tabModelSelector.openNewTab( |
| new LoadUrlParams(mSearchRequest.getSearchUrlForPromotion()), |
| TabLaunchType.FROM_LINK, |
| tabModelSelector.getCurrentTab(), |
| tabModelSelector.isIncognitoSelected()); |
| } |
| } |
| |
| @Override |
| public boolean isRunningInCompatibilityMode() { |
| return SysUtils.isLowEndDevice(); |
| } |
| |
| @Override |
| public void promoteToTab() { |
| assert mSearchPanel != null; |
| // TODO(pedrosimonetti): Consider removing this member. |
| mIsPromotingToTab = true; |
| |
| // If the request object is null that means that a Contextual Search has just started |
| // and the Search Term Resolution response hasn't arrived yet. In this case, promoting |
| // the Panel to a Tab will result in creating a new tab with URL about:blank. To prevent |
| // this problem, we are ignoring tap gestures in the Search Bar if we don't know what |
| // to search for. |
| if (mSearchRequest != null && getSearchPanelWebContents() != null) { |
| String url = getContentViewUrl(getSearchPanelWebContents()); |
| |
| // If it's a search URL, format it so the SearchBox becomes visible. |
| if (mSearchRequest.isContextualSearchUrl(url)) { |
| url = mSearchRequest.getSearchUrlForPromotion(); |
| } |
| |
| if (url != null) { |
| mTabPromotionDelegate.createContextualSearchTab(url); |
| mSearchPanel.closePanel(StateChangeReason.TAB_PROMOTION, false); |
| } |
| } |
| mIsPromotingToTab = false; |
| } |
| |
| /** |
| * Gets the currently loading or loaded URL in a WebContents. |
| * |
| * @param searchWebContents The given WebContents. |
| * @return The current loaded URL. |
| */ |
| private String getContentViewUrl(WebContents searchWebContents) { |
| // First, check the pending navigation entry, because there might be an navigation |
| // not yet committed being processed. Otherwise, get the URL from the WebContents. |
| NavigationEntry entry = searchWebContents.getNavigationController().getPendingEntry(); |
| return entry != null ? entry.getUrl() : searchWebContents.getLastCommittedUrl(); |
| } |
| |
| @Override |
| public void dismissContextualSearchBar() { |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| } |
| |
| @Override |
| public void onPanelFinishedShowing() { |
| mInProductHelp.onPanelFinishedShowing( |
| mWasActivatedByTap, Profile.getLastUsedProfile().getOriginalProfile()); |
| } |
| |
| @Override |
| public void onPanelResized() { |
| mInProductHelp.updateBubblePosition(); |
| } |
| |
| /** @return The {@link SelectionClient} used by Contextual Search. */ |
| SelectionClient getContextualSearchSelectionClient() { |
| return mContextualSearchSelectionClient; |
| } |
| |
| /** |
| * Implements the {@link SelectionClient} interface for Contextual Search. |
| * Handles messages from Content about selection changes. These are the key drivers of |
| * Contextual Search logic. |
| */ |
| private class ContextualSearchSelectionClient implements SelectionClient { |
| @Override |
| public void onSelectionChanged(String selection) { |
| if (!isOverlayVideoMode() && mSearchPanel != null) { |
| mSelectionController.handleSelectionChanged(selection); |
| mSearchPanel.updateBrowserControlsState(BrowserControlsState.BOTH, true); |
| } |
| } |
| |
| @Override |
| public void onSelectionEvent( |
| @SelectionEventType int eventType, float posXPix, float posYPix) { |
| if (!isOverlayVideoMode()) { |
| mSelectionController.handleSelectionEvent(eventType, posXPix, posYPix); |
| } |
| } |
| |
| @Override |
| public void selectWordAroundCaretAck(boolean didSelect, int startAdjust, int endAdjust) { |
| if (mSelectWordAroundCaretCounter > 0) mSelectWordAroundCaretCounter--; |
| if (mSelectWordAroundCaretCounter > 0 |
| || !mInternalStateController.isStillWorkingOn( |
| InternalState.START_SHOWING_TAP_UI)) { |
| return; |
| } |
| |
| if (didSelect) { |
| assert mContext != null; |
| mContext.onSelectionAdjusted(startAdjust, endAdjust); |
| // There's a race condition when we select the word between this Ack response and |
| // the onSelectionChanged call. Update the selection in case this method won the |
| // race so we ensure that there's a valid selected word. |
| // See https://crbug.com/889657 for details. |
| String adjustedSelection = mContext.getSelection(); |
| if (!TextUtils.isEmpty(adjustedSelection)) { |
| mSelectionController.setSelectedText(adjustedSelection); |
| } |
| showSelectionAsSearchInBar(mSelectionController.getSelectedText()); |
| mInternalStateController.notifyFinishedWorkOn(InternalState.START_SHOWING_TAP_UI); |
| } else { |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| } |
| } |
| |
| @Override |
| public boolean requestSelectionPopupUpdates(boolean shouldSuggest) { |
| return false; |
| } |
| |
| @Override |
| public void cancelAllRequests() {} |
| } |
| |
| /** Shows the Unhandled Tap UI. Called by {@link ContextualSearchTabHelper}. */ |
| void onShowUnhandledTapUIIfNeeded(int x, int y, int fontSizeDips, int textRunLength) { |
| if (!isOverlayVideoMode()) { |
| mSelectionController.handleShowUnhandledTapUIIfNeeded( |
| x, y, fontSizeDips, textRunLength); |
| } |
| } |
| |
| /** @return Whether the display is in a full-screen video overlay mode. */ |
| private boolean isOverlayVideoMode() { |
| return mActivity.getFullscreenManager().isOverlayVideoMode(); |
| } |
| |
| // ============================================================================================ |
| // Selection |
| // ============================================================================================ |
| |
| /** |
| * Returns a new {@code GestureStateListener} that will listen for events in the Base Page. |
| * This listener will handle all Contextual Search-related interactions that go through the |
| * listener. |
| */ |
| public GestureStateListener getGestureStateListener() { |
| return mSelectionController.getGestureStateListener(); |
| } |
| |
| @Override |
| public void handleScrollStart() { |
| if (mIsAccessibilityModeEnabled) return; |
| |
| if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CONTEXTUAL_SEARCH_LONGPRESS_RESOLVE) |
| || mSelectionController.getSelectionType() == SelectionType.TAP) { |
| hideContextualSearch(StateChangeReason.BASE_PAGE_SCROLL); |
| } else if (mSelectionController.getSelectionType() == SelectionType.RESOLVING_LONG_PRESS) { |
| mSearchPanel.makePanelNotVisible(StateChangeReason.BASE_PAGE_SCROLL); |
| } |
| } |
| |
| @Override |
| public void handleScrollEnd() { |
| if (mSelectionController.getSelectionType() == SelectionType.RESOLVING_LONG_PRESS) { |
| mSearchPanel.showPanel(StateChangeReason.BASE_PAGE_SCROLL); |
| } |
| } |
| |
| @Override |
| public void handleInvalidTap() { |
| if (mIsAccessibilityModeEnabled) return; |
| |
| hideContextualSearch(StateChangeReason.BASE_PAGE_TAP); |
| } |
| |
| @Override |
| public void handleSuppressedTap() { |
| if (mIsAccessibilityModeEnabled) return; |
| |
| hideContextualSearch(StateChangeReason.TAP_SUPPRESS); |
| } |
| |
| @Override |
| public void handleNonSuppressedTap(long tapTimeNanoseconds) { |
| if (mIsAccessibilityModeEnabled) return; |
| |
| // If there's a wait-after-tap experiment then we may want to delay a bit longer for |
| // the user to take an action like scrolling that will reset our internal state. |
| long delayBeforeFinishingWorkMs = 0; |
| if (ContextualSearchFieldTrial.getValue(ContextualSearchSetting.WAIT_AFTER_TAP_DELAY_MS) > 0 |
| && tapTimeNanoseconds > 0) { |
| delayBeforeFinishingWorkMs = ContextualSearchFieldTrial.getValue( |
| ContextualSearchSetting.WAIT_AFTER_TAP_DELAY_MS) |
| - (System.nanoTime() - tapTimeNanoseconds) |
| / TimeUtils.NANOSECONDS_PER_MILLISECOND; |
| } |
| |
| // Finish work on the current state, either immediately or with a delay. |
| if (delayBeforeFinishingWorkMs <= 0) { |
| finishSuppressionDecision(); |
| } else { |
| new Handler().postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| finishSuppressionDecision(); |
| } |
| }, delayBeforeFinishingWorkMs); |
| } |
| } |
| |
| /** |
| * Finishes work on the suppression decision if that work is still in progress. |
| * If no longer working on the suppression decision then resets the Ranker-logger. |
| */ |
| private void finishSuppressionDecision() { |
| if (mInternalStateController.isStillWorkingOn(InternalState.DECIDING_SUPPRESSION)) { |
| mInternalStateController.notifyFinishedWorkOn(InternalState.DECIDING_SUPPRESSION); |
| } else { |
| mInteractionRecorder.reset(); |
| } |
| } |
| |
| @Override |
| public void handleMetricsForWouldSuppressTap(ContextualSearchHeuristics tapHeuristics) { |
| mQuickAnswersHeuristic = tapHeuristics.getQuickAnswersHeuristic(); |
| if (mSearchPanel != null) { |
| mSearchPanel.getPanelMetrics().setResultsSeenExperiments(tapHeuristics); |
| } |
| } |
| |
| @Override |
| public void handleValidTap() { |
| if (mIsAccessibilityModeEnabled) return; |
| |
| mInternalStateController.enter(InternalState.TAP_RECOGNIZED); |
| } |
| |
| @Override |
| public void handleValidResolvingLongpress() { |
| if (mIsAccessibilityModeEnabled || !mPolicy.canResolveLongpress()) return; |
| |
| mInternalStateController.enter(InternalState.RESOLVING_LONG_PRESS_RECOGNIZED); |
| } |
| |
| /** |
| * Notifies this class that the selection has changed. This may be due to the user moving the |
| * selection handles after a long-press, or after a Tap gesture has called selectWordAroundCaret |
| * to expand the selection to a whole word. |
| */ |
| @Override |
| public void handleSelection( |
| String selection, boolean selectionValid, @SelectionType int type, float x, float y) { |
| if (mIsAccessibilityModeEnabled) return; |
| |
| if (!selection.isEmpty()) { |
| ContextualSearchUma.logSelectionIsValid(selectionValid); |
| |
| if (selectionValid && mSearchPanel != null) { |
| mSearchPanel.updateBasePageSelectionYPx(y); |
| if (!mSearchPanel.isShowing()) { |
| mSearchPanel.getPanelMetrics().onSelectionEstablished(selection); |
| } |
| showSelectionAsSearchInBar(selection); |
| |
| if (type == SelectionType.LONG_PRESS) { |
| mInternalStateController.enter(InternalState.LONG_PRESS_RECOGNIZED); |
| } else if (type == SelectionType.RESOLVING_LONG_PRESS) { |
| mInternalStateController.enter(InternalState.RESOLVING_LONG_PRESS_RECOGNIZED); |
| } |
| } else { |
| hideContextualSearch(StateChangeReason.INVALID_SELECTION); |
| } |
| } |
| } |
| |
| @Override |
| public void handleSelectionDismissal() { |
| if (mIsAccessibilityModeEnabled) return; |
| |
| if (isSearchPanelShowing() |
| && !mIsPromotingToTab |
| // If the selection is dismissed when the Panel is not peeking anymore, |
| // which means the Panel is at least partially expanded, then it means |
| // the selection was cleared by an external source (like JavaScript), |
| // so we should not dismiss the UI in here. |
| // See crbug.com/516665 |
| && mSearchPanel.isPeeking()) { |
| hideContextualSearch(StateChangeReason.CLEARED_SELECTION); |
| } |
| } |
| |
| @Override |
| public void handleSelectionModification( |
| String selection, boolean selectionValid, float x, float y) { |
| if (mIsAccessibilityModeEnabled) return; |
| |
| if (isSearchPanelShowing()) { |
| if (selectionValid) { |
| mSearchPanel.setSearchTerm(selection); |
| } else { |
| hideContextualSearch(StateChangeReason.BASE_PAGE_TAP); |
| } |
| } |
| } |
| |
| @Override |
| public void handleSelectionCleared() { |
| // The selection was just cleared, so we'll want to remove our UX unless it was due to |
| // another Tap while the Bar is showing. |
| mInternalStateController.enter(InternalState.SELECTION_CLEARED_RECOGNIZED); |
| } |
| |
| @Override |
| public void logNonHeuristicFeatures(ContextualSearchInteractionRecorder rankerLogger) { |
| boolean didOptIn = !mPolicy.isUserUndecided(); |
| rankerLogger.logFeature(ContextualSearchInteractionRecorder.Feature.DID_OPT_IN, didOptIn); |
| boolean isHttp = mPolicy.isBasePageHTTP(getBasePageURL()); |
| rankerLogger.logFeature(ContextualSearchInteractionRecorder.Feature.IS_HTTP, isHttp); |
| String contentLanguage = mContext.getDetectedLanguage(); |
| boolean isLanguageMismatch = mTranslateController.needsTranslation(contentLanguage); |
| rankerLogger.logFeature(ContextualSearchInteractionRecorder.Feature.IS_LANGUAGE_MISMATCH, |
| isLanguageMismatch); |
| } |
| |
| /** Shows the given selection as the Search Term in the Bar. */ |
| private void showSelectionAsSearchInBar(String selection) { |
| if (isSearchPanelShowing()) mSearchPanel.setSearchTerm(selection); |
| } |
| |
| // ============================================================================================ |
| // ContextualSearchInternalStateHandler implementation. |
| // ============================================================================================ |
| |
| @VisibleForTesting |
| ContextualSearchInternalStateHandler getContextualSearchInternalStateHandler() { |
| return new ContextualSearchInternalStateHandler() { |
| @Override |
| public void hideContextualSearchUi(@StateChangeReason int reason) { |
| // Called when the IDLE state has been entered. |
| if (mContext != null) mContext.destroy(); |
| mContext = null; |
| if (mSearchPanel == null) return; |
| |
| // Make sure we write to Ranker and reset at the end of every search, even if the |
| // panel was not showing because it was a suppressed tap. |
| mSearchPanel.getPanelMetrics().writeInteractionOutcomesAndReset(); |
| if (isSearchPanelShowing()) { |
| mSearchPanel.closePanel(reason, false); |
| } else { |
| if (mSelectionController.getSelectionType() == SelectionType.TAP) { |
| mSelectionController.clearSelection(); |
| } |
| } |
| } |
| |
| @Override |
| public void gatherSurroundingText() { |
| if (mContext != null) mContext.destroy(); |
| mContext = new ContextualSearchContext() { |
| @Override |
| void onSelectionChanged() { |
| notifyObserversOfContextSelectionChanged(); |
| } |
| }; |
| |
| boolean isResolvingGesture = |
| mSelectionController.getSelectionType() == SelectionType.TAP |
| || mSelectionController.getSelectionType() |
| == SelectionType.RESOLVING_LONG_PRESS; |
| if (isResolvingGesture && mPolicy.shouldPreviousGestureResolve()) { |
| ContextualSearchInteractionPersister.PersistedInteraction interaction = |
| mInteractionRecorder.getInteractionPersister() |
| .getAndClearPersistedInteraction(); |
| mContext.setResolveProperties(mPolicy.getHomeCountry(mActivity), |
| mPolicy.maySendBasePageUrl(), interaction.getEventId(), |
| interaction.getEncodedUserInteractions()); |
| } |
| WebContents webContents = getBaseWebContents(); |
| if (webContents != null) { |
| mInternalStateController.notifyStartingWorkOn( |
| InternalState.GATHERING_SURROUNDINGS); |
| ContextualSearchManagerJni.get().gatherSurroundingText( |
| mNativeContextualSearchManagerPtr, ContextualSearchManager.this, |
| mContext, webContents); |
| } else { |
| mInternalStateController.reset(StateChangeReason.UNKNOWN); |
| } |
| } |
| |
| /** First step where we're committed to processing the current Tap gesture. */ |
| @Override |
| public void tapGestureCommit() { |
| mInternalStateController.notifyStartingWorkOn(InternalState.TAP_GESTURE_COMMIT); |
| if (!mPolicy.isTapSupported() |
| || mSelectionController.getSelectionType() |
| == SelectionType.RESOLVING_LONG_PRESS) { |
| hideContextualSearch(StateChangeReason.UNKNOWN); |
| return; |
| } |
| // We may be processing a chained search (aka a retap -- a tap near a previous tap). |
| // If it's chained we need to log the outcomes and reset, because we won't be hiding |
| // the panel at the end of the previous search (we'll update it to the new Search). |
| if (isSearchPanelShowing()) { |
| mSearchPanel.getPanelMetrics().writeInteractionOutcomesAndReset(); |
| } |
| // Set up the next batch of Ranker logging. |
| mInteractionRecorder.setupLoggingForPage(getBaseWebContents()); |
| mSearchPanel.getPanelMetrics().setInteractionRecorder(mInteractionRecorder); |
| ContextualSearchUma.logRankerFeaturesAvailable(false); |
| mInternalStateController.notifyFinishedWorkOn(InternalState.TAP_GESTURE_COMMIT); |
| } |
| |
| /** Starts the process of deciding if we'll suppress the current Tap gesture or not. */ |
| @Override |
| public void decideSuppression() { |
| mInternalStateController.notifyStartingWorkOn(InternalState.DECIDING_SUPPRESSION); |
| // TODO(donnd): Move handleShouldSuppressTap out of the Selection Controller. |
| mSelectionController.handleShouldSuppressTap(mContext, mInteractionRecorder); |
| } |
| |
| /** Starts showing the Tap UI by selecting a word around the current caret. */ |
| @Override |
| public void startShowingTapUi() { |
| WebContents baseWebContents = getBaseWebContents(); |
| if (baseWebContents != null) { |
| mInternalStateController.notifyStartingWorkOn( |
| InternalState.START_SHOWING_TAP_UI); |
| mSelectWordAroundCaretCounter++; |
| baseWebContents.selectWordAroundCaret(); |
| // Let the policy know that a valid tap gesture has been received. |
| mPolicy.registerTap(); |
| } else { |
| mInternalStateController.reset(StateChangeReason.UNKNOWN); |
| } |
| } |
| |
| /** |
| * Waits for possible Tap gesture that's near enough to the previous tap to be |
| * considered a "re-tap". We've done some work on the previous Tap and we just saw the |
| * selection get cleared (probably due to a Tap that may or may not be valid). |
| * If it's invalid we'll want to hide the UI. If it's valid we'll want to just update |
| * the UI rather than having the Bar hide and re-show. |
| */ |
| @Override |
| public void waitForPossibleTapNearPrevious() { |
| mInternalStateController.notifyStartingWorkOn( |
| InternalState.WAITING_FOR_POSSIBLE_TAP_NEAR_PREVIOUS); |
| new Handler().postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| mInternalStateController.notifyFinishedWorkOn( |
| InternalState.WAITING_FOR_POSSIBLE_TAP_NEAR_PREVIOUS); |
| } |
| }, TAP_NEAR_PREVIOUS_DETECTION_DELAY_MS); |
| } |
| |
| /** |
| * Waits for possible Tap gesture that's on a previously established tap-selection. |
| * If the current Tap was on the previous tap-selection then this selection will become |
| * a Long-press selection and we'll recognize that gesture and start processing it. |
| * If that doesn't happen within our time window (which is the common case) then we'll |
| * advance to the next state in normal Tap processing. |
| */ |
| @Override |
| public void waitForPossibleTapOnTapSelection() { |
| mInternalStateController.notifyStartingWorkOn( |
| InternalState.WAITING_FOR_POSSIBLE_TAP_ON_TAP_SELECTION); |
| new Handler().postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| mInternalStateController.notifyFinishedWorkOn( |
| InternalState.WAITING_FOR_POSSIBLE_TAP_ON_TAP_SELECTION); |
| } |
| }, TAP_ON_TAP_SELECTION_DELAY_MS); |
| } |
| |
| /** Starts a Resolve request to our server for the best Search Term. */ |
| @Override |
| public void resolveSearchTerm() { |
| mInternalStateController.notifyStartingWorkOn(InternalState.RESOLVING); |
| |
| String selection = mSelectionController.getSelectedText(); |
| assert !TextUtils.isEmpty(selection); |
| boolean isRestrictedResolve = |
| mPolicy.isPrivacyAggressiveResolveEnabled() && mPolicy.isPromoAvailable() |
| || mSelectionController.isAdjustedSelection(); |
| mNetworkCommunicator.startSearchTermResolutionRequest( |
| selection, isRestrictedResolve); |
| // If the we were unable to start the resolve, we've hidden the UI and set the |
| // context to null. |
| if (mContext == null || mSearchPanel == null) return; |
| |
| // Update the UI to show the resolve is in progress. |
| mSearchPanel.setContextDetails( |
| selection, mContext.getTextContentFollowingSelection()); |
| } |
| |
| @Override |
| public void showContextualSearchResolvingUi() { |
| if (mSelectionController.getSelectionType() == SelectionType.UNDETERMINED) { |
| mInternalStateController.reset(StateChangeReason.INVALID_SELECTION); |
| } else { |
| mInternalStateController.notifyStartingWorkOn(InternalState.SHOW_RESOLVING_UI); |
| boolean isTap = mSelectionController.getSelectionType() == SelectionType.TAP; |
| showContextualSearch(isTap ? StateChangeReason.TEXT_SELECT_TAP |
| : StateChangeReason.TEXT_SELECT_LONG_PRESS); |
| if (isTap) ContextualSearchUma.logRankerFeaturesAvailable(true); |
| mInternalStateController.notifyFinishedWorkOn(InternalState.SHOW_RESOLVING_UI); |
| } |
| } |
| |
| @Override |
| public void showContextualSearchLongpressUi() { |
| mInternalStateController.notifyStartingWorkOn( |
| InternalState.SHOWING_LONGPRESS_SEARCH); |
| showContextualSearch(StateChangeReason.TEXT_SELECT_LONG_PRESS); |
| mInternalStateController.notifyFinishedWorkOn( |
| InternalState.SHOWING_LONGPRESS_SEARCH); |
| } |
| }; |
| } |
| |
| /** |
| * @param reporter A context reporter for the feature to report the current selection when |
| * triggered. |
| */ |
| public void enableContextReporting(ContextReporterDelegate reporter) { |
| mContextReportingObserver = new ContextualSearchObserver() { |
| @Override |
| public void onShowContextualSearch(GSAContextDisplaySelection contextSelection) { |
| if (contextSelection != null) reporter.reportDisplaySelection(contextSelection); |
| } |
| |
| @Override |
| public void onHideContextualSearch() { |
| reporter.reportDisplaySelection(null); |
| } |
| }; |
| addObserver(mContextReportingObserver); |
| } |
| |
| /** |
| * Disable context reporting for Contextual Search. |
| */ |
| public void disableContextReporting() { |
| removeObserver(mContextReportingObserver); |
| mContextReportingObserver = null; |
| } |
| |
| // ============================================================================================ |
| // Test helpers |
| // ============================================================================================ |
| |
| /** |
| * Sets the {@link ContextualSearchNetworkCommunicator} to use for server requests. |
| * @param networkCommunicator The communicator for all future requests. |
| */ |
| @VisibleForTesting |
| void setNetworkCommunicator(ContextualSearchNetworkCommunicator networkCommunicator) { |
| mNetworkCommunicator = networkCommunicator; |
| mPolicy.setNetworkCommunicator(mNetworkCommunicator); |
| } |
| |
| /** @return The ContextualSearchPolicy currently being used. */ |
| @VisibleForTesting |
| ContextualSearchPolicy getContextualSearchPolicy() { |
| return mPolicy; |
| } |
| |
| /** @param policy The {@link ContextualSearchPolicy} for testing. */ |
| @VisibleForTesting |
| void setContextualSearchPolicy(ContextualSearchPolicy policy) { |
| mPolicy = policy; |
| } |
| |
| /** @return The {@link ContextualSearchPanel}, for testing purposes only. */ |
| @VisibleForTesting |
| ContextualSearchPanel getContextualSearchPanel() { |
| return mSearchPanel; |
| } |
| |
| /** @return The selection controller, for testing purposes. */ |
| @VisibleForTesting |
| ContextualSearchSelectionController getSelectionController() { |
| return mSelectionController; |
| } |
| |
| /** @param controller The {@link ContextualSearchSelectionController}, for testing purposes. */ |
| @VisibleForTesting |
| void setSelectionController(ContextualSearchSelectionController controller) { |
| mSelectionController = controller; |
| } |
| |
| /** @return The current search request, or {@code null} if there is none, for testing. */ |
| @VisibleForTesting |
| ContextualSearchRequest getRequest() { |
| return mSearchRequest; |
| } |
| |
| @VisibleForTesting |
| ContextualSearchTabPromotionDelegate getTabPromotionDelegate() { |
| return mTabPromotionDelegate; |
| } |
| |
| @VisibleForTesting |
| void setContextualSearchInternalStateController( |
| ContextualSearchInternalStateController controller) { |
| mInternalStateController = controller; |
| } |
| |
| @VisibleForTesting |
| protected ContextualSearchInternalStateController getContextualSearchInternalStateController() { |
| return mInternalStateController; |
| } |
| |
| @VisibleForTesting |
| ContextualSearchInteractionRecorder getRankerLogger() { |
| return mInteractionRecorder; |
| } |
| |
| @VisibleForTesting |
| ContextualSearchContext getContext() { |
| return mContext; |
| } |
| |
| @NativeMethods |
| interface Natives { |
| long init(ContextualSearchManager caller); |
| |
| void destroy(long nativeContextualSearchManager, ContextualSearchManager caller); |
| void startSearchTermResolutionRequest(long nativeContextualSearchManager, |
| ContextualSearchManager caller, ContextualSearchContext contextualSearchContext, |
| WebContents baseWebContents); |
| void gatherSurroundingText(long nativeContextualSearchManager, |
| ContextualSearchManager caller, ContextualSearchContext contextualSearchContext, |
| WebContents baseWebContents); |
| void whitelistContextualSearchJsApiUrl( |
| long nativeContextualSearchManager, ContextualSearchManager caller, String url); |
| void enableContextualSearchJsApiForWebContents(long nativeContextualSearchManager, |
| ContextualSearchManager caller, WebContents overlayWebContents); |
| // Don't call these directly, instead call the private methods that cache the results. |
| String getTargetLanguage( |
| long nativeContextualSearchManager, ContextualSearchManager caller); |
| |
| String getAcceptLanguages( |
| long nativeContextualSearchManager, ContextualSearchManager caller); |
| } |
| } |