blob: 0566cc133df20f913e9ea1ae28e890a98818a642 [file] [log] [blame]
// 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.annotation.SuppressLint;
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 org.chromium.base.ObserverList;
import org.chromium.base.SysUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
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.ContextualSearchBlacklist.BlacklistReason;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchInternalStateController.InternalState;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchSelectionController.SelectionType;
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.gsa.GSAContextDisplaySelection;
import org.chromium.chrome.browser.infobar.InfoBarContainer;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabRedirectHandler;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabModel.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.chrome.browser.widget.findinpage.FindToolbarManager;
import org.chromium.chrome.browser.widget.findinpage.FindToolbarObserver;
import org.chromium.components.navigation_interception.NavigationParams;
import org.chromium.content.browser.ContentViewCore;
import org.chromium.content.browser.SelectionClient;
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.WebContents;
import org.chromium.content_public.common.BrowserControlsState;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.net.NetworkChangeNotifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* 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, SelectionClient {
// 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 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;
private final ObserverList<ContextualSearchObserver> mObservers =
new ObserverList<ContextualSearchObserver>();
private final ChromeActivity mActivity;
private final ContextualSearchTabPromotionDelegate mTabPromotionDelegate;
private final ViewTreeObserver.OnGlobalFocusChangeListener mOnFocusChangeListener;
private final TabModelObserver mTabModelObserver;
private ContextualSearchSelectionController mSelectionController;
private ContextualSearchNetworkCommunicator mNetworkCommunicator;
private ContextualSearchPolicy mPolicy;
private ContextualSearchInternalStateController mInternalStateController;
@VisibleForTesting
protected ContextualSearchTranslateController 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 TabModelSelectorTabObserver mTabModelSelectorTabObserver;
private FindToolbarManager mFindToolbarManager;
private FindToolbarObserver mFindToolbarObserver;
private boolean mDidStartLoadingResolvedSearchRequest;
private long mLoadedSearchUrlTimeMs;
// TODO(donnd): consider changing this member's name to indicate "opened" instead of "seen".
private boolean mWereSearchResultsSeen;
private boolean mWereInfoBarsHidden;
private boolean mDidPromoteSearchNavigation;
private boolean mWasActivatedByTap;
private boolean mIsInitialized;
// 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 mIsShowingPeekPromo;
private boolean mWouldShowPeekPromo;
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;
// TODO(pedrosimonetti): also store selected text, surroundings, url, bounding rect of selected
// text, and make sure that all states are cleared when starting a new contextual search to
// avoid having the values in a funky state.
private ContextualSearchRequest mSearchRequest;
private ContextualSearchRequest mLastSearchRequestLoaded;
/** Whether the Accessibility Mode is enabled. */
private boolean mIsAccessibilityModeEnabled;
/** Tap Experiments and other variable behavior. */
private ContextualSearchHeuristics mHeuristics;
private QuickAnswersHeuristic mQuickAnswersHeuristic;
/**
* The delegate that is responsible for promoting a {@link ContentViewCore} to a {@link Tab}
* when necessary.
*/
public interface ContextualSearchTabPromotionDelegate {
/**
* Called when {@code searchContentViewCore} 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 ContentViewCore}s.
*/
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);
}
}
};
mTabModelObserver = new EmptyTabModelObserver() {
@Override
public void didSelectTab(Tab tab, TabSelectionType type, int lastId) {
if (!mIsPromotingToTab && tab.getId() != lastId
|| mActivity.getTabModelSelector().isIncognitoSelected()) {
hideContextualSearch(StateChangeReason.UNKNOWN);
mSelectionController.onTabSelected();
}
}
@Override
public void didAddTab(Tab tab, TabLaunchType type) {
// If we're in the process of promoting this tab, just return and don't mess with
// this state.
if (tab.getContentViewCore() == mSearchPanel.getContentViewCore()) return;
hideContextualSearch(StateChangeReason.UNKNOWN);
}
};
mSelectionController = new ContextualSearchSelectionController(activity, this);
mNetworkCommunicator = this;
mPolicy = new ContextualSearchPolicy(mSelectionController, mNetworkCommunicator);
mTranslateController = new ContextualSearchTranslateController(activity, mPolicy, this);
mInternalStateController = new ContextualSearchInternalStateController(
mPolicy, getContextualSearchInternalStateHandler());
}
/**
* Initializes this manager.
* @param parentView The parent view to attach Contextual Search UX to.
*/
public void initialize(ViewGroup parentView) {
mNativeContextualSearchManagerPtr = nativeInit();
mParentView = parentView;
mParentView.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnFocusChangeListener);
mTabRedirectHandler = new TabRedirectHandler(mActivity);
mIsShowingPromo = false;
mDidLogPromoOutcome = false;
mDidStartLoadingResolvedSearchRequest = false;
mWereSearchResultsSeen = false;
mIsInitialized = true;
mInternalStateController.reset(StateChangeReason.UNKNOWN);
listenForTabModelSelectorNotifications();
}
/**
* Sets the {@link FindToolbarManager} and attaches an observer that dismisses the Contextual
* Search panel when the find toolbar is shown.
*
* @param findToolbarManager The {@link FindToolbarManager} for the current activity.
*/
public void setFindToolbarManager(FindToolbarManager findToolbarManager) {
if (mFindToolbarManager != null) {
mFindToolbarManager.removeObserver(mFindToolbarObserver);
}
mFindToolbarManager = findToolbarManager;
if (mFindToolbarObserver == null) {
mFindToolbarObserver = new FindToolbarObserver() {
@Override
public void onFindToolbarShown() {
hideContextualSearch(StateChangeReason.UNKNOWN);
}
};
}
mFindToolbarManager.addObserver(mFindToolbarObserver);
}
/**
* 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);
nativeDestroy(mNativeContextualSearchManagerPtr);
stopListeningForHideNotifications();
mTabRedirectHandler.clear();
if (mFindToolbarManager != null) {
mFindToolbarManager.removeObserver(mFindToolbarObserver);
mFindToolbarManager = null;
mFindToolbarObserver = null;
}
mInternalStateController.enter(InternalState.UNDEFINED);
}
@Override
public void setContextualSearchPanel(ContextualSearchPanel panel) {
mSearchPanel = panel;
mPolicy.setContextualSearchPanel(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.isPanelOpened();
}
/** @return The Base Page's {@link ContentViewCore}. */
@Nullable
private WebContents getBaseWebContents() {
return mSelectionController.getBaseWebContents();
}
/** 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();
}
/**
* Hides the Contextual Search UX by changing into the IDLE state.
* @param reason The {@link StateChangeReason} for hiding Contextual Search.
*/
public void hideContextualSearch(StateChangeReason reason) {
mInternalStateController.reset(reason);
}
@Override
public void onCloseContextualSearch(StateChangeReason 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.setIsObscuredByOtherView(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;
if (mIsShowingPeekPromo || mWouldShowPeekPromo) {
mPolicy.logPeekPromoMetrics(mIsShowingPeekPromo, mWouldShowPeekPromo);
}
if (mIsShowingPromo && !mDidLogPromoOutcome && mSearchPanel.wasPromoInteractive()) {
ContextualSearchUma.logPromoOutcome(mWasActivatedByTap, mIsMandatoryPromo);
mDidLogPromoOutcome = true;
}
mIsShowingPromo = false;
mSearchPanel.setIsPromoActive(false, false);
notifyHideContextualSearch();
}
/** Called when the system back button is pressed. Will hide the layout. */
public boolean onBackPressed() {
if (!mIsInitialized || !mSearchPanel.isShowing()) return false;
hideContextualSearch(StateChangeReason.BACK_PRESS);
return true;
}
/**
* Shows the Contextual Search UX.
* @param stateChangeReason The reason explaining the change of state.
*/
private void showContextualSearch(StateChangeReason stateChangeReason) {
if (mFindToolbarManager != null) {
mFindToolbarManager.hideToolbar(false);
}
// 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.setIsObscuredByOtherView(true);
}
}
// If the user is jumping from one unseen search to another search, remove the last search
// from history.
PanelState state = mSearchPanel.getPanelState();
if (!mWereSearchResultsSeen && mLoadedSearchUrlTimeMs != 0L
&& state != PanelState.UNDEFINED && state != PanelState.CLOSED) {
removeLastSearchVisit();
}
mSearchPanel.destroyContent();
String selection = mSelectionController.getSelectedText();
boolean isTap = mSelectionController.getSelectionType() == SelectionType.TAP;
if (isTap) {
// If the user action was not a long-press, we should not delay before loading content.
mShouldLoadDelayedSearch = false;
}
if (isTap && mPolicy.shouldPreviousTapResolve()) {
// Cache the native translate data, so JNI calls won't be made when time-critical.
mTranslateController.cacheNativeTranslateData();
} else {
boolean shouldPrefetch = mPolicy.shouldPrefetchSearchResult();
mSearchRequest = createContextualSearchRequest(selection, null, null, 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 (!isTap && mSearchPanel.isPeeking()) {
boolean isSingleWord =
!CONTAINS_WHITESPACE_PATTERN.matcher(selection.trim()).find();
RecordUserAction.record(isSingleWord ? "ContextualSearch.ManualRefineSingleWord"
: "ContextualSearch.ManualRefineMultiWord");
}
}
mWereSearchResultsSeen = false;
// Show the Peek Promo only when the Panel wasn't previously visible, provided
// the policy allows it.
if (!mSearchPanel.isShowing()) {
mWouldShowPeekPromo = mPolicy.isPeekPromoConditionSatisfied();
mIsShowingPeekPromo = mPolicy.isPeekPromoAvailable();
if (mIsShowingPeekPromo) {
mSearchPanel.showPeekPromo();
mPolicy.registerPeekPromoSeen();
}
}
// 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();
}
// TODO(donnd): If there was a previously ongoing contextual search, we should ensure
// it's registered as closed.
mSearchPanel.requestPanelShow(stateChangeReason);
assert mSelectionController.getSelectionType() != SelectionType.UNDETERMINED;
mWasActivatedByTap = mSelectionController.getSelectionType() == SelectionType.TAP;
}
@Override
public void startSearchTermResolutionRequest(String selection) {
WebContents baseWebContents = getBaseWebContents();
if (baseWebContents != null && mContext != null && mContext.canResolve()) {
nativeStartSearchTermResolutionRequest(
mNativeContextualSearchManagerPtr, 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.getUrl());
} catch (MalformedURLException e) {
return null;
}
}
/**
* A method that can override the creation of a standard search request. This should only be
* used for testing.
* @param term The search term to create the request with.
* @param altTerm An alternate search term.
* @param isLowPriorityEnabled Whether the request can be made at low priority.
*/
protected ContextualSearchRequest createContextualSearchRequest(
String term, String altTerm, String mid, boolean isLowPriorityEnabled) {
return new ContextualSearchRequest(term, altTerm, mid, isLowPriorityEnabled);
}
/** Accessor for the {@code InfoBarContainer} currently attached to the {@code Tab}. */
private InfoBarContainer getInfoBarContainer() {
Tab tab = mActivity.getActivityTab();
return tab == null ? null : tab.getInfoBarContainer();
}
/** Listens for notifications that should hide the Contextual Search bar. */
private void listenForTabModelSelectorNotifications() {
TabModelSelector selector = mActivity.getTabModelSelector();
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, boolean sadTabShown) {
if (sadTabShown) {
// Hide contextual search if the foreground tab crashed
hideContextualSearch(StateChangeReason.UNKNOWN);
}
}
@Override
public void onClosingStateChanged(Tab tab, boolean closing) {
if (closing) hideContextualSearch(StateChangeReason.UNKNOWN);
}
};
for (TabModel tabModel : selector.getModels()) {
tabModel.addObserver(mTabModelObserver);
}
}
/** Stops listening for notifications that should hide the Contextual Search bar. */
private void stopListeningForHideNotifications() {
if (mTabModelSelectorTabObserver != null) mTabModelSelectorTabObserver.destroy();
TabModelSelector selector = mActivity.getTabModelSelector();
if (selector != null) {
for (TabModel tabModel : selector.getModels()) {
tabModel.removeObserver(mTabModelObserver);
}
}
}
/** 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
private 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 ContextualSearchManager#nativeStartSearchTermResolutionRequest} method.
* If {@code nativeStartSearchTermResolutionRequest} 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.
*/
@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, final int quickActionCategory) {
mNetworkCommunicator.handleSearchTermResolutionResponse(isNetworkUnavailable, responseCode,
searchTerm, displayText, alternateTerm, mid, doPreventPreload, selectionStartAdjust,
selectionEndAdjust, contextLanguage, thumbnailUrl, caption, quickActionUri,
quickActionCategory);
}
@SuppressLint("StringFormatMatches")
@Override
public void handleSearchTermResolutionResponse(boolean isNetworkUnavailable, int responseCode,
String searchTerm, String displayText, String alternateTerm, String mid,
boolean doPreventPreload, int selectionStartAdjust, int selectionEndAdjust,
String contextLanguage, String thumbnailUrl, String caption, String quickActionUri,
int quickActionCategory) {
if (!mInternalStateController.isStillWorkingOn(InternalState.RESOLVING)) return;
// Show an appropriate message for what to search for.
String message;
boolean doLiteralSearch = false;
if (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(responseCode) && !TextUtils.isEmpty(displayText)) {
message = displayText;
} else if (!mPolicy.shouldShowErrorCodeInBar()) {
message = mSelectionController.getSelectedText();
doLiteralSearch = true;
} else {
// TODO(crbug.com/635567): Fix lint properly.
message = mActivity.getResources().getString(
R.string.contextual_search_error, responseCode);
doLiteralSearch = true;
}
boolean receivedCaptionOrThumbnail = !TextUtils.isEmpty(caption)
|| !TextUtils.isEmpty(thumbnailUrl);
mSearchPanel.onSearchTermResolved(message, thumbnailUrl, quickActionUri,
quickActionCategory);
if (!TextUtils.isEmpty(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(caption, doesAnswer);
}
boolean quickActionShown =
mSearchPanel.getSearchBarControl().getQuickActionControl().hasQuickAction();
boolean receivedContextualCardsEntityData = !quickActionShown && receivedCaptionOrThumbnail;
ContextualSearchUma.logContextualCardsDataShown(receivedContextualCardsEntityData);
mSearchPanel.getPanelMetrics().setWasContextualCardsDataShown(
receivedContextualCardsEntityData);
if (ContextualSearchFieldTrial.isContextualSearchSingleActionsEnabled()) {
ContextualSearchUma.logQuickActionShown(quickActionShown, quickActionCategory);
mSearchPanel.getPanelMetrics().setWasQuickActionShown(quickActionShown,
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.
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 =
createContextualSearchRequest(searchTerm, alternateTerm, mid, shouldPreload);
// Trigger translation, if enabled.
mTranslateController.forceTranslateIfNeeded(mSearchRequest, 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.
if ((selectionStartAdjust != 0 || selectionEndAdjust != 0)
&& mSelectionController.getSelectionType() == SelectionType.TAP) {
String originalSelection = mContext == null ? null : mContext.getInitialSelectedWord();
if (originalSelection != null
&& originalSelection.equals(mSelectionController.getSelectedText())) {
mSelectionController.adjustSelection(selectionStartAdjust, selectionEndAdjust);
mContext.onSelectionAdjusted(selectionStartAdjust, selectionEndAdjust);
}
}
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() {
mLoadedSearchUrlTimeMs = System.currentTimeMillis();
mLastSearchRequestLoaded = mSearchRequest;
mSearchPanel.loadUrlInPanel(mSearchRequest.getSearchUrl());
mDidStartLoadingResolvedSearchRequest = true;
// TODO(pedrosimonetti): 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 ContentViewCore.onShow() while it's being created has no effect. Need
// to coordinate with Chrome-Android folks to come up with a proper fix for this.
// 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() && mSearchPanel.getContentViewCore() != null) {
mSearchPanel.getContentViewCore().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)) 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);
}
/**
* Notifies that the Accessibility Mode state has changed.
*
* @param enabled Whether the Accessibility Mode is enabled.
*/
public void onAccessibilityModeChanged(boolean enabled) {
mIsAccessibilityModeEnabled = enabled;
}
/**
* 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() {
mSearchPanel.getContentViewCore().getWebContents().stop();
}
// ============================================================================================
// Observers
// ============================================================================================
/** @param observer An observer to notify when the user performs a contextual search. */
public void addObserver(ContextualSearchObserver observer) {
mObservers.addObserver(observer);
}
/** @param observer An observer to no longer notify when the user performs a contextual search.
*/
public 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.isPageContentNotificationDisabled()) {
GSAContextDisplaySelection selection = new GSAContextDisplaySelection(
mContext.getEncoding(), surroundingText, startOffset, endOffset);
notifyShowContextualSearch(selection);
}
mSearchPanel.setWasSelectionPartOfUrl(
ContextualSearchSelectionController.isSelectionPartOfUrl(
surroundingText, startOffset, endOffset));
}
/**
* 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 nativeGetAcceptLanguages(mNativeContextualSearchManagerPtr);
}
@Override
public String getTranslateServiceTargetLanguage() {
// TODO(donnd): remove once issue 607127 has been resolved.
if (mNativeContextualSearchManagerPtr == 0) {
throw new RuntimeException("mNativeContextualSearchManagerPtr is 0!");
}
return nativeGetTargetLanguage(mNativeContextualSearchManagerPtr);
}
// ============================================================================================
// 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) {
mSearchPanel.updateBrowserControlsState();
if (isExternalUrl) {
onExternalNavigation(url);
}
}
@Override
public void onMainFrameNavigation(String url, boolean isExternalUrl, boolean isFailure) {
if (isExternalUrl) {
if (!ContextualSearchFieldTrial.isAmpAsSeparateTabDisabled()
&& 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.shouldPreviousTapResolve();
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()) {
mSearchRequest = createContextualSearchRequest(
mSelectionController.getSelectedText(), null, null, false);
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(ContentViewCore contentViewCore) {
// TODO(donnd): Consider moving to OverlayPanelContent.
// Enable the Contextual Search JavaScript API between our service and the new view.
nativeEnableContextualSearchJsApiForOverlay(
mNativeContextualSearchManagerPtr, contentViewCore.getWebContents());
// TODO(mdjones): Move SearchContentViewDelegate ownership to panel.
mSearchContentViewDelegate.setOverlayPanelContentViewCore(contentViewCore);
}
@Override
public void onContentViewDestroyed() {
if (mSearchContentViewDelegate != null) {
mSearchContentViewDelegate.releaseOverlayPanelContentViewCore();
}
}
@Override
public void onContentViewSeen() {
mSearchPanel.setWasSearchContentViewSeen();
}
@Override
public boolean shouldInterceptNavigation(
ExternalNavigationHandler externalNavHandler, NavigationParams navigationParams) {
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;
}
if (navigationParams.isExternalProtocol) {
return false;
}
return true;
}
}
// ============================================================================================
// 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() {
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 (mSearchPanel.getContentViewCore() != 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.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.
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()) {
// 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() {
// 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
&& mSearchPanel.getContentViewCore() != null
&& mSearchPanel.getContentViewCore().getWebContents() != null) {
String url = getContentViewUrl(mSearchPanel.getContentViewCore());
// 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 current loaded URL in a ContentViewCore.
*
* @param searchContentViewCore The given ContentViewCore.
* @return The current loaded URL.
*/
private String getContentViewUrl(ContentViewCore searchContentViewCore) {
// 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 =
searchContentViewCore.getWebContents().getNavigationController().getPendingEntry();
String url =
entry != null ? entry.getUrl() : searchContentViewCore.getWebContents().getUrl();
return url;
}
@Override
public void dismissContextualSearchBar() {
hideContextualSearch(StateChangeReason.UNKNOWN);
}
// ============================================================================================
// SelectionClient -- interface used by ContentViewCore.
// ============================================================================================
@Override
public void onSelectionChanged(String selection) {
if (!isOverlayVideoMode()) {
mSelectionController.handleSelectionChanged(selection);
mSearchPanel.updateBrowserControlsState(BrowserControlsState.BOTH, true);
}
}
@Override
public void onSelectionEvent(int eventType, float posXPix, float posYPix) {
if (!isOverlayVideoMode()) {
mSelectionController.handleSelectionEvent(eventType, posXPix, posYPix);
}
}
@Override
public void showUnhandledTapUIIfNeeded(final int x, final int y) {
if (!isOverlayVideoMode()) {
mSelectionController.handleShowUnhandledTapUIIfNeeded(x, y);
}
}
@Override
public boolean requestSelectionPopupUpdates(boolean shouldSuggest) {
return false;
}
@Override
public void cancelAllRequests() {}
// TODO(donnd): add handling of an ACK to selectWordAroundCaret (crbug.com/435778 has details).
/**
* @return Whether the display is in a full-screen video overlay mode.
*/
private boolean isOverlayVideoMode() {
return mActivity.getFullscreenManager() != null
&& 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 handleScroll() {
if (mIsAccessibilityModeEnabled) return;
hideContextualSearch(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.BASE_PAGE_TAP);
}
@Override
public void handleNonSuppressedTap() {
if (mIsAccessibilityModeEnabled) return;
mInternalStateController.notifyFinishedWorkOn(InternalState.DECIDING_SUPPRESSION);
}
@Override
public void handleMetricsForWouldSuppressTap(ContextualSearchHeuristics tapHeuristics) {
mHeuristics = tapHeuristics;
// TODO(donnd): QuickAnswersHeuristic is getting added to TapSuppressionHeuristics and
// and getting considered in TapSuppressionHeuristics#shouldSuppressTap(). It should
// be a part of ContextualSearchHeuristics for logging purposes but not for suppression.
mQuickAnswersHeuristic = new QuickAnswersHeuristic();
mHeuristics.add(mQuickAnswersHeuristic);
mSearchPanel.getPanelMetrics().setResultsSeenExperiments(mHeuristics);
mSearchPanel.getPanelMetrics().setRankerLogExperiments(mHeuristics);
}
@Override
public void handleValidTap() {
if (mIsAccessibilityModeEnabled) return;
mInternalStateController.enter(InternalState.TAP_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 type,
float x, float y) {
if (mIsAccessibilityModeEnabled) return;
if (!selection.isEmpty()) {
ContextualSearchUma.logSelectionIsValid(selectionValid);
// Update the context so it knows the selection has changed.
if (mContext != null) mContext.updateContextFromSelection(selection);
if (selectionValid && mSearchPanel != null) {
mSearchPanel.updateBasePageSelectionYPx(y);
if (!mSearchPanel.isShowing()) {
mSearchPanel.getPanelMetrics().onSelectionEstablished(selection);
}
showSelectionAsSearchInBar(selection);
// TODO(donnd): remove this complication when we get an ACK message from
// selectWordAroundCaret (see crbug.com/435778).
if (type == SelectionType.TAP) {
// Make sure we have a context -- we'll need one to show the UI.
if (mContext == null) {
// Some unknown failure happened, hide the UI.
hideContextualSearch(StateChangeReason.UNKNOWN);
return;
}
mInternalStateController.notifyFinishedWorkOn(
InternalState.START_SHOWING_TAP_UI);
} else {
mInternalStateController.enter(InternalState.LONG_PRESS_RECOGNIZED);
}
} else {
hideContextualSearch(StateChangeReason.INVALID_SELECTION);
}
}
}
@Override
public void handleSelectionDismissal() {
if (mIsAccessibilityModeEnabled) return;
if (mSearchPanel != null && mSearchPanel.isShowing()
&& !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 (mSearchPanel != null && mSearchPanel.isShowing()) {
if (selectionValid) {
mSearchPanel.setSearchTerm(selection);
} else {
hideContextualSearch(StateChangeReason.BASE_PAGE_TAP);
}
}
}
@Override
public void handleSelectionSuppression(BlacklistReason reason) {
if (mIsAccessibilityModeEnabled) return;
if (mSearchPanel != null) mSearchPanel.getPanelMetrics().setBlacklistReason(reason);
}
@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);
}
/** Shows the given selection as the Search Term in the Bar. */
private void showSelectionAsSearchInBar(String selection) {
if (mSearchPanel.isShowing()) mSearchPanel.setSearchTerm(selection);
}
// ============================================================================================
// ContextualSearchInternalStateHandler implementation.
// ============================================================================================
@VisibleForTesting
ContextualSearchInternalStateHandler getContextualSearchInternalStateHandler() {
return new ContextualSearchInternalStateHandler() {
@Override
public void hideContextualSearchUi(StateChangeReason reason) {
// Called when the IDLE state has been entered.
if (mContext != null) mContext.destroy();
mContext = null;
if (mSearchPanel == null) return;
if (mSearchPanel.isShowing()) {
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 isTap = mSelectionController.getSelectionType() == SelectionType.TAP;
if (isTap && mPolicy.shouldPreviousTapResolve()) {
mContext.setResolveProperties(
mPolicy.getHomeCountry(mActivity), mPolicy.maySendBasePageUrl());
}
WebContents webContents = getBaseWebContents();
if (webContents != null) {
mInternalStateController.notifyStartingWorkOn(
InternalState.GATHERING_SURROUNDINGS);
nativeGatherSurroundingText(
mNativeContextualSearchManagerPtr, mContext, webContents);
} else {
mInternalStateController.reset(StateChangeReason.UNKNOWN);
}
}
/** Starts the process of deciding if we'll suppress the current Tap gesture or not. */
@Override
public void decideSuppression() {
mInternalStateController.notifyStartingWorkOn(InternalState.DECIDING_SUPPRESSION);
mSelectionController.handleShouldSuppressTap();
}
/** Starts showing the Tap UI by selecting a word around the current caret. */
@Override
public void startShowingTapUi() {
WebContents baseWebContents = getBaseWebContents();
// TODO(donnd): Call isTapSupported earlier so we don't waste time gathering
// surrounding text and deciding suppression when unsupported, or remove the whole
// idea of unsupported taps in favor of deciding suppression better.
// Details in crbug.com/715297.
if (baseWebContents != null && mPolicy.isTapSupported()) {
mInternalStateController.notifyStartingWorkOn(
InternalState.START_SHOWING_TAP_UI);
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);
}
/** 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);
mNetworkCommunicator.startSearchTermResolutionRequest(selection);
// Update the UI to show the resolve is in progress.
assert mContext != null;
assert mContext.getTextContentFollowingSelection() != null;
mSearchPanel.setContextDetails(
selection, mContext.getTextContentFollowingSelection());
}
@Override
public void showContextualSearchTapUi() {
mInternalStateController.notifyStartingWorkOn(InternalState.SHOW_FULL_TAP_UI);
showContextualSearch(StateChangeReason.TEXT_SELECT_TAP);
mInternalStateController.notifyFinishedWorkOn(InternalState.SHOW_FULL_TAP_UI);
}
@Override
public void showContextualSearchLongpressUi() {
mInternalStateController.notifyStartingWorkOn(
InternalState.SHOWING_LONGPRESS_SEARCH);
showContextualSearch(StateChangeReason.TEXT_SELECT_LONG_PRESS);
mInternalStateController.notifyFinishedWorkOn(
InternalState.SHOWING_LONGPRESS_SEARCH);
}
};
}
// ============================================================================================
// 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;
}
// ============================================================================================
// Native calls
// ============================================================================================
private native long nativeInit();
private native void nativeDestroy(long nativeContextualSearchManager);
private native void nativeStartSearchTermResolutionRequest(long nativeContextualSearchManager,
ContextualSearchContext contextualSearchContext, WebContents baseWebContents);
protected native void nativeGatherSurroundingText(long nativeContextualSearchManager,
ContextualSearchContext contextualSearchContext, WebContents baseWebContents);
private native void nativeEnableContextualSearchJsApiForOverlay(
long nativeContextualSearchManager, WebContents overlayWebContents);
// Don't call these directly, instead call the private methods that cache the results.
private native String nativeGetTargetLanguage(long nativeContextualSearchManager);
private native String nativeGetAcceptLanguages(long nativeContextualSearchManager);
}