Chromium Code Reviews| Index: chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java |
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java b/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java |
| index 20f75a0474b4999314d4d57019c5aa7f086d0132..3ea63c36fc1a7998f012776f3bf6a3b8e56e9465 100644 |
| --- a/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java |
| @@ -4,6 +4,7 @@ |
| package org.chromium.chrome.browser.contextualsearch; |
| +import android.os.Handler; |
| import android.text.TextUtils; |
| import android.view.View; |
| import android.view.ViewGroup; |
| @@ -24,6 +25,7 @@ import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelContentViewD |
| import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel; |
| import org.chromium.chrome.browser.contextualsearch.ContextualSearchBlacklist.BlacklistReason; |
| import org.chromium.chrome.browser.contextualsearch.ContextualSearchSelectionController.SelectionType; |
| +import org.chromium.chrome.browser.contextualsearch.ContextualSearchStateController.State; |
| import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler; |
| import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler.OverrideUrlLoadingResult; |
| import org.chromium.chrome.browser.externalnav.ExternalNavigationParams; |
| @@ -53,6 +55,7 @@ import org.chromium.ui.base.WindowAndroid; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| +import java.util.concurrent.TimeUnit; |
| import java.util.regex.Pattern; |
| import javax.annotation.Nullable; |
| @@ -63,10 +66,10 @@ import javax.annotation.Nullable; |
| * 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 { |
| +public class ContextualSearchManager |
| + implements ContextualSearchManagementDelegate, ContextualSearchTranslateInterface, |
| + ContextualSearchNetworkCommunicator, ContextualSearchSelectionHandler, |
| + ContextualSearchStateControlled, SelectionClient { |
|
Theresa
2017/03/28 15:52:55
There are a lot of interfaces listed here. I think
Donn Denman
2017/03/29 18:56:51
Hopefully this is clear now that the State is an I
|
| private static final String INTENT_URL_PREFIX = "intent:"; |
| // The animation duration of a URL being promoted to a tab when triggered by an |
| @@ -82,6 +85,9 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| // 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 = ""; |
| + // The default navigation-detection-delay in milliseconds. |
| + private static final int TAP_NAVIGATION_DETECTION_DELAY = 20; |
| + |
| private final ObserverList<ContextualSearchObserver> mObservers = |
| new ObserverList<ContextualSearchObserver>(); |
| @@ -89,6 +95,7 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| private final ContextualSearchTabPromotionDelegate mTabPromotionDelegate; |
| private final ViewTreeObserver.OnGlobalFocusChangeListener mOnFocusChangeListener; |
| private final TabModelObserver mTabModelObserver; |
| + private final ContextualSearchStateController mController; |
| private ContextualSearchSelectionController mSelectionController; |
| private ContextualSearchNetworkCommunicator mNetworkCommunicator; |
| @@ -116,6 +123,8 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| private boolean mWereSearchResultsSeen; |
| private boolean mWereInfoBarsHidden; |
| private boolean mDidPromoteSearchNavigation; |
| + |
| + // Detects navigation of the base page for crbug.com/428368. |
| private boolean mDidBasePageLoadJustStart; |
| private boolean mWasActivatedByTap; |
| private boolean mIsInitialized; |
| @@ -219,6 +228,8 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| mPolicy = new ContextualSearchPolicy(mSelectionController, mNetworkCommunicator); |
| mTranslateController = new ContextualSearchTranslateController(activity, mPolicy, this); |
| + |
| + mController = new ContextualSearchStateController(mSelectionController, mPolicy, this); |
| } |
| /** |
| @@ -239,6 +250,8 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| mWereSearchResultsSeen = false; |
| mIsInitialized = true; |
| + mController.reset(StateChangeReason.UNKNOWN); |
| + |
| listenForTabModelSelectorNotifications(); |
| } |
| @@ -283,6 +296,7 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| mFindToolbarManager = null; |
| mFindToolbarObserver = null; |
| } |
| + mController.enter(State.UNDEFINED); |
| } |
| @Override |
| @@ -326,10 +340,18 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| } |
| /** |
| - * Hides the Contextual Search UX. |
| + * 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) { |
| + mController.reset(reason); |
| + } |
| + |
| + /** |
| + * Does the work of hiding the Contextual Search UX. |
| + * @param reason The {@link StateChangeReason} for hiding Contextual Search. |
| + */ |
| + private void handleHideContextualSearch(StateChangeReason reason) { |
| if (mContext != null) mContext.destroy(); |
| mContext = null; |
| if (mSearchPanel == null) return; |
| @@ -422,23 +444,18 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| removeLastSearchVisit(); |
| } |
| - // TODO(pedrosimonetti): Fix for M47. Replace this with a better delayed load approach. |
| mSearchPanel.destroyContent(); |
| String selection = mSelectionController.getSelectedText(); |
| + boolean didResolve = false; |
| boolean isTap = mSelectionController.getSelectionType() == SelectionType.TAP; |
| - boolean didRequestSurroundings = false; |
| if (isTap) { |
| - // If the user action was not a long-press, immediately start loading content. |
| + // If the user action was not a long-press, we'll immediately start loading content. |
| mShouldLoadDelayedSearch = false; |
| } |
| if (isTap && mPolicy.shouldPreviousTapResolve()) { |
| - if (mContext != null) mContext.destroy(); |
| - mContext = new ContextualSearchContext( |
| - selection, mPolicy.getHomeCountry(mActivity), mPolicy.maySendBasePageUrl()); |
| - nativeGatherSurroundingText( |
| - mNativeContextualSearchManagerPtr, mContext, getBaseWebContents()); |
| - didRequestSurroundings = true; |
| + // establish(State.RESOLVING); |
| + didResolve = true; |
| // Cache the native translate data, so JNI calls won't be made when time-critical. |
| mTranslateController.cacheNativeTranslateData(); |
| } else { |
| @@ -457,20 +474,12 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| RecordUserAction.record(isSingleWord ? "ContextualSearch.ManualRefineSingleWord" |
| : "ContextualSearch.ManualRefineMultiWord"); |
| } |
| + /* |
| + if (isTap) { |
| + establish(State.SHOWING_TAP); |
| + } |
| + */ |
| } |
| - |
| - if (!didRequestSurroundings && getBaseWebContents() != null) { |
| - // 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. |
| - if (mContext != null) mContext.destroy(); |
| - mContext = new ContextualSearchContext(); |
| - nativeGatherSurroundingText( |
| - mNativeContextualSearchManagerPtr, mContext, getBaseWebContents()); |
| - } |
| - |
| mWereSearchResultsSeen = false; |
| // Show the Peek Promo only when the Panel wasn't previously visible, provided |
| @@ -505,6 +514,16 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| assert mSelectionController.getSelectionType() != SelectionType.UNDETERMINED; |
| mWasActivatedByTap = mSelectionController.getSelectionType() == SelectionType.TAP; |
| + |
| + // Update the Bar Context to show the text and after-text. |
| + if (mSearchRequest == null && mContext != null) { |
| + assert mContext.getTextContentFollowingSelection() != null; |
| + assert !TextUtils.isEmpty(selection); |
| + String afterText = mContext.getTextContentFollowingSelection(); |
| + mSearchPanel.setContextDetails(selection, afterText); |
| + } |
| + |
| + notifyContextualSearchSelection(); |
| } |
| @Override |
| @@ -619,11 +638,10 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| */ |
| @CalledByNative |
| private void onSurroundingTextAvailable(final String afterText) { |
| - String selection = mSelectionController.getSelectedText(); |
| - // TODO(donnd): check if panel has been requested to show. |
| - // We used to call mSearchPanel.isShowing() here, but that's unreliable (crbug.com/669600). |
| - mSearchPanel.setContextDetails(selection, afterText); |
| - mNetworkCommunicator.startSearchTermResolutionRequest(selection); |
| + // The panel is not yet showing, so save the afterText so we can show it when we show the |
| + // panel. |
| + assert mContext != null; |
| + mContext.setTextContentFollowingSelection(afterText); |
| } |
| /** |
| @@ -633,12 +651,13 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| @CalledByNative |
| private void onIcingSelectionAvailable( |
| final String encoding, final String surroundingText, int startOffset, int endOffset) { |
| - GSAContextDisplaySelection selection = |
| - new GSAContextDisplaySelection(encoding, surroundingText, startOffset, endOffset); |
| - mSearchPanel.setWasSelectionPartOfUrl( |
| - ContextualSearchSelectionController.isSelectionPartOfUrl( |
| - surroundingText, startOffset, endOffset)); |
| - notifyShowContextualSearch(selection); |
| + // TODO(donnd): Rename this method! |
| + System.out.println("ctxs onIcingSelectionAvailable"); |
| + if (!mController.isStillWorkingOn(State.GATHERING_SURROUNDINGS)) return; |
| + |
| + assert mContext != null; |
| + mContext.setSurroundingText(encoding, surroundingText, startOffset, endOffset); |
| + mController.notifyFinishedWorkOn(State.GATHERING_SURROUNDINGS); |
| } |
| /** |
| @@ -669,10 +688,13 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| final String mid, boolean doPreventPreload, int selectionStartAdjust, |
| int selectionEndAdjust, final String contextLanguage, final String thumbnailUrl, |
| final String caption, final String quickActionUri, final int quickActionCategory) { |
| + if (!mController.isStillWorkingOn(State.RESOLVING)) return; |
| + |
| mNetworkCommunicator.handleSearchTermResolutionResponse(isNetworkUnavailable, responseCode, |
| searchTerm, displayText, alternateTerm, mid, doPreventPreload, selectionStartAdjust, |
| selectionEndAdjust, contextLanguage, thumbnailUrl, caption, quickActionUri, |
| quickActionCategory); |
| + mController.notifyFinishedWorkOn(State.RESOLVING); |
| } |
| @Override |
| @@ -754,7 +776,7 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| // Adjust the selection unless the user changed it since we initiated the search. |
| if (selectionStartAdjust != 0 || selectionEndAdjust != 0) { |
| - String originalSelection = mContext == null ? null : mContext.getSelection(); |
| + String originalSelection = mContext == null ? null : mContext.getSelectedWord(); |
| if (originalSelection != null |
| && originalSelection.equals(mSelectionController.getSelectedText())) { |
| mSelectionController.adjustSelection(selectionStartAdjust, selectionEndAdjust); |
| @@ -877,6 +899,25 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| } |
| /** |
| + * Notifies that a new selection has been established that's available for Contextual Search. |
| + * Specifically this means we're showing the Contextual Search UX for the given selection. |
| + * Notifies Icing of the current selection. |
| + * Notifies the panel whether the selection was part of a URL. |
| + */ |
| + private void notifyContextualSearchSelection() { |
| + String surroundingText = mContext.getSurroundingText(); |
| + int startOffset = mContext.getSelectionStartOffset(); |
| + int endOffset = mContext.getSelectionEndOffset(); |
| + 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. |
| */ |
| @@ -1242,6 +1283,21 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| } |
| } |
| + // DLD @Override |
| + public void selectWordAroundCaretAck(boolean didSelect, int startAdjust, int endAdjust) { |
| + if (isOverlayVideoMode() || !mController.isStillWorkingOn(State.SELECTING_WORD)) return; |
| + if (didSelect) { |
| + // DLD needed???? |
| + // mSelectionController.selectWordAroundCaretAck(didSelect, startAdjust, |
| + // endAdjust); |
| + |
| + System.out.println("ctxs selectWordAroundCaretAck setting start/end adjust"); |
| + assert mContext != null; |
| + mContext.setSelectionAdjust(startAdjust, endAdjust); |
| + } |
| + mController.notifyFinishedWorkOn(State.SELECTING_WORD); |
| + } |
| + |
| private boolean isOverlayVideoMode() { |
| return mActivity.getFullscreenManager() != null |
| && mActivity.getFullscreenManager().isOverlayVideoMode(); |
| @@ -1278,7 +1334,14 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| public void handleSuppressedTap() { |
| if (mIsAccessibilityModeEnabled) return; |
| - hideContextualSearch(StateChangeReason.BASE_PAGE_TAP); |
| + hideContextualSearch(StateChangeReason.UNKNOWN); |
| + } |
| + |
| + @Override |
| + public void handleNonSuppressedTap() { |
| + if (mIsAccessibilityModeEnabled) return; |
| + |
| + mController.notifyFinishedWorkOn(State.DECIDING_SUPPRESSION); |
| } |
| @Override |
| @@ -1295,46 +1358,67 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| mSearchPanel.getPanelMetrics().setRankerLogExperiments(mHeuristics); |
| } |
| + /* |
| + Control flow between this class and the ContextualSearchSelectionController has changed in early |
| + 2017 and can be a bit complicated. |
| + Here's a summary of the Tap control flow: |
| + * The user taps or does a long-press, which establishes an initial selection (or caret for tap). |
| + * We gather page text surrounding the selection. Resolving taps need more than the other kinds. |
| + * We determine if UX should be shown or not (which may be based on page content). |
| + * If it was a tap: |
| + * We select the word. |
| + * Once the word is selected we show our UX and send a resolve request to the server. |
| + * When the search term returns, or if we didn't resolve, we prefetch the SERP in the overlay. |
| + * |
| + * This flow is implemented in these methods: |
| + * handleShowUnhandledTapUIIfNeeded -> handleValidTap (or handleInvalidTap) |
| + * handleValidTap --> nativeGatherSurroundingText OR handleSuppressedTap if tap not allowed. |
| + * nativeGatherSurroundingText --> onSurroundingTextAvailable --> handleShouldSuppressTap. |
| + * handleShouldSuppressTap --> handleSuppressedTap OR handleNonSuppressedTap. |
| + * handleNonSuppressedTap --> selectWordAroundCaret. |
| + * selectWordAroundCaret --> handleSelection --> showContextualSearch. |
| + */ |
| + |
| @Override |
| public void handleValidTap() { |
| if (mIsAccessibilityModeEnabled) return; |
| - if (isTapSupported()) { |
| - // Here we are probably starting a new Contextual Search with a Tap gesture (or we'll |
| - // ignore the tap), 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; |
| - |
| - if (mContext != null) mContext.destroy(); |
| - mContext = null; |
| - |
| - WebContents baseWebContents = getBaseWebContents(); |
| - if (baseWebContents != null) { |
| - // Let the policy know that a tap gesture has been received. |
| - mPolicy.registerTap(); |
| - |
| - baseWebContents.selectWordAroundCaret(); |
| - } |
| - } |
| + System.out.println("ctxs handleValidTap : " + mSelectionController.getSelectionType()); |
| + mController.enter(State.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) { |
| + float x, float y, long selectionStartTimeNanoseconds) { |
| + System.out.println("ctxs handleSelection " + selection); |
| if (mIsAccessibilityModeEnabled) return; |
| if (!selection.isEmpty()) { |
| - StateChangeReason stateChangeReason = type == SelectionType.TAP |
| - ? StateChangeReason.TEXT_SELECT_TAP : StateChangeReason.TEXT_SELECT_LONG_PRESS; |
| ContextualSearchUma.logSelectionIsValid(selectionValid); |
| + if (mContext != null) { |
| + mContext.updateContextFromSelection(selection, selectionStartTimeNanoseconds); |
| + } |
| if (selectionValid && mSearchPanel != null) { |
| mSearchPanel.updateBasePageSelectionYPx(y); |
| if (!mSearchPanel.isShowing()) { |
| mSearchPanel.getPanelMetrics().onSelectionEstablished(selection); |
| } |
| - showContextualSearch(stateChangeReason); |
| + |
| + if (type == SelectionType.TAP) { |
| + System.out.println("ctxs handleSelection Tap, selecting word!"); |
| + mController.notifyFinishedWorkOn(State.SELECTING_WORD); |
| + } else { |
| + System.out.println("ctxs handleSelection longpress!"); |
| + mController.enter(State.LONG_PRESS_RECOGNIZED); |
| + } |
| } else { |
| - hideContextualSearch(stateChangeReason); |
| + System.out.println("ctxs handleSelection HIDING!"); |
| + hideContextualSearch(StateChangeReason.INVALID_SELECTION); |
| } |
| } |
| } |
| @@ -1377,6 +1461,98 @@ public class ContextualSearchManager implements ContextualSearchManagementDelega |
| } |
| // ============================================================================================ |
| + // ContextualSearchStateControlled implementation. |
| + // ============================================================================================ |
| + |
| + /** |
| + * Gathers text surrounding the current selection, which may have been created by either a Tap |
| + * or a Long-press gesture. |
| + */ |
| + @Override |
| + public void gatherSurroundingText() { |
| + if (mContext != null) mContext.destroy(); |
| + boolean isTap = mSelectionController.getSelectionType() == SelectionType.TAP; |
| + if (isTap && mPolicy.shouldPreviousTapResolve()) { |
| + mContext = new ContextualSearchContext( |
| + mPolicy.getHomeCountry(mActivity), mPolicy.maySendBasePageUrl()); |
| + } else { |
| + mContext = new ContextualSearchContext(); |
| + } |
| + |
| + System.out.println("ctxs gatherSurroundingText"); |
| + mController.notifyStartingWorkOn(State.GATHERING_SURROUNDINGS); |
| + nativeGatherSurroundingText( |
| + mNativeContextualSearchManagerPtr, mContext, getBaseWebContents()); |
| + } |
| + |
| + /** |
| + * Starts the process of deciding if we'll suppress the current Tap gesture or not. |
| + */ |
| + @Override |
| + public void decideTapSuppression() { |
| + mController.notifyStartingWorkOn(State.DECIDING_SUPPRESSION); |
| + mSelectionController.handleShouldSuppressTap(); |
| + } |
| + |
| + /** |
| + * Starts the process of selecting a word around the current caret. |
| + */ |
| + @Override |
| + public void selectWordAroundCaret() { |
| + WebContents baseWebContents = getBaseWebContents(); |
| + if (baseWebContents != null) { |
| + mController.notifyStartingWorkOn(State.SELECTING_WORD); |
| + baseWebContents.selectWordAroundCaret(); |
| + } else { |
| + mController.reset(StateChangeReason.UNKNOWN); |
| + } |
| + } |
| + |
| + /** |
| + * Waits for possible navigation. |
| + * Many web pages have non-link elements that actually do navigation, so we pause before |
| + * advancing to the next processing state in order to detect the navigation before showing our |
| + * UX. |
| + * See crbug.com/428368. |
| + */ |
| + @Override |
| + public void waitForPossibleNavigation() { |
| + mController.notifyStartingWorkOn(State.WAITING_FOR_POSSIBLE_NAVIGATION); |
| + long elapsedTimeNanoseconds = |
| + System.nanoTime() - mContext.getSelectionStartTimeNanoseconds(); |
| + long elapsedTimeMilliseconds = TimeUnit.NANOSECONDS.toMillis(elapsedTimeNanoseconds); |
| + long delay = Math.max(0, TAP_NAVIGATION_DETECTION_DELAY - elapsedTimeMilliseconds); |
| + new Handler().postDelayed(new Runnable() { |
| + @Override |
| + public void run() { |
| + // If we detected a navigation, we'll be in the IDLE state again, but that's OK. |
| + mController.notifyFinishedWorkOn(State.WAITING_FOR_POSSIBLE_NAVIGATION); |
| + } |
| + }, delay); |
| + } |
| + |
| + /** |
| + * Starts a Resolve request to our server for the best Search Term. |
| + */ |
| + @Override |
| + public void startSearchTermResolutionRequest() { |
| + mController.notifyStartingWorkOn(State.RESOLVING); |
| + String selection = mSelectionController.getSelectedText(); |
| + assert !TextUtils.isEmpty(selection); |
| + mNetworkCommunicator.startSearchTermResolutionRequest(selection); |
| + } |
| + |
| + @Override |
| + public void hideContextualSearchUx(StateChangeReason reason) { |
| + handleHideContextualSearch(reason); |
| + } |
| + |
| + @Override |
| + public void showContextualSearchUx(StateChangeReason reason) { |
| + showContextualSearch(reason); |
| + } |
| + |
| + // ============================================================================================ |
| // Test helpers |
| // ============================================================================================ |