| Index: chrome/android/java_staging/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java
|
| diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..f282abc0ae6b982239e6f33bef7e2ad72104cd7c
|
| --- /dev/null
|
| +++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java
|
| @@ -0,0 +1,1381 @@
|
| +// 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.app.Activity;
|
| +import android.os.Handler;
|
| +import android.util.Log;
|
| +import android.view.View;
|
| +import android.view.ViewGroup;
|
| +import android.view.ViewTreeObserver;
|
| +import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
|
| +
|
| +import com.google.android.apps.chrome.R;
|
| +
|
| +import org.chromium.base.ActivityState;
|
| +import org.chromium.base.ApplicationStatus;
|
| +import org.chromium.base.ApplicationStatus.ActivityStateListener;
|
| +import org.chromium.base.CalledByNative;
|
| +import org.chromium.base.SysUtils;
|
| +import org.chromium.base.VisibleForTesting;
|
| +import org.chromium.chrome.browser.ChromeActivity;
|
| +import org.chromium.chrome.browser.ContentViewUtil;
|
| +import org.chromium.chrome.browser.Tab;
|
| +import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchControl;
|
| +import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel.PanelState;
|
| +import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel.StateChangeReason;
|
| +import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanelDelegate;
|
| +import org.chromium.chrome.browser.contextualsearch.ContextualSearchSelectionController.SelectionType;
|
| +import org.chromium.chrome.browser.device.DeviceClassManager;
|
| +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.preferences.PrefServiceBridge;
|
| +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.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.InterceptNavigationDelegate;
|
| +import org.chromium.components.navigation_interception.NavigationParams;
|
| +import org.chromium.components.web_contents_delegate_android.WebContentsDelegateAndroid;
|
| +import org.chromium.content.browser.ContentView;
|
| +import org.chromium.content.browser.ContentViewCore;
|
| +import org.chromium.content.browser.ContextualSearchClient;
|
| +import org.chromium.content_public.browser.GestureStateListener;
|
| +import org.chromium.content_public.browser.JavaScriptCallback;
|
| +import org.chromium.content_public.browser.LoadUrlParams;
|
| +import org.chromium.content_public.browser.WebContents;
|
| +import org.chromium.content_public.browser.WebContentsObserver;
|
| +import org.chromium.content_public.common.TopControlsState;
|
| +import org.chromium.ui.base.WindowAndroid;
|
| +
|
| +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 extends ContextualSearchObservable
|
| + implements ContextualSearchManagementDelegate,
|
| + ContextualSearchNetworkCommunicator, ContextualSearchSelectionHandler,
|
| + ContextualSearchClient, ActivityStateListener {
|
| +
|
| + private static final String TAG = "ContextualSearch";
|
| +
|
| + private static final String CONTAINS_WORD_PATTERN = "(\\w|\\p{L}|\\p{N})+";
|
| +
|
| + // Constants related to the first-run flow.
|
| + private static final String FIRST_RUN_URL_PREFIX = "chrome://contextual-search-promo/";
|
| + private static final String FIRST_RUN_FLOW_URL = FIRST_RUN_URL_PREFIX + "promo.html";
|
| + private static final String FIRST_RUN_OPTIN_URL = FIRST_RUN_FLOW_URL + "#optin";
|
| + private static final String FIRST_RUN_OPTOUT_URL = FIRST_RUN_FLOW_URL + "#optout";
|
| + private static final String FIRST_RUN_LEARN_MORE_URL = FIRST_RUN_FLOW_URL + "#learn-more";
|
| +
|
| + // Max selection length must be limited or the entire request URL can go past the 2K limit.
|
| + private static final int MAX_SELECTION_LENGTH = 100;
|
| +
|
| + private static final boolean ALWAYS_USE_RESOLVED_SEARCH_TERM = true;
|
| + private static final boolean NEVER_USE_RESOLVED_SEARCH_TERM = false;
|
| +
|
| + 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 = "about:blank";
|
| +
|
| + private final ContextualSearchSelectionController mSelectionController;
|
| + private final Pattern mContainsWordPattern;
|
| + private final ChromeActivity mActivity;
|
| + private ViewGroup mParentView;
|
| + private final ViewTreeObserver.OnGlobalFocusChangeListener mOnFocusChangeListener;
|
| +
|
| + private final WindowAndroid mWindowAndroid;
|
| + private ContentViewCore mSearchContentViewCore;
|
| + private WebContentsObserver mSearchWebContentsObserver;
|
| + private final WebContentsDelegateAndroid mWebContentsDelegate;
|
| + private ContextualSearchContentViewDelegate mSearchContentViewDelegate;
|
| + private final ContextualSearchTabPromotionDelegate mTabPromotionDelegate;
|
| + private final FindToolbarObserver mFindToolbarObserver;
|
| + private FindToolbarManager mFindToolbarManager;
|
| + private TabModelSelectorTabObserver mTabModelSelectorTabObserver;
|
| + private TabModelObserver mTabModelObserver;
|
| + private boolean mIsSearchContentViewShowing;
|
| + private boolean mDidLoadResolvedSearchRequest;
|
| + 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 mDidLoadAnyUrl;
|
| + private boolean mDidPromoteSearchNavigation;
|
| + private boolean mDidBasePageLoadJustStart;
|
| + private boolean mWasActivatedByTap;
|
| + private boolean mIsInitialized;
|
| +
|
| + private boolean mIsShowingPromo;
|
| + private boolean mDidLogPromoOutcome;
|
| +
|
| + private ContextualSearchNetworkCommunicator mNetworkCommunicator;
|
| + private ContextualSearchPanelDelegate mSearchPanelDelegate;
|
| +
|
| + // 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;
|
| +
|
| + // The native manager associated with this object.
|
| + private long mNativeContextualSearchManagerPtr;
|
| +
|
| + private TabRedirectHandler mTabRedirectHandler;
|
| +
|
| + /**
|
| + * The delegate that is notified when the Search Panel ContentViewCore is ready to be rendered.
|
| + */
|
| + public interface ContextualSearchContentViewDelegate {
|
| + /**
|
| + * Sets the {@code ContentViewCore} associated to the Contextual Search Panel.
|
| + * @param contentViewCore Reference to the ContentViewCore.
|
| + */
|
| + void setContextualSearchContentViewCore(ContentViewCore contentViewCore);
|
| +
|
| + /**
|
| + * Releases the {@code ContentViewCore} associated to the Contextual Search Panel.
|
| + */
|
| + void releaseContextualSearchContentViewCore();
|
| + }
|
| +
|
| + /**
|
| + * 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 searchContentViewCore The {@link ContentViewCore} to promote.
|
| + * @return Whether or not {@code searchContentViewCore}'s ownership
|
| + * was transferred to the new {@link Tab} or not. If
|
| + * {@code false}, this {@link ContentViewCore} might be
|
| + * destroyed after this call.
|
| + */
|
| + boolean createContextualSearchTab(ContentViewCore searchContentViewCore);
|
| + }
|
| +
|
| + /**
|
| + * Constructs the manager for the given activity, and will attach views to the given parent.
|
| + * @param activity The {@code ChromeActivity} in use.
|
| + * @param windowAndroid The {@code WindowAndroid} associated with Chrome.
|
| + * @param tabPromotionDelegate The {@link ContextualSearchTabPromotionDelegate} that is
|
| + * responsible for building tabs from contextual search
|
| + * {@link ContentViewCore}s.
|
| + */
|
| + public ContextualSearchManager(ChromeActivity activity, WindowAndroid windowAndroid,
|
| + ContextualSearchTabPromotionDelegate tabPromotionDelegate) {
|
| + super(activity);
|
| + mActivity = activity;
|
| + mWindowAndroid = windowAndroid;
|
| + mTabPromotionDelegate = tabPromotionDelegate;
|
| + mContainsWordPattern = Pattern.compile(CONTAINS_WORD_PATTERN);
|
| +
|
| + mSelectionController = new ContextualSearchSelectionController(activity, this);
|
| +
|
| + mWebContentsDelegate = new WebContentsDelegateAndroid() {
|
| + @Override
|
| + public void onLoadStarted() {
|
| + super.onLoadStarted();
|
| + mSearchPanelDelegate.onLoadStarted();
|
| + }
|
| +
|
| + @Override
|
| + public void onLoadStopped() {
|
| + super.onLoadStopped();
|
| + mSearchPanelDelegate.onLoadStopped();
|
| + }
|
| +
|
| + @Override
|
| + public void onLoadProgressChanged(int progress) {
|
| + super.onLoadProgressChanged(progress);
|
| + mSearchPanelDelegate.onLoadProgressChanged(progress);
|
| + }
|
| + };
|
| +
|
| + mFindToolbarObserver = new FindToolbarObserver() {
|
| + @Override
|
| + public void onFindToolbarShown() {
|
| + hideContextualSearch(StateChangeReason.UNKNOWN);
|
| + }
|
| + };
|
| +
|
| + 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 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() == mSearchContentViewCore) return;
|
| + hideContextualSearch(StateChangeReason.UNKNOWN);
|
| + }
|
| + };
|
| + }
|
| +
|
| + /**
|
| + * Sets the manager in charge of find in page.
|
| + * @param manager A {@link FindToolbarManager} instance.
|
| + */
|
| + public void setFindToolbarManager(FindToolbarManager manager) {
|
| + assert manager != null;
|
| + mFindToolbarManager = manager;
|
| + mFindToolbarManager.addObserver(mFindToolbarObserver);
|
| + }
|
| +
|
| + /**
|
| + * Initializes this manager. Must be called before {@link #getContextualSearchControl()}.
|
| + * @param parentView The parent view to attach Contextual Search UX to.
|
| + */
|
| + public void initialize(ViewGroup parentView) {
|
| + mParentView = parentView;
|
| + mParentView.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnFocusChangeListener);
|
| + mNativeContextualSearchManagerPtr = nativeInit();
|
| + listenForHideNotifications();
|
| + mTabRedirectHandler = new TabRedirectHandler(mActivity);
|
| +
|
| + mIsShowingPromo = false;
|
| + mDidLogPromoOutcome = false;
|
| + mDidLoadResolvedSearchRequest = false;
|
| + mWereSearchResultsSeen = false;
|
| + mNetworkCommunicator = this;
|
| + ApplicationStatus.registerStateListenerForActivity(this, mActivity);
|
| + mIsInitialized = true;
|
| + }
|
| +
|
| + /**
|
| + * 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();
|
| + ApplicationStatus.unregisterActivityStateListener(this);
|
| + }
|
| +
|
| + @Override
|
| + public void setContextualSearchPanelDelegate(ContextualSearchPanelDelegate delegate) {
|
| + mSearchPanelDelegate = delegate;
|
| +
|
| + boolean shouldHide = nativeShouldHidePromoHeader(mNativeContextualSearchManagerPtr);
|
| + mSearchPanelDelegate.setShouldHidePromoHeader(shouldHide);
|
| + }
|
| +
|
| + /**
|
| + * @return The {@link ContextualSearchPanelDelegate}, for testing purposes only.
|
| + */
|
| + @VisibleForTesting
|
| + public ContextualSearchPanelDelegate getContextualSearchPanelDelegate() {
|
| + return mSearchPanelDelegate;
|
| + }
|
| +
|
| + /**
|
| + * Sets the selection controller for testing purposes.
|
| + */
|
| + @VisibleForTesting
|
| + ContextualSearchSelectionController getSelectionController() {
|
| + return mSelectionController;
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + boolean isSearchPanelShowing() {
|
| + return mSearchPanelDelegate.isShowing();
|
| + }
|
| +
|
| + /**
|
| + * @return Whether the Search Panel is opened. That is, whether it is EXPANDED or MAXIMIZED.
|
| + */
|
| + public boolean isSearchPanelOpened() {
|
| + PanelState state = mSearchPanelDelegate.getPanelState();
|
| + return state == PanelState.EXPANDED || state == PanelState.MAXIMIZED;
|
| + }
|
| +
|
| + /**
|
| + * @return The Base Page's {@link ContentViewCore}.
|
| + */
|
| + @Nullable private ContentViewCore getBaseContentView() {
|
| + return mSelectionController.getBaseContentView();
|
| + }
|
| +
|
| + @Override
|
| + public void setPreferenceState(boolean enabled) {
|
| + PrefServiceBridge.getInstance().setContextualSearchState(enabled);
|
| + }
|
| +
|
| + @Override
|
| + public boolean isOptOutPromoAvailable() {
|
| + return mPolicy.isOptOutPromoAvailable();
|
| + }
|
| +
|
| + /**
|
| + * Hides the Contextual Search UX.
|
| + * @param reason The {@link StateChangeReason} for hiding Contextual Search.
|
| + */
|
| + public void hideContextualSearch(StateChangeReason reason) {
|
| + if (mSearchPanelDelegate == null) return;
|
| +
|
| + if (mSearchPanelDelegate.isShowing()) {
|
| + mSearchPanelDelegate.closePanel(reason, false);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void onCloseContextualSearch() {
|
| + if (mSearchPanelDelegate == null) return;
|
| +
|
| + // NOTE(pedrosimonetti): hideContextualSearch() will also be called after swiping the
|
| + // Panel down in order to dismiss it. In this case, hideContextualSearch() will be called
|
| + // after completing the hide animation, and at that moment the Panel will not be showing
|
| + // anymore. Therefore, we need to always clear selection, regardless of when the Panel
|
| + // was still visible, in order to make sure the selection will be cleared appropriately.
|
| + if (mSelectionController.getSelectionType() == SelectionType.TAP) {
|
| + mSelectionController.clearSelection();
|
| + }
|
| +
|
| + // Show the infobar container if it was visible before Contextual Search was shown.
|
| + if (mWereInfoBarsHidden) {
|
| + mWereInfoBarsHidden = false;
|
| + InfoBarContainer container = getInfoBarContainer();
|
| + if (container != null) {
|
| + container.setVisibility(View.VISIBLE);
|
| + container.setDoStayInvisible(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;
|
| +
|
| + mNetworkCommunicator.destroySearchContentView();
|
| + mSearchRequest = null;
|
| +
|
| + if (mIsShowingPromo && !mDidLogPromoOutcome) {
|
| + logPromoOutcome();
|
| + }
|
| +
|
| + mIsShowingPromo = false;
|
| + mSearchPanelDelegate.setIsPromoActive(false);
|
| + }
|
| +
|
| + /**
|
| + * Called when the system back button is pressed. Will hide the layout.
|
| + */
|
| + public boolean onBackPressed() {
|
| + if (!mIsInitialized || !mSearchPanelDelegate.isShowing()) return false;
|
| + hideContextualSearch(StateChangeReason.BACK_PRESS);
|
| + return true;
|
| + }
|
| +
|
| + /**
|
| + * Called when the orientation of the device changes.
|
| + */
|
| + public void onOrientationChange() {
|
| + if (!mIsInitialized) return;
|
| +
|
| + // NOTE(pedrosimonetti): Invalidates the existing height. This is to prevent keeping
|
| + // the wrong height for a short period after rotating the device.
|
| + mSearchPanelDelegate.setPromoContentHeight(0.f);
|
| + hideContextualSearch(StateChangeReason.UNKNOWN);
|
| + }
|
| +
|
| + /**
|
| + * Sets the {@link ContextualSearchNetworkCommunicator} to use for server requests.
|
| + * @param networkCommunicator The communicator for all future requests.
|
| + */
|
| + @VisibleForTesting
|
| + public void setNetworkCommunicator(ContextualSearchNetworkCommunicator networkCommunicator) {
|
| + mNetworkCommunicator = networkCommunicator;
|
| + }
|
| +
|
| + /**
|
| + * Determines if the given selection is valid or not.
|
| + * @param selection The selection portion of the context.
|
| + * @return whether the given selection is considered a valid target for a search.
|
| + */
|
| + private boolean isValidSelection(String selection) {
|
| + return isValidSelection(selection, getBaseContentView());
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + boolean isValidSelection(String selection, ContentViewCore baseContentView) {
|
| + if (selection.length() > MAX_SELECTION_LENGTH || !doesContainAWord(selection)) {
|
| + return false;
|
| + }
|
| + return (baseContentView != null) && !baseContentView.isFocusedNodeEditable();
|
| + }
|
| +
|
| + /**
|
| + * Shows the Contextual Search UX.
|
| + * Calls back into onGetContextualSearchQueryResponse.
|
| + * @param stateChangeReason The reason explaining the change of state.
|
| + */
|
| + private void showContextualSearch(StateChangeReason stateChangeReason) {
|
| + if (!mSearchPanelDelegate.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.setVisibility(View.INVISIBLE);
|
| + container.setDoStayInvisible(true);
|
| + }
|
| + }
|
| +
|
| + // If the user is jumping from one unseen search to another search, remove the last search
|
| + // from history.
|
| + PanelState state = mSearchPanelDelegate.getPanelState();
|
| + if (!mWereSearchResultsSeen && mLoadedSearchUrlTimeMs != 0L
|
| + && state != PanelState.UNDEFINED && state != PanelState.CLOSED) {
|
| + removeLastSearchVisit();
|
| + }
|
| +
|
| + // Make sure we'll create a new Content View when needed.
|
| + mNetworkCommunicator.destroySearchContentView();
|
| +
|
| + boolean isTap = mSelectionController.getSelectionType() == SelectionType.TAP;
|
| + boolean didRequestSurroundings = false;
|
| + if (mPolicy.isOptInPromoAvailable()) {
|
| + showFirstRunFlow();
|
| + } else if (isTap && mPolicy.shouldPreviousTapResolve(
|
| + mNetworkCommunicator.getBasePageUrl())) {
|
| + mNetworkCommunicator.startSearchTermResolutionRequest(
|
| + mSelectionController.getSelectedText());
|
| + didRequestSurroundings = true;
|
| + } else {
|
| + boolean shouldPrefetch = mPolicy.shouldPrefetchSearchResult(isTap);
|
| + mSearchRequest = new ContextualSearchRequest(mSelectionController.getSelectedText(),
|
| + null, shouldPrefetch);
|
| + mDidLoadResolvedSearchRequest = false;
|
| + getContextualSearchControl().setCentralText(mSelectionController.getSelectedText());
|
| + if (shouldPrefetch) loadSearchUrl();
|
| + }
|
| + if (!didRequestSurroundings) {
|
| + // Gather surrounding text for Icing integration, which will make the selection and
|
| + // a shorter version of the surroundings available for Conversational Search.
|
| + // Although the surroundings are extracted, they will not be sent to the server as
|
| + // part of search term resolution, just sent to Icing which keeps them local until
|
| + // the user activates a Voice Search.
|
| + nativeGatherSurroundingText(mNativeContextualSearchManagerPtr,
|
| + mSelectionController.getSelectedText(), NEVER_USE_RESOLVED_SEARCH_TERM,
|
| + getBaseContentView());
|
| + }
|
| +
|
| + mWereSearchResultsSeen = false;
|
| +
|
| + // TODO(donnd): although we are showing the bar here, we have not yet set the text!
|
| + // Refactor to show the bar and set the text at the same time!
|
| + // TODO(donnd): If there was a previously ongoing contextual search, we should ensure
|
| + // it's registered as closed.
|
| + mSearchPanelDelegate.peekPanel(stateChangeReason);
|
| +
|
| + // Note: now that the contextual search has properly started, set the first run involvement.
|
| + if (mPolicy.isOptOutPromoAvailable()) {
|
| + mIsShowingPromo = true;
|
| + mDidLogPromoOutcome = false;
|
| + mSearchPanelDelegate.setDidSearchInvolvePromo();
|
| + }
|
| +
|
| + assert mSelectionController.getSelectionType() != SelectionType.UNDETERMINED;
|
| + mWasActivatedByTap = mSelectionController.getSelectionType() == SelectionType.TAP;
|
| + }
|
| +
|
| + @Override
|
| + public void startSearchTermResolutionRequest(String selection) {
|
| + ContentViewCore baseContentView = getBaseContentView();
|
| + if (baseContentView != null) {
|
| + nativeStartSearchTermResolutionRequest(mNativeContextualSearchManagerPtr, selection,
|
| + true, getBaseContentView());
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void continueSearchTermResolutionRequest() {
|
| + nativeContinueSearchTermResolutionRequest(mNativeContextualSearchManagerPtr);
|
| + }
|
| +
|
| + @Override
|
| + @Nullable public URL getBasePageUrl() {
|
| + ContentViewCore baseContentViewCore = getBaseContentView();
|
| + if (baseContentViewCore == null) return null;
|
| +
|
| + try {
|
| + return new URL(baseContentViewCore.getWebContents().getUrl());
|
| + } catch (MalformedURLException e) {
|
| + return null;
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Loads the first-run flow in the search content view and sets the text on the contextual
|
| + * search bar.
|
| + */
|
| + private void showFirstRunFlow() {
|
| + mIsShowingPromo = true;
|
| + mDidLogPromoOutcome = false;
|
| + getContextualSearchControl().setFirstRunText(mSelectionController.getSelectedText());
|
| + loadUrl(FIRST_RUN_FLOW_URL);
|
| + mSearchPanelDelegate.setIsPromoActive(true);
|
| + // Saved in the case a search needs to be made after first run.
|
| + if (mSelectionController.getSelectionType() == SelectionType.TAP) {
|
| + nativeGatherSurroundingText(mNativeContextualSearchManagerPtr,
|
| + mSelectionController.getSelectedText(),
|
| + ALWAYS_USE_RESOLVED_SEARCH_TERM, getBaseContentView());
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void updateTopControlsState(int current, boolean animate) {
|
| + Tab currentTab = mActivity.getActivityTab();
|
| + if (currentTab != null) {
|
| + currentTab.updateTopControlsState(current, animate);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Determines if the given selection contains a word or not.
|
| + * @param selection The the selection to check for a word.
|
| + * @return Whether the selection contains a word anywhere within it or not.
|
| + */
|
| + @VisibleForTesting
|
| + public boolean doesContainAWord(String selection) {
|
| + return mContainsWordPattern.matcher(selection).find();
|
| + }
|
| +
|
| + /**
|
| + * Accessor for the {@code InfoBarContainer} currently attached to the {@code Tab}.
|
| + */
|
| + private InfoBarContainer getInfoBarContainer() {
|
| + Tab tab = mActivity.getActivityTab();
|
| + return tab == null ? null : tab.getInfoBarContainer();
|
| + }
|
| +
|
| + /**
|
| + * Inflates the Contextual Search control, if needed.
|
| + */
|
| + private ContextualSearchControl getContextualSearchControl() {
|
| + return mSearchPanelDelegate.getContextualSearchControl();
|
| + }
|
| +
|
| + /**
|
| + * Listens for notifications that should hide the Contextual Search bar.
|
| + */
|
| + private void listenForHideNotifications() {
|
| + TabModelSelector selector = mActivity.getTabModelSelector();
|
| +
|
| + mTabModelSelectorTabObserver = new TabModelSelectorTabObserver(selector) {
|
| + @Override
|
| + public void onPageLoadStarted(Tab tab) {
|
| + hideContextualSearch(StateChangeReason.UNKNOWN);
|
| + mDidBasePageLoadJustStart = true;
|
| + }
|
| +
|
| + @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();
|
| + if (mFindToolbarManager != null) mFindToolbarManager.removeObserver(mFindToolbarObserver);
|
| +
|
| + TabModelSelector selector = mActivity.getTabModelSelector();
|
| + if (selector != null) {
|
| + for (TabModel tabModel : selector.getModels()) {
|
| + tabModel.removeObserver(mTabModelObserver);
|
| + }
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void onActivityStateChange(Activity activity, int newState) {
|
| + if (newState == ActivityState.RESUMED || newState == ActivityState.STOPPED
|
| + || newState == ActivityState.DESTROYED) {
|
| + hideContextualSearch(StateChangeReason.UNKNOWN);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * 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 when surrounding text is available.
|
| + * @param beforeText to be shown before the selected word.
|
| + * @param afterText to be shown after the selected word.
|
| + */
|
| + @CalledByNative
|
| + private void onSurroundingTextAvailable(final String beforeText, final String afterText) {
|
| + if (mSearchPanelDelegate.isShowing()) {
|
| + getContextualSearchControl().setSearchContext(
|
| + mSelectionController.getSelectedText(), beforeText, afterText);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Called by native code when a selection is available to share with Icing (for Conversational
|
| + * Search).
|
| + */
|
| + @CalledByNative
|
| + private void onIcingSelectionAvailable(
|
| + final String encoding, final String surroundingText, int startOffset, int endOffset) {
|
| + GSAContextDisplaySelection selection =
|
| + new GSAContextDisplaySelection(encoding, surroundingText, startOffset, endOffset);
|
| + notifyShowContextualSearch(selection, mNetworkCommunicator.getBasePageUrl());
|
| + }
|
| +
|
| + /**
|
| + * Called in response to the
|
| + * {@link ContextualSearchManager#nativeStartSearchTermResolutionRequest} method.
|
| + * @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.
|
| + */
|
| + @CalledByNative
|
| + public void onSearchTermResolutionResponse(boolean isNetworkUnavailable, int responseCode,
|
| + final String searchTerm, final String displayText, final String alternateTerm,
|
| + boolean doPreventPreload) {
|
| + mNetworkCommunicator.handleSearchTermResolutionResponse(isNetworkUnavailable, responseCode,
|
| + searchTerm, displayText, alternateTerm, doPreventPreload);
|
| + }
|
| +
|
| + @Override
|
| + public void handleSearchTermResolutionResponse(boolean isNetworkUnavailable, int responseCode,
|
| + String searchTerm, String displayText, String alternateTerm, boolean doPreventPreload) {
|
| + if (!mSearchPanelDelegate.isShowing()) return;
|
| +
|
| + // Show an appropriate message for what to search for.
|
| + String message;
|
| + boolean doLiteralSearch = false;
|
| + if (isNetworkUnavailable) {
|
| + message = mActivity.getResources().getString(
|
| + R.string.contextual_search_network_unavailable);
|
| + } else if (!isHttpFailureCode(responseCode)) {
|
| + message = displayText;
|
| + } else if (!mPolicy.shouldShowErrorCodeInBar()) {
|
| + message = mSelectionController.getSelectedText();
|
| + doLiteralSearch = true;
|
| + } else {
|
| + message = mActivity.getResources().getString(
|
| + R.string.contextual_search_error, responseCode);
|
| + doLiteralSearch = true;
|
| + }
|
| + getContextualSearchControl().setCentralText(message);
|
| +
|
| + // 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 (!searchTerm.isEmpty()) {
|
| + // 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(true);
|
| + mSearchRequest = new ContextualSearchRequest(searchTerm, alternateTerm, shouldPreload);
|
| + mDidLoadResolvedSearchRequest = false;
|
| + if (mIsSearchContentViewShowing) {
|
| + mSearchRequest.setNormalPriority();
|
| + }
|
| + if (mIsSearchContentViewShowing || shouldPreload) {
|
| + loadSearchUrl();
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Loads a Search Request in the Contextual Search's Content View.
|
| + */
|
| + private void loadSearchUrl() {
|
| + mLoadedSearchUrlTimeMs = System.currentTimeMillis();
|
| + mNetworkCommunicator.loadUrl(mSearchRequest.getSearchUrl());
|
| + mSearchPanelDelegate.setIsPromoActive(false);
|
| + mDidLoadResolvedSearchRequest = 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 (mIsSearchContentViewShowing && mSearchContentViewCore != null) {
|
| + mSearchContentViewCore.onShow();
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * @return Whether a Tap gesture is currently supported.
|
| + */
|
| + private boolean isTapSupported() {
|
| + // Base page just started navigating away, so taps should be ignored.
|
| + if (mDidBasePageLoadJustStart) return false;
|
| +
|
| + return mPolicy.isTapSupported();
|
| + }
|
| +
|
| + // --------------------------------------------------------------------------------------------
|
| + // Search Content View
|
| + // --------------------------------------------------------------------------------------------
|
| +
|
| + /**
|
| + * Gets the {@code ContentViewCore} associated with Contextual Search Panel.
|
| + * @return Contextual Search Panel's {@code ContentViewCore}.
|
| + */
|
| + @Override
|
| + public ContentViewCore getSearchContentViewCore() {
|
| + return mSearchContentViewCore;
|
| + }
|
| +
|
| + /**
|
| + * Sets the {@code ContextualSearchContentViewDelegate} associated with the Content View.
|
| + * @param delegate
|
| + */
|
| + public void setSearchContentViewDelegate(ContextualSearchContentViewDelegate delegate) {
|
| + mSearchContentViewDelegate = delegate;
|
| + }
|
| +
|
| + /**
|
| + * Removes the last resolved search URL from the Chrome history.
|
| + */
|
| + private void removeLastSearchVisit() {
|
| + if (mSearchRequest != null) {
|
| + nativeRemoveLastSearchVisit(mNativeContextualSearchManagerPtr,
|
| + mSearchRequest.getSearchUrl(), mLoadedSearchUrlTimeMs);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Called when the Search content view navigates to a specific URL related to the first run
|
| + * flow.
|
| + * @param url The new URL that is being navigated to.
|
| + */
|
| + private void onFirstRunNavigation(String url) {
|
| + if (FIRST_RUN_OPTIN_URL.equals(url)) {
|
| + mSearchPanelDelegate.setIsPromoActive(false);
|
| + mSearchPanelDelegate.animateAfterFirstRunSuccess();
|
| + PrefServiceBridge.getInstance().setContextualSearchState(true);
|
| + logPromoOutcome();
|
| + String selection = mSelectionController.getSelectedText();
|
| + if (mSelectionController.getSelectionType() == SelectionType.LONG_PRESS) {
|
| + mSearchRequest = new ContextualSearchRequest(selection);
|
| + loadSearchUrl();
|
| + getContextualSearchControl().setCentralText(selection);
|
| + } else {
|
| + mNetworkCommunicator.continueSearchTermResolutionRequest();
|
| + }
|
| + } else if (FIRST_RUN_OPTOUT_URL.equals(url)) {
|
| + PrefServiceBridge.getInstance().setContextualSearchState(false);
|
| + logPromoOutcome();
|
| + mSearchPanelDelegate.closePanel(StateChangeReason.OPTOUT, true);
|
| + mSearchPanelDelegate.setIsPromoActive(false);
|
| + } else if (FIRST_RUN_LEARN_MORE_URL.equals(url)) {
|
| + openContextualSearchLearnMore();
|
| + mSearchPanelDelegate.setIsPromoActive(false);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * 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 (mSearchContentViewCore != null) {
|
| + // When running tests the Content View might not exist.
|
| + mSearchContentViewCore.getWebContents().stop();
|
| + }
|
| + mSearchRequest.setHasFailed();
|
| + mSearchRequest.setNormalPriority();
|
| + // If the content view is showing, load at normal priority now.
|
| + if (mIsSearchContentViewShowing) {
|
| + loadSearchUrl();
|
| + } else {
|
| + mDidLoadResolvedSearchRequest = false;
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Opens the Contextual Search "Learn More" experience.
|
| + */
|
| + private void openContextualSearchLearnMore() {
|
| + openUrlInNewTab(mActivity.getString(R.string.contextual_search_learn_more_url));
|
| + }
|
| +
|
| + @Override
|
| + public void logPromoOutcome() {
|
| + ContextualSearchUma.logPromoOutcome(mWasActivatedByTap);
|
| + mDidLogPromoOutcome = true;
|
| + }
|
| +
|
| + /**
|
| + * Called when the Search Content view has finished loading to record how long it takes the SERP
|
| + * to load after opening the panel.
|
| + */
|
| + private void onSearchResultsLoaded() {
|
| + if (mSearchRequest == null) return;
|
| +
|
| + mSearchPanelDelegate.onSearchResultsLoaded(mSearchRequest.wasPrefetch());
|
| + }
|
| +
|
| + /**
|
| + * Creates a new Content View Core to display search results, if needed.
|
| + */
|
| + private void createNewSearchContentViewCoreIfNeeded() {
|
| + if (mSearchContentViewCore == null) {
|
| + mNetworkCommunicator.createNewSearchContentView();
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void loadUrl(String url) {
|
| + createNewSearchContentViewCoreIfNeeded();
|
| + if (mSearchContentViewCore != null && mSearchContentViewCore.getWebContents() != null) {
|
| + mDidLoadAnyUrl = true;
|
| + mSearchContentViewCore.getWebContents().getNavigationController().loadUrl(
|
| + new LoadUrlParams(url));
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void createNewSearchContentView() {
|
| + if (mSearchContentViewCore != null) {
|
| + mNetworkCommunicator.destroySearchContentView();
|
| + }
|
| +
|
| + mSearchContentViewCore = new ContentViewCore(mActivity);
|
| + ContentView cv = new ContentView(mActivity, mSearchContentViewCore);
|
| + // Creates an initially hidden WebContents which gets shown when the panel is opened.
|
| + mSearchContentViewCore.initialize(cv, cv,
|
| + ContentViewUtil.createWebContents(false, true), mWindowAndroid);
|
| +
|
| + // Transfers the ownership of the WebContents to the native ContextualSearchManager.
|
| + nativeSetWebContents(mNativeContextualSearchManagerPtr, mSearchContentViewCore,
|
| + mWebContentsDelegate);
|
| +
|
| + mSearchWebContentsObserver =
|
| + new WebContentsObserver(mSearchContentViewCore.getWebContents()) {
|
| + @Override
|
| + public void didStartLoading(String url) {
|
| + mDidPromoteSearchNavigation = false;
|
| + }
|
| +
|
| + @Override
|
| + public void didStartProvisionalLoadForFrame(long frameId, long parentFrameId,
|
| + boolean isMainFrame, String validatedUrl, boolean isErrorPage,
|
| + boolean isIframeSrcdoc) {
|
| + if (isMainFrame) onExternalNavigation(validatedUrl);
|
| + }
|
| +
|
| + @Override
|
| + public void didNavigateMainFrame(String url, String baseUrl,
|
| + boolean isNavigationToDifferentPage, boolean isNavigationInPage,
|
| + int httpResultCode) {
|
| + mNetworkCommunicator.handleDidNavigateMainFrame(url, httpResultCode);
|
| + }
|
| +
|
| + @Override
|
| + public void didFinishLoad(long frameId, String validatedUrl,
|
| + boolean isMainFrame) {
|
| + onSearchResultsLoaded();
|
| + boolean shouldClearHistory = mSearchRequest != null
|
| + && mSearchRequest.getHasFailed();
|
| + if (validatedUrl.equals(FIRST_RUN_FLOW_URL)) {
|
| + extractFirstRunFlowContentHeight();
|
| + } else if (mIsShowingPromo
|
| + && !validatedUrl.startsWith(FIRST_RUN_URL_PREFIX)) {
|
| + shouldClearHistory = true;
|
| + }
|
| + // Any time we place a page in a ContentViewCore, clear history if needed.
|
| + // This prevents the First Run Flow URL or error URLs from
|
| + // appearing in the Tab's history stack.
|
| + // Also please note that clearHistory() will not
|
| + // clear the current entry (search results page in this case),
|
| + // and it will not work properly if there are pending navigations.
|
| + // That's why we need to clear the history here, after the navigation
|
| + // is completed.
|
| + if (shouldClearHistory && mSearchContentViewCore != null) {
|
| + mSearchContentViewCore.getWebContents().getNavigationController()
|
| + .clearHistory();
|
| + }
|
| + }
|
| + };
|
| +
|
| + mSearchContentViewDelegate.setContextualSearchContentViewCore(mSearchContentViewCore);
|
| + nativeSetInterceptNavigationDelegate(mNativeContextualSearchManagerPtr,
|
| + new InterceptNavigationDelegateImpl(), mSearchContentViewCore.getWebContents());
|
| + }
|
| +
|
| + @Override
|
| + public void handleDidNavigateMainFrame(String url, int httpResultCode) {
|
| + if (url.startsWith(FIRST_RUN_URL_PREFIX)) {
|
| + // Navigation should not be done inside this WebContentsObserver,
|
| + // and since FirstRun may navigate, we need to delay that navigation.
|
| + final String firstRunUrl = url;
|
| + new Handler().post(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + onFirstRunNavigation(firstRunUrl);
|
| + }
|
| + });
|
| + } else if (shouldPromoteSearchNavigation()) {
|
| + onExternalNavigation(url);
|
| + } else {
|
| + // Could be just prefetching, check if that failed.
|
| + boolean isFailure = isHttpFailureCode(httpResultCode);
|
| + onContextualSearchRequestNavigation(isFailure);
|
| + }
|
| + mDidLoadAnyUrl = false;
|
| + }
|
| +
|
| + /**
|
| + * @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 mSearchPanelDelegate.didTouchSearchContentView() && !mDidLoadAnyUrl;
|
| + }
|
| +
|
| + /**
|
| + * 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.
|
| + */
|
| + private 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;
|
| + mSearchPanelDelegate.maximizePanelThenPromoteToTab(StateChangeReason.SERP_NAVIGATION);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Grab the content height from the first run flow and store it.
|
| + */
|
| + private void extractFirstRunFlowContentHeight() {
|
| + mSearchContentViewCore.getWebContents().evaluateJavaScript("getContentHeight()",
|
| + new JavaScriptCallback() {
|
| + @Override
|
| + public void handleJavaScriptResult(String result) {
|
| + try {
|
| + mSearchPanelDelegate.setPromoContentHeight(
|
| + Float.parseFloat(result));
|
| + } catch (NumberFormatException e) {
|
| + Log.w(TAG, "Could not extract first-run content height, using "
|
| + + "default value.");
|
| + }
|
| + }
|
| + });
|
| + }
|
| +
|
| + @Override
|
| + public void destroySearchContentView() {
|
| + if (mSearchContentViewCore != null && mSearchContentViewDelegate != null) {
|
| + nativeDestroyWebContents(mNativeContextualSearchManagerPtr);
|
| + mSearchContentViewDelegate.releaseContextualSearchContentViewCore();
|
| + mSearchContentViewCore.destroy();
|
| + mSearchContentViewCore = null;
|
| + if (mSearchWebContentsObserver != null) {
|
| + mSearchWebContentsObserver.destroy();
|
| + mSearchWebContentsObserver = null;
|
| + }
|
| + }
|
| +
|
| + // This should be called last here. The setSearchContentViewVisibility method
|
| + // will change the visibility the SearchContentView but also set the value of the
|
| + // internal property mIsSearchContentViewShowing. If we call this after deleting
|
| + // the SearchContentView, it will be faster, because only the internal property
|
| + // will be changed, since there will be no need to change the visibility of the
|
| + // SearchContentView.
|
| + setSearchContentViewVisibility(false);
|
| + }
|
| +
|
| + @Override
|
| + public void openResolvedSearchUrlInNewTab() {
|
| + if (mSearchRequest != null && mSearchRequest.getSearchUrl() != null) {
|
| + openUrlInNewTab(mSearchRequest.getSearchUrl());
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Convenience method for opening a specific |url| in a new Tab.
|
| + */
|
| + private void openUrlInNewTab(String url) {
|
| + TabModelSelector tabModelSelector = mActivity.getTabModelSelector();
|
| + tabModelSelector.openNewTab(
|
| + new LoadUrlParams(url),
|
| + TabLaunchType.FROM_MENU_OR_OVERVIEW,
|
| + tabModelSelector.getCurrentTab(),
|
| + tabModelSelector.isIncognitoSelected());
|
| + }
|
| +
|
| + @Override
|
| + public boolean isRunningInCompatibilityMode() {
|
| + return DeviceClassManager.isAccessibilityModeEnabled(mActivity)
|
| + || SysUtils.isLowEndDevice();
|
| + }
|
| +
|
| + @Override
|
| + public void promoteToTab(boolean shouldFocusOmnibox) {
|
| + // 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
|
| + && mSearchContentViewCore != null
|
| + && mSearchContentViewCore.getWebContents() != null
|
| + && !mSearchContentViewCore.getWebContents().getUrl().equals(FIRST_RUN_FLOW_URL)) {
|
| + mSelectionController.clearSelection();
|
| + nativeReleaseWebContents(mNativeContextualSearchManagerPtr);
|
| + mSearchContentViewDelegate.releaseContextualSearchContentViewCore();
|
| + if (!mTabPromotionDelegate.createContextualSearchTab(mSearchContentViewCore)) {
|
| + nativeDestroyWebContentsFromContentViewCore(mNativeContextualSearchManagerPtr,
|
| + mSearchContentViewCore);
|
| + mSearchContentViewCore.destroy();
|
| + }
|
| + if (mSearchWebContentsObserver != null) {
|
| + mSearchWebContentsObserver.destroy();
|
| + mSearchWebContentsObserver = null;
|
| + }
|
| + mSearchContentViewCore = null;
|
| + mIsSearchContentViewShowing = false;
|
| + mSearchRequest = null;
|
| +
|
| + // NOTE(pedrosimonetti): The Panel should be closed after being promoted to a Tab
|
| + // to prevent Chrome-Android from animating the creation of the new Tab.
|
| + mSearchPanelDelegate.closePanel(StateChangeReason.TAB_PROMOTION, false);
|
| +
|
| + // Focus the Omnibox.
|
| + if (shouldFocusOmnibox) {
|
| + new Handler().post(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + View urlBarView = mActivity.findViewById(R.id.url_bar);
|
| + urlBarView.requestFocus();
|
| + }
|
| + });
|
| + }
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void resetSearchContentViewScroll() {
|
| + if (mSearchContentViewCore != null) {
|
| + mSearchContentViewCore.scrollTo(0, 0);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public float getSearchContentViewVerticalScroll() {
|
| + return mSearchContentViewCore != null
|
| + ? mSearchContentViewCore.computeVerticalScrollOffset() : -1.f;
|
| + }
|
| +
|
| + @Override
|
| + public void setSearchContentViewVisibility(boolean isVisible) {
|
| + if (mIsSearchContentViewShowing == isVisible) return;
|
| +
|
| + mIsSearchContentViewShowing = 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(mSelectionController,
|
| + mNetworkCommunicator.getBasePageUrl())) {
|
| + mSearchRequest = new ContextualSearchRequest(
|
| + mSelectionController.getSelectedText());
|
| + mDidLoadResolvedSearchRequest = false;
|
| + }
|
| + if (mSearchRequest != null && !mDidLoadResolvedSearchRequest) {
|
| + mSearchRequest.setNormalPriority();
|
| + loadSearchUrl();
|
| + }
|
| + // The CVC is created with the search request, but if none was made we'll need
|
| + // one in order to display an empty panel.
|
| + createNewSearchContentViewCoreIfNeeded();
|
| + if (mSearchContentViewCore != null) mSearchContentViewCore.onShow();
|
| + mSearchPanelDelegate.setWasSearchContentViewSeen();
|
| + mPolicy.resetTapCounters();
|
| + } else {
|
| + if (mSearchContentViewCore != null) mSearchContentViewCore.onHide();
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void preserveBasePageSelectionOnNextLossOfFocus() {
|
| + ContentViewCore basePageContentView = getBaseContentView();
|
| + if (basePageContentView != null) {
|
| + basePageContentView.preserveSelectionOnNextLossOfFocus();
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void dismissContextualSearchBar() {
|
| + hideContextualSearch(StateChangeReason.UNKNOWN);
|
| + }
|
| +
|
| + // Used to intercept intent navigations.
|
| + // TODO(jeremycho): Consider creating a Tab with the Panel's ContentViewCore,
|
| + // which would also handle functionality like long-press-to-paste.
|
| + private class InterceptNavigationDelegateImpl implements InterceptNavigationDelegate {
|
| + final ExternalNavigationHandler mExternalNavHandler = new ExternalNavigationHandler(
|
| + mActivity);
|
| + @Override
|
| + public boolean shouldIgnoreNavigation(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, getReferrerUrl(),
|
| + navigationParams.pageTransitionType, navigationParams.isRedirect)
|
| + .setApplicationMustBeInForeground(true)
|
| + .setRedirectHandler(mTabRedirectHandler)
|
| + .setIsMainFrame(navigationParams.isMainFrame)
|
| + .build();
|
| + if (mExternalNavHandler.shouldOverrideUrlLoading(params)
|
| + != OverrideUrlLoadingResult.NO_OVERRIDE) {
|
| + mSearchPanelDelegate.maximizePanelThenPromoteToTab(
|
| + StateChangeReason.TAB_PROMOTION,
|
| + INTERCEPT_NAVIGATION_PROMOTION_ANIMATION_DURATION_MS);
|
| + return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + private String getReferrerUrl() {
|
| + if (mSearchContentViewCore != null && mSearchContentViewCore.getWebContents() != null) {
|
| + return mSearchContentViewCore.getWebContents().getNavigationController()
|
| + .getOriginalUrlForVisibleNavigationEntry();
|
| + } else {
|
| + return null;
|
| + }
|
| + }
|
| + }
|
| +
|
| + // --------------------------------------------------------------------------------------------
|
| + // ContextualSearchClient -- interface used by ContentViewCore.
|
| + // --------------------------------------------------------------------------------------------
|
| +
|
| + @Override
|
| + public void onSelectionChanged(String selection) {
|
| + mSelectionController.handleSelectionChanged(selection);
|
| + updateTopControlsState(TopControlsState.BOTH, true);
|
| + }
|
| +
|
| + @Override
|
| + public void onSelectionEvent(int eventType, float posXPix, float posYPix) {
|
| + mSelectionController.handleSelectionEvent(eventType, posXPix, posYPix);
|
| + }
|
| +
|
| + @Override
|
| + public void showUnhandledTapUIIfNeeded(final int x, final int y) {
|
| + mDidBasePageLoadJustStart = false;
|
| + mSelectionController.handleShowUnhandledTapUIIfNeeded(x, y);
|
| + }
|
| +
|
| + // --------------------------------------------------------------------------------------------
|
| + // 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() {
|
| + hideContextualSearch(StateChangeReason.BASE_PAGE_SCROLL);
|
| + }
|
| +
|
| + @Override
|
| + public void handleInvalidTap() {
|
| + hideContextualSearch(StateChangeReason.BASE_PAGE_TAP);
|
| + }
|
| +
|
| + @Override
|
| + public void handleValidTap() {
|
| + if (isTapSupported()) {
|
| + // Here we are starting a new Contextual Search with a Tap gesture, therefore
|
| + // we need to clear to properly reflect that a search just started and we don't
|
| + // have the resolved search term yet.
|
| + mSearchRequest = null;
|
| +
|
| + // Let the policy know that a tap gesture has been received.
|
| + mPolicy.registerTap();
|
| +
|
| + ContentViewCore baseContentView = getBaseContentView();
|
| + if (baseContentView != null) baseContentView.getWebContents().selectWordAroundCaret();
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void handleSelection(String selection, SelectionType type, float x, float y) {
|
| + if (!selection.isEmpty()) {
|
| + boolean isSelectionValid = isValidSelection(selection);
|
| +
|
| + StateChangeReason stateChangeReason = type == SelectionType.TAP
|
| + ? StateChangeReason.TEXT_SELECT_TAP : StateChangeReason.TEXT_SELECT_LONG_PRESS;
|
| + ContextualSearchUma.logSelectionIsValid(isSelectionValid);
|
| +
|
| + if (isSelectionValid) {
|
| + mSearchPanelDelegate.updateBasePageSelectionYPx(y);
|
| + showContextualSearch(stateChangeReason);
|
| + } else {
|
| + hideContextualSearch(stateChangeReason);
|
| + }
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void handleSelectionModification(String selection, float x, float y) {
|
| + if (mSearchPanelDelegate.isShowing()) {
|
| + getContextualSearchControl().setCentralText(selection);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void onClearSelection() {
|
| + notifyHideContextualSearch();
|
| + }
|
| +
|
| + // --------------------------------------------------------------------------------------------
|
| + // Native calls
|
| + // --------------------------------------------------------------------------------------------
|
| +
|
| + private native long nativeInit();
|
| + private native void nativeDestroy(long nativeContextualSearchManager);
|
| + private native void nativeStartSearchTermResolutionRequest(
|
| + long nativeContextualSearchManager, String selection, boolean useResolvedSearchTerm,
|
| + ContentViewCore baseContentViewCore);
|
| + private native void nativeGatherSurroundingText(
|
| + long nativeContextualSearchManager, String selection, boolean useResolvedSearchTerm,
|
| + ContentViewCore baseContentViewCore);
|
| + private native void nativeContinueSearchTermResolutionRequest(
|
| + long nativeContextualSearchManager);
|
| + private native void nativeRemoveLastSearchVisit(
|
| + long nativeContextualSearchManager, String searchUrl, long searchUrlTimeMs);
|
| + private native void nativeSetWebContents(long nativeContextualSearchManager,
|
| + ContentViewCore searchContentViewCore, WebContentsDelegateAndroid delegate);
|
| + private native void nativeDestroyWebContents(long nativeContextualSearchManager);
|
| + private native void nativeReleaseWebContents(long nativeContextualSearchManager);
|
| + private native void nativeDestroyWebContentsFromContentViewCore(
|
| + long nativeContextualSearchManager, ContentViewCore contentViewCore);
|
| + private native boolean nativeShouldHidePromoHeader(long nativeContextualSearchManager);
|
| + private native void nativeSetInterceptNavigationDelegate(long nativeContextualSearchManager,
|
| + InterceptNavigationDelegate delegate, WebContents webContents);
|
| +}
|
|
|