1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.contextualsearch;
6 
7 import android.graphics.Point;
8 import android.os.Handler;
9 import android.text.TextUtils;
10 import android.view.View;
11 import android.view.ViewGroup;
12 import android.view.ViewTreeObserver;
13 import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
14 
15 import androidx.annotation.NonNull;
16 import androidx.annotation.Nullable;
17 import androidx.annotation.VisibleForTesting;
18 
19 import org.chromium.base.Log;
20 import org.chromium.base.ObserverList;
21 import org.chromium.base.SysUtils;
22 import org.chromium.base.TimeUtils;
23 import org.chromium.base.annotations.CalledByNative;
24 import org.chromium.base.annotations.NativeMethods;
25 import org.chromium.base.supplier.Supplier;
26 import org.chromium.chrome.R;
27 import org.chromium.chrome.browser.app.ChromeActivity;
28 import org.chromium.chrome.browser.compositor.bottombar.OverlayContentDelegate;
29 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.PanelState;
30 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.StateChangeReason;
31 import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel;
32 import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
33 import org.chromium.chrome.browser.contextualsearch.ContextualSearchFieldTrial.ContextualSearchSetting;
34 import org.chromium.chrome.browser.contextualsearch.ContextualSearchFieldTrial.ContextualSearchSwitch;
35 import org.chromium.chrome.browser.contextualsearch.ContextualSearchInternalStateController.InternalState;
36 import org.chromium.chrome.browser.contextualsearch.ContextualSearchSelectionController.SelectionType;
37 import org.chromium.chrome.browser.contextualsearch.ResolvedSearchTerm.CardTag;
38 import org.chromium.chrome.browser.fullscreen.FullscreenManager;
39 import org.chromium.chrome.browser.fullscreen.FullscreenOptions;
40 import org.chromium.chrome.browser.gsa.GSAContextDisplaySelection;
41 import org.chromium.chrome.browser.infobar.InfoBarContainer;
42 import org.chromium.chrome.browser.preferences.Pref;
43 import org.chromium.chrome.browser.profiles.Profile;
44 import org.chromium.chrome.browser.tab.SadTab;
45 import org.chromium.chrome.browser.tab.Tab;
46 import org.chromium.chrome.browser.tab.TabCreationState;
47 import org.chromium.chrome.browser.tab.TabLaunchType;
48 import org.chromium.chrome.browser.tab.TabSelectionType;
49 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
50 import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
51 import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
52 import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
53 import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
54 import org.chromium.components.external_intents.ExternalNavigationHandler;
55 import org.chromium.components.external_intents.ExternalNavigationHandler.OverrideUrlLoadingResult;
56 import org.chromium.components.external_intents.ExternalNavigationParams;
57 import org.chromium.components.external_intents.RedirectHandler;
58 import org.chromium.components.navigation_interception.NavigationParams;
59 import org.chromium.components.prefs.PrefService;
60 import org.chromium.components.user_prefs.UserPrefs;
61 import org.chromium.content_public.browser.GestureStateListener;
62 import org.chromium.content_public.browser.LoadUrlParams;
63 import org.chromium.content_public.browser.NavigationEntry;
64 import org.chromium.content_public.browser.SelectionClient;
65 import org.chromium.content_public.browser.WebContents;
66 import org.chromium.content_public.common.BrowserControlsState;
67 import org.chromium.content_public.common.ContentUrlConstants;
68 import org.chromium.contextual_search.mojom.OverlayPosition;
69 import org.chromium.net.NetworkChangeNotifier;
70 import org.chromium.ui.touch_selection.SelectionEventType;
71 
72 import java.net.MalformedURLException;
73 import java.net.URL;
74 
75 /**
76  * Manages the Contextual Search feature. This class keeps track of the status of Contextual
77  * Search and coordinates the control with the layout.
78  * This class is driven by {@link ContextualSearchInternalStateController} through the
79  * {@link ContextualSearchInternalStateHandler} interface to advance each stage of processing
80  * events. The events are fed in by {@link ContextualSearchSelectionController} and business
81  * decisions are made in the {@link ContextualSearchPolicy} class. There is a native
82  * class corresponding to this class that communicates with the server through a delegate.
83  * The server interaction is vectored through an interface to allow a stub for testing in
84  * {@Link ContextualSearchNetworkCommunicator}.
85  * The lifetime of this class corresponds to the Activity, and this class creates and owns a
86  * {@link ContextualSearchPanel} with the same lifetime.
87  */
88 public class ContextualSearchManager
89         implements ContextualSearchManagementDelegate, ContextualSearchNetworkCommunicator,
90                    ContextualSearchSelectionHandler, ChromeAccessibilityUtil.Observer {
91     /** A delegate for reporting selected context to GSA for search quality. */
92     public interface ContextReporterDelegate {
93         /**
94          * Reports that the given display selection has been established for the current tab.
95          * @param displaySelection The information about the selection being displayed.
96          */
reportDisplaySelection(@ullable GSAContextDisplaySelection displaySelection)97         void reportDisplaySelection(@Nullable GSAContextDisplaySelection displaySelection);
98     }
99 
100     // TODO(donnd): provide an inner class that implements some of these interfaces rather than
101     // having the manager itself implement the interface because that exposes all the public methods
102     // of that interface at the manager level.
103 
104     private static final String TAG = "ContextualSearch";
105 
106     private static final String INTENT_URL_PREFIX = "intent:";
107 
108     // We blacklist this URL because malformed URLs may bring up this page.
109     private static final String BLACKLISTED_URL = ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL;
110 
111     // How long to wait for a tap near a previous tap before hiding the UI or showing a re-Tap.
112     // This setting is not critical: in practice it determines how long to wait after an invalid
113     // tap for the page to respond before hiding the UI. Specifically this setting just needs to be
114     // long enough for Blink's decisions before calling handleShowUnhandledTapUIIfNeeded (which
115     // probably are page-dependent), and short enough that the Bar goes away fairly quickly after a
116     // tap on non-text or whitespace: We currently do not get notification in these cases (hence the
117     // timer).
118     private static final int TAP_NEAR_PREVIOUS_DETECTION_DELAY_MS = 100;
119 
120     // How long to wait for a Tap to be converted to a Long-press gesture when the user taps on
121     // an existing tap-selection.
122     private static final int TAP_ON_TAP_SELECTION_DELAY_MS = 100;
123 
124     // Constants related to the Contextual Search preference.
125     private static final String CONTEXTUAL_SEARCH_DISABLED = "false";
126     private static final String CONTEXTUAL_SEARCH_ENABLED = "true";
127 
128     private final ObserverList<ContextualSearchObserver> mObservers =
129             new ObserverList<ContextualSearchObserver>();
130 
131     private final ChromeActivity mActivity;
132     private final ContextualSearchTabPromotionDelegate mTabPromotionDelegate;
133     private final ViewTreeObserver.OnGlobalFocusChangeListener mOnFocusChangeListener;
134     private final FullscreenManager.Observer mFullscreenObserver;
135 
136     /**
137      * The {@link ContextualSearchInteractionRecorder} to use to record user interactions and apply
138      * ML, etc.
139      */
140     private final ContextualSearchInteractionRecorder mInteractionRecorder;
141 
142     @VisibleForTesting
143     protected final ContextualSearchTranslation mTranslateController;
144     private final ContextualSearchSelectionClient mContextualSearchSelectionClient;
145     private final ContextualSearchIPH mInProductHelp;
146 
147     private final ScrimCoordinator mScrimCoordinator;
148 
149     private ContextualSearchSelectionController mSelectionController;
150     private ContextualSearchNetworkCommunicator mNetworkCommunicator;
151     @NonNull
152     private ContextualSearchPolicy mPolicy;
153     private ContextualSearchInternalStateController mInternalStateController;
154 
155     // The Overlay panel.
156     private ContextualSearchPanel mSearchPanel;
157 
158     // The native manager associated with this object.
159     private long mNativeContextualSearchManagerPtr;
160 
161     private ViewGroup mParentView;
162     private RedirectHandler mRedirectHandler;
163     private TabModelSelectorTabModelObserver mTabModelObserver;
164     private TabModelSelectorTabObserver mTabModelSelectorTabObserver;
165 
166     private boolean mDidStartLoadingResolvedSearchRequest;
167     private long mLoadedSearchUrlTimeMs;
168     private boolean mWereSearchResultsSeen;
169     private boolean mWereInfoBarsHidden;
170     private boolean mDidPromoteSearchNavigation;
171 
172     private boolean mWasActivatedByTap;
173     private boolean mIsInitialized;
174     private boolean mReceivedContextualCardsEntityData;
175 
176     // The current search context, or null.
177     private ContextualSearchContext mContext;
178 
179     /**
180      * This boolean is used for loading content after a long-press when content is not immediately
181      * loaded.
182      */
183     private boolean mShouldLoadDelayedSearch;
184 
185     private boolean mIsShowingPromo;
186     private boolean mIsMandatoryPromo;
187     private boolean mDidLogPromoOutcome;
188 
189     /**
190      * Whether contextual search manager is currently promoting a tab. We should be ignoring hide
191      * requests when mIsPromotingTab is set to true.
192      */
193     private boolean mIsPromotingToTab;
194 
195     private ContextualSearchRequest mSearchRequest;
196     private ContextualSearchRequest mLastSearchRequestLoaded;
197 
198     /** Whether the Accessibility Mode is enabled. */
199     private boolean mIsAccessibilityModeEnabled;
200 
201     /** Whether bottom sheet is visible. */
202     private boolean mIsBottomSheetVisible;
203 
204     /** Tap Experiments and other variable behavior. */
205     private QuickAnswersHeuristic mQuickAnswersHeuristic;
206 
207     // Counter for how many times we've called SelectWordAroundCaret without an ACK returned.
208     // TODO(donnd): replace with a more systematic approach using the InternalStateController.
209     private int mSelectWordAroundCaretCounter;
210 
211     /** An observer that reports selected context to GSA for search quality. */
212     private ContextualSearchObserver mContextReportingObserver;
213 
214     /** A means of accessing the currently active tab. */
215     private Supplier<Tab> mTabSupplier;
216 
217     /** A means of observing scene changes and attaching overlays. */
218     private LayoutManagerImpl mLayoutManager;
219 
220     /**
221      * The delegate that is responsible for promoting a {@link WebContents} to a {@link Tab}
222      * when necessary.
223      */
224     public interface ContextualSearchTabPromotionDelegate {
225         /**
226          * Called when {@link WebContents} for contextual search should be promoted to a {@link
227          * Tab}.
228          * @param searchUrl The Search URL to be promoted.
229          */
createContextualSearchTab(String searchUrl)230         void createContextualSearchTab(String searchUrl);
231     }
232 
233     /**
234      * Constructs the manager for the given activity, and will attach views to the given parent.
235      * @param activity The {@code ChromeActivity} in use.
236      * @param tabPromotionDelegate The {@link ContextualSearchTabPromotionDelegate} that is
237      *        responsible for building tabs from contextual search {@link WebContents}.
238      * @param scrimCoordinator A mechanism for showing and hiding the shared scrim.
239      * @param tabSupplier Access to the tab that is currently active.
240      */
ContextualSearchManager(ChromeActivity activity, ContextualSearchTabPromotionDelegate tabPromotionDelegate, ScrimCoordinator scrimCoordinator, Supplier<Tab> tabSupplier)241     public ContextualSearchManager(ChromeActivity activity,
242             ContextualSearchTabPromotionDelegate tabPromotionDelegate,
243             ScrimCoordinator scrimCoordinator, Supplier<Tab> tabSupplier) {
244         mActivity = activity;
245         mTabPromotionDelegate = tabPromotionDelegate;
246         mScrimCoordinator = scrimCoordinator;
247         mTabSupplier = tabSupplier;
248 
249         final View controlContainer = mActivity.findViewById(R.id.control_container);
250         mOnFocusChangeListener = new OnGlobalFocusChangeListener() {
251             @Override
252             public void onGlobalFocusChanged(View oldFocus, View newFocus) {
253                 if (controlContainer != null && controlContainer.hasFocus()) {
254                     hideContextualSearch(StateChangeReason.UNKNOWN);
255                 }
256             }
257         };
258 
259         mFullscreenObserver = new FullscreenManager.Observer() {
260             @Override
261             public void onEnterFullscreen(Tab tab, FullscreenOptions options) {
262                 hideContextualSearch(StateChangeReason.UNKNOWN);
263             }
264 
265             @Override
266             public void onExitFullscreen(Tab tab) {
267                 hideContextualSearch(StateChangeReason.UNKNOWN);
268             }
269         };
270 
271         mActivity.getFullscreenManager().addObserver(mFullscreenObserver);
272         mSelectionController = new ContextualSearchSelectionController(activity, this);
273         mNetworkCommunicator = this;
274         mPolicy = new ContextualSearchPolicy(mSelectionController, mNetworkCommunicator);
275         mTranslateController = new ContextualSearchTranslationImpl();
276         mInternalStateController = new ContextualSearchInternalStateController(
277                 mPolicy, getContextualSearchInternalStateHandler());
278         mInteractionRecorder = new ContextualSearchRankerLoggerImpl();
279         mContextualSearchSelectionClient = new ContextualSearchSelectionClient();
280         mInProductHelp = new ContextualSearchIPH();
281     }
282 
283     /**
284      * Initializes this manager.
285      * @param parentView The parent view to attach Contextual Search UX to.
286      * @param layoutManager A means of attaching the OverlayPanel to the scene.
287      */
initialize(ViewGroup parentView, LayoutManagerImpl layoutManager)288     public void initialize(ViewGroup parentView, LayoutManagerImpl layoutManager) {
289         mNativeContextualSearchManagerPtr = ContextualSearchManagerJni.get().init(this);
290 
291         mParentView = parentView;
292         mParentView.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnFocusChangeListener);
293 
294         mLayoutManager = layoutManager;
295 
296         ContextualSearchPanel panel = new ContextualSearchPanel(
297                 mActivity, mLayoutManager, mLayoutManager.getOverlayPanelManager());
298         panel.setManagementDelegate(this);
299         setContextualSearchPanel(panel);
300         mLayoutManager.addSceneOverlay(panel);
301 
302         mInProductHelp.setParentView(parentView);
303 
304         mRedirectHandler = RedirectHandler.create();
305 
306         mIsShowingPromo = false;
307         mDidLogPromoOutcome = false;
308         mDidStartLoadingResolvedSearchRequest = false;
309         mWereSearchResultsSeen = false;
310         mIsInitialized = true;
311 
312         mInternalStateController.reset(StateChangeReason.UNKNOWN);
313 
314         listenForTabModelSelectorNotifications();
315         ChromeAccessibilityUtil.get().addObserver(this);
316     }
317 
318     /**
319      * Destroys the native Contextual Search Manager.
320      * Call this method before orphaning this object to allow it to be garbage collected.
321      */
destroy()322     public void destroy() {
323         if (!mIsInitialized) return;
324 
325         hideContextualSearch(StateChangeReason.UNKNOWN);
326         mActivity.getFullscreenManager().removeObserver(mFullscreenObserver);
327         mParentView.getViewTreeObserver().removeOnGlobalFocusChangeListener(mOnFocusChangeListener);
328         ContextualSearchManagerJni.get().destroy(mNativeContextualSearchManagerPtr, this);
329         stopListeningForHideNotifications();
330         mRedirectHandler.clear();
331         mInternalStateController.enter(InternalState.UNDEFINED);
332         ChromeAccessibilityUtil.get().removeObserver(this);
333 
334         if (mSearchPanel != null) mSearchPanel.destroy();
335         mSearchPanel = null;
336     }
337 
338     @Override
setContextualSearchPanel(ContextualSearchPanel panel)339     public void setContextualSearchPanel(ContextualSearchPanel panel) {
340         assert panel != null;
341         mSearchPanel = panel;
342         mPolicy.setContextualSearchPanel(panel);
343         mInProductHelp.setSearchPanel(panel);
344     }
345 
346     @Override
getChromeActivity()347     public ChromeActivity getChromeActivity() {
348         return mActivity;
349     }
350 
351     /** @return Whether the Search Panel is opened. That is, whether it is EXPANDED or MAXIMIZED. */
isSearchPanelOpened()352     public boolean isSearchPanelOpened() {
353         return mSearchPanel != null && mSearchPanel.isPanelOpened();
354     }
355 
356     /** @return Whether the {@code mSearchPanel} is not {@code null} and is showing. */
isSearchPanelShowing()357     boolean isSearchPanelShowing() {
358         return mSearchPanel != null && mSearchPanel.isShowing();
359     }
360 
361     /** @return Whether the {@code mSearchPanel} is not {@code null} and is currently active. */
isSearchPanelActive()362     boolean isSearchPanelActive() {
363         return mSearchPanel != null && mSearchPanel.isActive();
364     }
365 
366     /**
367      * @return the {@link WebContents} of the {@code mSearchPanel} or {@code null} if
368      *         {@code mSearchPanel} is null or the search panel doesn't currently hold one.
369      */
getSearchPanelWebContents()370     private @Nullable WebContents getSearchPanelWebContents() {
371         return mSearchPanel == null ? null : mSearchPanel.getWebContents();
372     }
373 
374     /** @return The Base Page's {@link WebContents}. */
375     @Nullable
getBaseWebContents()376     private WebContents getBaseWebContents() {
377         return mSelectionController.getBaseWebContents();
378     }
379 
380     /** @return The Base Page's {@link URL}. */
381     @Nullable
getBasePageURL()382     private URL getBasePageURL() {
383         WebContents baseWebContents = mSelectionController.getBaseWebContents();
384         if (baseWebContents == null) return null;
385         try {
386             return new URL(baseWebContents.getVisibleUrl().getSpec());
387         } catch (MalformedURLException e) {
388             return null;
389         }
390     }
391 
392     /** Notifies that the base page has started loading a page. */
onBasePageLoadStarted()393     public void onBasePageLoadStarted() {
394         mSelectionController.onBasePageLoadStarted();
395     }
396 
397     /** Notifies that a Context Menu has been shown. */
onContextMenuShown()398     void onContextMenuShown() {
399         mSelectionController.onContextMenuShown();
400     }
401 
402     @Override
hideContextualSearch(@tateChangeReason int reason)403     public void hideContextualSearch(@StateChangeReason int reason) {
404         mInternalStateController.reset(reason);
405     }
406 
407     @Override
onCloseContextualSearch(@tateChangeReason int reason)408     public void onCloseContextualSearch(@StateChangeReason int reason) {
409         if (mSearchPanel == null) return;
410 
411         mSelectionController.onSearchEnded(reason);
412 
413         // Show the infobar container if it was visible before Contextual Search was shown.
414         if (mWereInfoBarsHidden) {
415             mWereInfoBarsHidden = false;
416             InfoBarContainer container = getInfoBarContainer();
417             if (container != null) {
418                 container.setHidden(false);
419             }
420         }
421 
422         if (mWereSearchResultsSeen) {
423             // Clear the selection, since the user just acted upon it by looking at the panel.
424             // However if the selection is invalid we don't need to clear it.
425             // The invalid selection might also be due to a "select-all" action by the user.
426             if (reason != StateChangeReason.INVALID_SELECTION) {
427                 mSelectionController.clearSelection();
428             }
429         } else if (mLoadedSearchUrlTimeMs != 0L) {
430             removeLastSearchVisit();
431         }
432 
433         // Clear the timestamp. This is to avoid future calls to hideContextualSearch clearing
434         // the current URL.
435         mLoadedSearchUrlTimeMs = 0L;
436         mWereSearchResultsSeen = false;
437 
438         mSearchRequest = null;
439 
440         mInProductHelp.onCloseContextualSearch();
441 
442         if (mIsShowingPromo && !mDidLogPromoOutcome && mSearchPanel.wasPromoInteractive()) {
443             ContextualSearchUma.logPromoOutcome(mWasActivatedByTap, mIsMandatoryPromo);
444             mDidLogPromoOutcome = true;
445         }
446 
447         mIsShowingPromo = false;
448         mSearchPanel.setIsPromoActive(false, false);
449         notifyHideContextualSearch();
450     }
451 
452     /**
453      * Shows the Contextual Search UX.
454      * @param stateChangeReason The reason explaining the change of state.
455      */
showContextualSearch(@tateChangeReason int stateChangeReason)456     private void showContextualSearch(@StateChangeReason int stateChangeReason) {
457         assert mSearchPanel != null;
458 
459         // Dismiss the undo SnackBar if present by committing all tab closures.
460         mActivity.getTabModelSelector().commitAllTabClosures();
461 
462         if (!mSearchPanel.isShowing()) {
463             // If visible, hide the infobar container before showing the Contextual Search panel.
464             InfoBarContainer container = getInfoBarContainer();
465             if (container != null && container.getVisibility() == View.VISIBLE) {
466                 mWereInfoBarsHidden = true;
467                 container.setHidden(true);
468             }
469         }
470 
471         // If the user is jumping from one unseen search to another search, remove the last search
472         // from history.
473         @PanelState
474         int state = mSearchPanel.getPanelState();
475         if (!mWereSearchResultsSeen && mLoadedSearchUrlTimeMs != 0L
476                 && state != PanelState.UNDEFINED && state != PanelState.CLOSED) {
477             removeLastSearchVisit();
478         }
479 
480         mSearchPanel.destroyContent();
481         mReceivedContextualCardsEntityData = false;
482 
483         String selection = mSelectionController.getSelectedText();
484         boolean canResolve = mPolicy.isResolvingGesture();
485         if (canResolve) {
486             // If we can resolve then we should not delay before loading content.
487             mShouldLoadDelayedSearch = false;
488         }
489         if (canResolve && mPolicy.shouldPreviousGestureResolve()) {
490             // For a resolving gestures we'll figure out translation need after the Resolve.
491         } else if (!TextUtils.isEmpty(selection)) {
492             // Build the literal search request for the selection.
493             boolean shouldPrefetch = mPolicy.shouldPrefetchSearchResult();
494             mSearchRequest = new ContextualSearchRequest(selection, shouldPrefetch);
495             mTranslateController.forceAutoDetectTranslateUnlessDisabled(mSearchRequest);
496             mDidStartLoadingResolvedSearchRequest = false;
497             mSearchPanel.setSearchTerm(selection);
498             if (shouldPrefetch) loadSearchUrl();
499         } else {
500             // The selection is no longer valid, so we can't build a request.  Don't show the UX.
501             hideContextualSearch(StateChangeReason.UNKNOWN);
502             return;
503         }
504         mWereSearchResultsSeen = false;
505 
506         // Note: now that the contextual search has properly started, set the promo involvement.
507         if (mPolicy.isPromoAvailable()) {
508             mIsShowingPromo = true;
509             mIsMandatoryPromo = mPolicy.isMandatoryPromoAvailable();
510             mDidLogPromoOutcome = false;
511             mSearchPanel.setIsPromoActive(true, mIsMandatoryPromo);
512             mSearchPanel.setDidSearchInvolvePromo();
513         }
514 
515         mSearchPanel.requestPanelShow(stateChangeReason);
516 
517         assert mSelectionController.getSelectionType() != SelectionType.UNDETERMINED;
518         mWasActivatedByTap = mSelectionController.getSelectionType() == SelectionType.TAP;
519 
520         mInProductHelp.onSearchPanelShown(mWasActivatedByTap,
521                 Profile.fromWebContents(mActivity.getActivityTab().getWebContents()));
522     }
523 
524     @Override
startSearchTermResolutionRequest(String selection, boolean isExactResolve)525     public void startSearchTermResolutionRequest(String selection, boolean isExactResolve) {
526         WebContents baseWebContents = getBaseWebContents();
527         if (baseWebContents != null && mContext != null && mContext.canResolve()) {
528             mContext.prepareToResolve(
529                     isExactResolve, mPolicy.getRelatedSearchesStamp((getBasePageLanguage())));
530             ContextualSearchManagerJni.get().startSearchTermResolutionRequest(
531                     mNativeContextualSearchManagerPtr, this, mContext, getBaseWebContents());
532             ContextualSearchUma.logResolveRequested(mSelectionController.isTapSelection());
533         } else {
534             // Something went wrong and we couldn't resolve.
535             hideContextualSearch(StateChangeReason.UNKNOWN);
536         }
537     }
538 
539     @Override
540     @Nullable
getBasePageUrl()541     public URL getBasePageUrl() {
542         WebContents baseWebContents = getBaseWebContents();
543         if (baseWebContents == null) return null;
544 
545         try {
546             return new URL(baseWebContents.getLastCommittedUrl());
547         } catch (MalformedURLException e) {
548             return null;
549         }
550     }
551 
552     /** Accessor for the {@code InfoBarContainer} currently attached to the {@code Tab}. */
getInfoBarContainer()553     private InfoBarContainer getInfoBarContainer() {
554         Tab tab = mActivity.getActivityTab();
555         return tab == null ? null : InfoBarContainer.get(tab);
556     }
557 
558     /** Listens for notifications that should hide the Contextual Search bar. */
listenForTabModelSelectorNotifications()559     private void listenForTabModelSelectorNotifications() {
560         TabModelSelector selector = mActivity.getTabModelSelector();
561         mTabModelObserver = new TabModelSelectorTabModelObserver(selector) {
562             @Override
563             public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) {
564                 if ((!mIsPromotingToTab && tab.getId() != lastId)
565                         || mActivity.getTabModelSelector().isIncognitoSelected()) {
566                     hideContextualSearch(StateChangeReason.UNKNOWN);
567                     mSelectionController.onTabSelected();
568                 }
569             }
570 
571             @Override
572             public void didAddTab(
573                     Tab tab, @TabLaunchType int type, @TabCreationState int creationState) {
574                 // If we're in the process of promoting this tab, just return and don't mess with
575                 // this state.
576                 if (tab.getWebContents() == getSearchPanelWebContents()) return;
577                 hideContextualSearch(StateChangeReason.UNKNOWN);
578             }
579         };
580         mTabModelSelectorTabObserver = new TabModelSelectorTabObserver(selector) {
581             @Override
582             public void onPageLoadStarted(Tab tab, String url) {
583                 // Detects navigation of the base page for crbug.com/428368 (navigation-detection).
584                 hideContextualSearch(StateChangeReason.UNKNOWN);
585             }
586 
587             @Override
588             public void onCrash(Tab tab) {
589                 if (SadTab.isShowing(tab)) {
590                     // Hide contextual search if the foreground tab crashed
591                     hideContextualSearch(StateChangeReason.UNKNOWN);
592                 }
593             }
594 
595             @Override
596             public void onClosingStateChanged(Tab tab, boolean closing) {
597                 if (closing) hideContextualSearch(StateChangeReason.UNKNOWN);
598             }
599         };
600     }
601 
602     /** Stops listening for notifications that should hide the Contextual Search bar. */
stopListeningForHideNotifications()603     private void stopListeningForHideNotifications() {
604         if (mTabModelObserver != null) mTabModelObserver.destroy();
605         if (mTabModelSelectorTabObserver != null) mTabModelSelectorTabObserver.destroy();
606         mTabModelObserver = null;
607         mTabModelSelectorTabObserver = null;
608     }
609 
610     /** Clears our private member referencing the native manager. */
611     @CalledByNative
clearNativeManager()612     public void clearNativeManager() {
613         assert mNativeContextualSearchManagerPtr != 0;
614         mNativeContextualSearchManagerPtr = 0;
615     }
616 
617     /**
618      * Sets our private member referencing the native manager.
619      * @param nativeManager The pointer to the native Contextual Search manager.
620      */
621     @CalledByNative
setNativeManager(long nativeManager)622     public void setNativeManager(long nativeManager) {
623         assert mNativeContextualSearchManagerPtr == 0;
624         mNativeContextualSearchManagerPtr = nativeManager;
625     }
626 
627     /**
628      * Called by native code when the surrounding text and selection range are available.
629      * This is done for both Tap and Long-press gestures.
630      * @param encoding The original encoding used on the base page.
631      * @param surroundingText The Text surrounding the selection.
632      * @param startOffset The start offset of the selection.
633      * @param endOffset The end offset of the selection.
634      */
635     @CalledByNative
636     @VisibleForTesting
onTextSurroundingSelectionAvailable( final String encoding, final String surroundingText, int startOffset, int endOffset)637     void onTextSurroundingSelectionAvailable(
638             final String encoding, final String surroundingText, int startOffset, int endOffset) {
639         if (mInternalStateController.isStillWorkingOn(InternalState.GATHERING_SURROUNDINGS)) {
640             assert mContext != null;
641             // Sometimes Blink returns empty surroundings and 0 offsets so reset in that case.
642             // See crbug.com/393100.
643             if (surroundingText.length() == 0) {
644                 mInternalStateController.reset(StateChangeReason.UNKNOWN);
645             } else {
646                 mContext.setSurroundingText(encoding, surroundingText, startOffset, endOffset);
647                 mPolicy.logRelatedSearchesQualifiedUsers(getBasePageLanguage());
648                 mInternalStateController.notifyFinishedWorkOn(InternalState.GATHERING_SURROUNDINGS);
649             }
650         }
651     }
652 
653     /**
654      * Called in response to the {@link ContextualSearchManagerJni#startSearchTermResolutionRequest}
655      * method. If {@code startSearchTermResolutionRequest} is called with a previous request sill
656      * pending our native delegate is supposed to cancel all previous requests.  So this code should
657      * only be called with data corresponding to the most recent request.
658      * @param isNetworkUnavailable Indicates if the network is unavailable, in which case all other
659      *        parameters should be ignored.
660      * @param responseCode The HTTP response code. If the code is not OK, the query should be
661      *        ignored.
662      * @param searchTerm The term to use in our subsequent search.
663      * @param displayText The text to display in our UX.
664      * @param alternateTerm The alternate term to display on the results page.
665      * @param mid the MID for an entity to use to trigger a Knowledge Panel, or an empty string.
666      *        A MID is a unique identifier for an entity in the Search Knowledge Graph.
667      * @param selectionStartAdjust A positive number of characters that the start of the existing
668      *        selection should be expanded by.
669      * @param selectionEndAdjust A positive number of characters that the end of the existing
670      *        selection should be expanded by.
671      * @param contextLanguage The language of the original search term, or an empty string.
672      * @param thumbnailUrl The URL of the thumbnail to display in our UX.
673      * @param caption The caption to display.
674      * @param quickActionUri The URI for the intent associated with the quick action.
675      * @param quickActionCategory The {@link QuickActionCategory} for the quick action.
676      * @param loggedEventId The EventID logged by the server, which should be recorded and sent back
677      *        to the server along with user action results in a subsequent request.
678      * @param searchUrlFull The URL for the full search to present in the overlay, or empty.
679      * @param searchUrlPreload The URL for the search to preload into the overlay, or empty.
680      * @param cocaCardTag The primary internal Coca card tag for the response, or {@code 0} if none.
681      */
682     @CalledByNative
onSearchTermResolutionResponse(boolean isNetworkUnavailable, int responseCode, final String searchTerm, final String displayText, final String alternateTerm, final String mid, boolean doPreventPreload, int selectionStartAdjust, int selectionEndAdjust, final String contextLanguage, final String thumbnailUrl, final String caption, final String quickActionUri, @QuickActionCategory final int quickActionCategory, final long loggedEventId, final String searchUrlFull, final String searchUrlPreload, @CardTag final int cocaCardTag)683     public void onSearchTermResolutionResponse(boolean isNetworkUnavailable, int responseCode,
684             final String searchTerm, final String displayText, final String alternateTerm,
685             final String mid, boolean doPreventPreload, int selectionStartAdjust,
686             int selectionEndAdjust, final String contextLanguage, final String thumbnailUrl,
687             final String caption, final String quickActionUri,
688             @QuickActionCategory final int quickActionCategory, final long loggedEventId,
689             final String searchUrlFull, final String searchUrlPreload,
690             @CardTag final int cocaCardTag) {
691         ContextualSearchUma.logResolveReceived(mSelectionController.isTapSelection());
692         ResolvedSearchTerm resolvedSearchTerm =
693                 new ResolvedSearchTerm
694                         .Builder(isNetworkUnavailable, responseCode, searchTerm, displayText,
695                                 alternateTerm, mid, doPreventPreload, selectionStartAdjust,
696                                 selectionEndAdjust, contextLanguage, thumbnailUrl, caption,
697                                 quickActionUri, quickActionCategory, loggedEventId, searchUrlFull,
698                                 searchUrlPreload, cocaCardTag)
699                         .build();
700         mNetworkCommunicator.handleSearchTermResolutionResponse(resolvedSearchTerm);
701     }
702 
703     @Override
handleSearchTermResolutionResponse(ResolvedSearchTerm resolvedSearchTerm)704     public void handleSearchTermResolutionResponse(ResolvedSearchTerm resolvedSearchTerm) {
705         if (!mInternalStateController.isStillWorkingOn(InternalState.RESOLVING)) return;
706 
707         // Show an appropriate message for what to search for.
708         String message;
709         boolean doLiteralSearch = false;
710         if (resolvedSearchTerm.isNetworkUnavailable()) {
711             // TODO(donnd): double-check that the network is really unavailable, maybe using
712             // NetworkChangeNotifier#isOnline.
713             message = mActivity.getResources().getString(
714                     R.string.contextual_search_network_unavailable);
715         } else if (!isHttpFailureCode(resolvedSearchTerm.responseCode())
716                 && !TextUtils.isEmpty(resolvedSearchTerm.displayText())) {
717             message = resolvedSearchTerm.displayText();
718         } else if (!mPolicy.shouldShowErrorCodeInBar()) {
719             message = mSelectionController.getSelectedText();
720             doLiteralSearch = true;
721         } else {
722             message = mActivity.getResources().getString(
723                     R.string.contextual_search_error, resolvedSearchTerm.responseCode());
724             doLiteralSearch = true;
725         }
726 
727         boolean receivedCaptionOrThumbnail = !TextUtils.isEmpty(resolvedSearchTerm.caption())
728                 || !TextUtils.isEmpty(resolvedSearchTerm.thumbnailUrl());
729 
730         assert mSearchPanel != null;
731         mSearchPanel.onSearchTermResolved(message, resolvedSearchTerm.thumbnailUrl(),
732                 resolvedSearchTerm.quickActionUri(), resolvedSearchTerm.quickActionCategory(),
733                 resolvedSearchTerm.cardTagEnum());
734         if (!TextUtils.isEmpty(resolvedSearchTerm.caption())) {
735             // Call #onSetCaption() to set the caption. For entities, the caption should not be
736             // regarded as an answer. In the future, when quick actions are added, doesAnswer will
737             // need to be determined rather than always set to false.
738             boolean doesAnswer = false;
739             onSetCaption(resolvedSearchTerm.caption(), doesAnswer);
740         }
741 
742         boolean quickActionShown =
743                 mSearchPanel.getSearchBarControl().getQuickActionControl().hasQuickAction();
744         mReceivedContextualCardsEntityData = !quickActionShown && receivedCaptionOrThumbnail;
745 
746         if (mReceivedContextualCardsEntityData) {
747             mInProductHelp.onEntityDataReceived(
748                     mWasActivatedByTap, Profile.getLastUsedRegularProfile());
749         }
750 
751         ContextualSearchUma.logContextualCardsDataShown(mReceivedContextualCardsEntityData);
752         mSearchPanel.getPanelMetrics().setWasContextualCardsDataShown(
753                 mReceivedContextualCardsEntityData, resolvedSearchTerm.cardTagEnum());
754         ContextualSearchUma.logQuickActionShown(
755                 quickActionShown, resolvedSearchTerm.quickActionCategory());
756         mSearchPanel.getPanelMetrics().setWasQuickActionShown(
757                 quickActionShown, resolvedSearchTerm.quickActionCategory());
758 
759         // If there was an error, fall back onto a literal search for the selection.
760         // Since we're showing the panel, there must be a selection.
761         String searchTerm = resolvedSearchTerm.searchTerm();
762         String alternateTerm = resolvedSearchTerm.alternateTerm();
763         boolean doPreventPreload = resolvedSearchTerm.doPreventPreload();
764         if (doLiteralSearch) {
765             searchTerm = mSelectionController.getSelectedText();
766             alternateTerm = null;
767             doPreventPreload = true;
768         }
769         if (!TextUtils.isEmpty(searchTerm)) {
770             // TODO(donnd): Instead of preloading, we should prefetch (ie the URL should not
771             // appear in the user's history until the user views it).  See crbug.com/406446.
772             boolean shouldPreload = !doPreventPreload && mPolicy.shouldPrefetchSearchResult();
773             mSearchRequest = new ContextualSearchRequest(searchTerm, alternateTerm,
774                     resolvedSearchTerm.mid(), shouldPreload, resolvedSearchTerm.searchUrlFull(),
775                     resolvedSearchTerm.searchUrlPreload());
776             // Trigger translation, if enabled.
777             mTranslateController.forceTranslateIfNeeded(mSearchRequest,
778                     resolvedSearchTerm.contextLanguage(), mSelectionController.isTapSelection());
779             mDidStartLoadingResolvedSearchRequest = false;
780             if (mSearchPanel.isContentShowing()) {
781                 mSearchRequest.setNormalPriority();
782             }
783             if (mSearchPanel.isContentShowing() || shouldPreload) {
784                 loadSearchUrl();
785             }
786             mPolicy.logSearchTermResolutionDetails(searchTerm);
787         }
788 
789         // Adjust the selection unless the user changed it since we initiated the search.
790         int selectionStartAdjust = resolvedSearchTerm.selectionStartAdjust();
791         int selectionEndAdjust = resolvedSearchTerm.selectionEndAdjust();
792         if ((selectionStartAdjust != 0 || selectionEndAdjust != 0)
793                 && (mSelectionController.getSelectionType() == SelectionType.TAP
794                         || mSelectionController.getSelectionType()
795                                 == SelectionType.RESOLVING_LONG_PRESS)) {
796             String originalSelection = mContext == null ? null : mContext.getInitialSelectedWord();
797             String currentSelection = mSelectionController.getSelectedText();
798             if (currentSelection != null) currentSelection = currentSelection.trim();
799             if (originalSelection != null && originalSelection.trim().equals(currentSelection)) {
800                 mSelectionController.adjustSelection(selectionStartAdjust, selectionEndAdjust);
801                 mContext.onSelectionAdjusted(selectionStartAdjust, selectionEndAdjust);
802             }
803         }
804 
805         // Tell the Interaction Recorder about the current Event ID for persisted interaction.
806         mInteractionRecorder.persistInteraction(resolvedSearchTerm.loggedEventId());
807 
808         mInternalStateController.notifyFinishedWorkOn(InternalState.RESOLVING);
809     }
810 
811     /**
812      * External entry point to determine if the device is currently online or not.
813      * Stubbed out when under test.
814      * @return Whether the device is currently online.
815      */
isDeviceOnline()816     boolean isDeviceOnline() {
817         return mNetworkCommunicator.isOnline();
818     }
819 
820     /** Handles this {@link ContextualSearchNetworkCommunicator} vector when not under test. */
821     @Override
isOnline()822     public boolean isOnline() {
823         return NetworkChangeNotifier.isOnline();
824     }
825 
826     /** Loads a Search Request in the Contextual Search's Content View. */
loadSearchUrl()827     private void loadSearchUrl() {
828         assert mSearchPanel != null;
829         mLoadedSearchUrlTimeMs = System.currentTimeMillis();
830         mLastSearchRequestLoaded = mSearchRequest;
831         String searchUrl = mSearchRequest.getSearchUrl();
832         ContextualSearchManagerJni.get().whitelistContextualSearchJsApiUrl(
833                 mNativeContextualSearchManagerPtr, this, searchUrl);
834         mSearchPanel.loadUrlInPanel(searchUrl);
835         mDidStartLoadingResolvedSearchRequest = true;
836 
837         // TODO(donnd): If the user taps on a word and quickly after that taps on the
838         // peeking Search Bar, the Search Content View will not be displayed. It seems that
839         // calling WebContents.onShow() while it's being created has no effect.
840         // For now, we force the ContentView to be displayed by calling onShow() again
841         // when a URL is being loaded. See: crbug.com/398206
842         if (mSearchPanel.isContentShowing() && getSearchPanelWebContents() != null) {
843             getSearchPanelWebContents().onShow();
844         }
845     }
846 
847     /**
848      * Called to set a caption. The caption may either be included with the search term resolution
849      * response or set by the page through the CS JavaScript API used to notify CS that there is
850      * a caption available on the current overlay.
851      * @param caption The caption to display.
852      * @param doesAnswer Whether the caption should be regarded as an answer such
853      *        that the user may not need to open the panel, or whether the caption
854      *        is simply informative or descriptive of the answer in the full results.
855      */
856     @CalledByNative
onSetCaption(String caption, boolean doesAnswer)857     private void onSetCaption(String caption, boolean doesAnswer) {
858         if (TextUtils.isEmpty(caption) || mSearchPanel == null) return;
859 
860         // Notify the UI of the caption.
861         mSearchPanel.setCaption(caption);
862         if (mQuickAnswersHeuristic != null) {
863             mQuickAnswersHeuristic.setConditionSatisfied(true);
864             mQuickAnswersHeuristic.setDoesAnswer(doesAnswer);
865         }
866 
867         // Update Tap counters to account for a possible answer.
868         mPolicy.updateCountersForQuickAnswer(mWasActivatedByTap, doesAnswer);
869     }
870 
871     /**
872      * Called by JavaScript in the Overlay to change the position of the overlay.
873      * The panel cannot be changed to any opened position if it's not already opened.
874      * @param desiredPosition The desired position of the Overlay Panel expressed as an
875      *        OverlayPosition int (defined in contextual_search_js_api_service.mojom).
876      */
877     @CalledByNative
onChangeOverlayPosition(int desiredPosition)878     private void onChangeOverlayPosition(int desiredPosition) {
879         assert desiredPosition >= OverlayPosition.CLOSE
880                 && desiredPosition <= OverlayPosition.MAXIMIZE;
881         // Ignore requests when the panel is not already open to prevent spam or abuse of the API.
882         if (!mSearchPanel.isShowing() || desiredPosition < OverlayPosition.CLOSE
883                 || desiredPosition > OverlayPosition.MAXIMIZE) {
884             Log.w(TAG, "Unexpected request to set Overlay position to " + desiredPosition);
885             return;
886         }
887 
888         // Set the position.
889         switch (desiredPosition) {
890             case OverlayPosition.CLOSE:
891                 mSearchPanel.closePanel(StateChangeReason.UNKNOWN, true);
892                 break;
893             case OverlayPosition.PEEK:
894                 mSearchPanel.peekPanel(StateChangeReason.UNKNOWN);
895                 break;
896             case OverlayPosition.EXPAND:
897                 mSearchPanel.expandPanel(StateChangeReason.UNKNOWN);
898                 break;
899             case OverlayPosition.MAXIMIZE:
900                 mSearchPanel.maximizePanel(StateChangeReason.UNKNOWN);
901                 break;
902         }
903     }
904 
905     @Override
onAccessibilityModeChanged(boolean enabled)906     public void onAccessibilityModeChanged(boolean enabled) {
907         mIsAccessibilityModeEnabled = enabled;
908         if (enabled) hideContextualSearch(StateChangeReason.UNKNOWN);
909     }
910 
911     /**
912      * Update bottom sheet visibility state.
913      */
onBottomSheetVisible(boolean visible)914     public void onBottomSheetVisible(boolean visible) {
915         mIsBottomSheetVisible = visible;
916         if (visible) hideContextualSearch(StateChangeReason.RESET);
917     }
918 
919     /**
920      * Notifies that the preference state has changed.
921      * @param isEnabled Whether the feature is enabled.
922      */
onContextualSearchPrefChanged(boolean isEnabled)923     public void onContextualSearchPrefChanged(boolean isEnabled) {
924         // The pref may be automatically changed during application startup due to enterprise
925         // configuration settings, so we may not have a panel yet.
926         if (mSearchPanel != null) mSearchPanel.onContextualSearchPrefChanged(isEnabled);
927     }
928 
929     @Override
stopPanelContentsNavigation()930     public void stopPanelContentsNavigation() {
931         if (getSearchPanelWebContents() == null) return;
932 
933         getSearchPanelWebContents().stop();
934     }
935 
936     // ============================================================================================
937     // Observers
938     // ============================================================================================
939 
940     /** @param observer An observer to notify when the user performs a contextual search. */
addObserver(ContextualSearchObserver observer)941     void addObserver(ContextualSearchObserver observer) {
942         mObservers.addObserver(observer);
943     }
944 
945     /** @param observer An observer to no longer notify when the user performs a contextual search.
946      */
removeObserver(ContextualSearchObserver observer)947     void removeObserver(ContextualSearchObserver observer) {
948         mObservers.removeObserver(observer);
949     }
950 
951     /**
952      * Notifies that a new selection has been established and available for Contextual Search.
953      * Should be called when the selection changes to notify listeners that care about the selection
954      * and surrounding text.
955      * Specifically this means we're showing the Contextual Search UX for the given selection.
956      * Notifies Icing of the current selection.
957      * Also notifies the panel whether the selection was part of a URL.
958      */
notifyObserversOfContextSelectionChanged()959     private void notifyObserversOfContextSelectionChanged() {
960         assert mContext != null;
961         String surroundingText = mContext.getSurroundingText();
962         assert surroundingText != null;
963         int startOffset = mContext.getSelectionStartOffset();
964         int endOffset = mContext.getSelectionEndOffset();
965         if (!ContextualSearchFieldTrial.getSwitch(
966                     ContextualSearchSwitch.IS_PAGE_CONTENT_NOTIFICATION_DISABLED)) {
967             GSAContextDisplaySelection selection = new GSAContextDisplaySelection(
968                     mContext.getEncoding(), surroundingText, startOffset, endOffset);
969             notifyShowContextualSearch(selection);
970         }
971     }
972 
973     /**
974      * Notifies all Contextual Search observers that a search has occurred.
975      * @param selectionContext The selection and context that triggered the search.
976      */
notifyShowContextualSearch(GSAContextDisplaySelection selectionContext)977     private void notifyShowContextualSearch(GSAContextDisplaySelection selectionContext) {
978         if (!mPolicy.canSendSurroundings()) selectionContext = null;
979 
980         for (ContextualSearchObserver observer : mObservers) {
981             observer.onShowContextualSearch(selectionContext);
982         }
983     }
984 
985     /** Notifies all Contextual Search observers that a search ended and is no longer in effect. */
notifyHideContextualSearch()986     private void notifyHideContextualSearch() {
987         for (ContextualSearchObserver observer : mObservers) {
988             observer.onHideContextualSearch();
989         }
990     }
991 
992     // ============================================================================================
993     // OverlayContentDelegate
994     // ============================================================================================
995 
996     @Override
getOverlayContentDelegate()997     public OverlayContentDelegate getOverlayContentDelegate() {
998         return new SearchOverlayContentDelegate();
999     }
1000 
1001     /** Implementation of OverlayContentDelegate. Made public for testing purposes. */
1002     public class SearchOverlayContentDelegate extends OverlayContentDelegate {
1003         // Note: New navigation or changes to the WebContents are not advised in this class since
1004         // the WebContents is being observed and navigation is already being performed.
1005 
SearchOverlayContentDelegate()1006         public SearchOverlayContentDelegate() {}
1007 
1008         @Override
onMainFrameLoadStarted(String url, boolean isExternalUrl)1009         public void onMainFrameLoadStarted(String url, boolean isExternalUrl) {
1010             assert mSearchPanel != null;
1011             mSearchPanel.updateBrowserControlsState();
1012 
1013             if (isExternalUrl) {
1014                 onExternalNavigation(url);
1015             }
1016         }
1017 
1018         @Override
onMainFrameNavigation( String url, boolean isExternalUrl, boolean isFailure, boolean isError)1019         public void onMainFrameNavigation(
1020                 String url, boolean isExternalUrl, boolean isFailure, boolean isError) {
1021             assert mSearchPanel != null;
1022             if (isExternalUrl) {
1023                 if (!ContextualSearchFieldTrial.getSwitch(
1024                             ContextualSearchSwitch.IS_AMP_AS_SEPARATE_TAB_DISABLED)
1025                         && mPolicy.isAmpUrl(url) && mSearchPanel.didTouchContent()) {
1026                     onExternalNavigation(url);
1027                 }
1028             } else {
1029                 // Could be just prefetching, check if that failed.
1030                 onContextualSearchRequestNavigation(isFailure);
1031 
1032                 // Record metrics for when the prefetched results became viewable.
1033                 if (mSearchRequest != null && mSearchRequest.wasPrefetch()) {
1034                     boolean didResolve = mPolicy.shouldPreviousGestureResolve();
1035                     mSearchPanel.onPanelNavigatedToPrefetchedSearch(didResolve);
1036                 }
1037             }
1038         }
1039 
1040         @Override
onContentLoadStarted(String url)1041         public void onContentLoadStarted(String url) {
1042             mDidPromoteSearchNavigation = false;
1043         }
1044 
1045         @Override
onVisibilityChanged(boolean isVisible)1046         public void onVisibilityChanged(boolean isVisible) {
1047             if (isVisible) {
1048                 mWereSearchResultsSeen = true;
1049                 // If there's no current request, then either a search term resolution
1050                 // is in progress or we should do a verbatim search now.
1051                 if (mSearchRequest == null && mPolicy.shouldCreateVerbatimRequest()
1052                         && !TextUtils.isEmpty(mSelectionController.getSelectedText())) {
1053                     mSearchRequest =
1054                             new ContextualSearchRequest(mSelectionController.getSelectedText());
1055                     mDidStartLoadingResolvedSearchRequest = false;
1056                 }
1057                 if (mSearchRequest != null
1058                         && (!mDidStartLoadingResolvedSearchRequest || mShouldLoadDelayedSearch)) {
1059                     // mShouldLoadDelayedSearch is used in the non-preloading case to load content.
1060                     // Since content is now created and destroyed for each request, was impossible
1061                     // to know if content was already loaded or recently needed to be; this is for
1062                     // the case where it needed to be.
1063                     mSearchRequest.setNormalPriority();
1064                     loadSearchUrl();
1065                 }
1066                 mShouldLoadDelayedSearch = true;
1067                 mPolicy.updateCountersForOpen();
1068             }
1069         }
1070 
1071         @Override
onContentViewCreated()1072         public void onContentViewCreated() {
1073             ContextualSearchManagerJni.get().enableContextualSearchJsApiForWebContents(
1074                     mNativeContextualSearchManagerPtr, ContextualSearchManager.this,
1075                     getSearchPanelWebContents());
1076         }
1077 
1078         @Override
onContentViewSeen()1079         public void onContentViewSeen() {
1080             assert mSearchPanel != null;
1081             mSearchPanel.setWasSearchContentViewSeen();
1082         }
1083 
1084         @Override
shouldInterceptNavigation( ExternalNavigationHandler externalNavHandler, NavigationParams navigationParams)1085         public boolean shouldInterceptNavigation(
1086                 ExternalNavigationHandler externalNavHandler, NavigationParams navigationParams) {
1087             assert mSearchPanel != null;
1088             mRedirectHandler.updateNewUrlLoading(navigationParams.pageTransitionType,
1089                     navigationParams.isRedirect,
1090                     navigationParams.hasUserGesture || navigationParams.hasUserGestureCarryover,
1091                     mActivity.getLastUserInteractionTime(), RedirectHandler.INVALID_ENTRY_INDEX);
1092             ExternalNavigationParams params =
1093                     new ExternalNavigationParams
1094                             .Builder(navigationParams.url, false, navigationParams.referrer,
1095                                     navigationParams.pageTransitionType,
1096                                     navigationParams.isRedirect)
1097                             .setApplicationMustBeInForeground(true)
1098                             .setRedirectHandler(mRedirectHandler)
1099                             .setIsMainFrame(navigationParams.isMainFrame)
1100                             .build();
1101             if (externalNavHandler.shouldOverrideUrlLoading(params)
1102                     != OverrideUrlLoadingResult.NO_OVERRIDE) {
1103                 return false;
1104             }
1105             return !navigationParams.isExternalProtocol;
1106         }
1107     }
1108 
1109     // ============================================================================================
1110     // Search Content View
1111     // ============================================================================================
1112 
1113     /** Removes the last resolved search URL from the Chrome history. */
removeLastSearchVisit()1114     private void removeLastSearchVisit() {
1115         assert mSearchPanel != null;
1116         if (mLastSearchRequestLoaded != null) {
1117             // TODO(pedrosimonetti): Consider having this feature builtin into OverlayPanelContent.
1118             mSearchPanel.removeLastHistoryEntry(
1119                     mLastSearchRequestLoaded.getSearchUrl(), mLoadedSearchUrlTimeMs);
1120         }
1121     }
1122 
1123     /**
1124      * Called when the Search content view navigates to a contextual search request URL.
1125      * This navigation could be for a prefetch when the panel is still closed, or
1126      * a load of a user-visible search result.
1127      * @param isFailure Whether the navigation failed.
1128      */
onContextualSearchRequestNavigation(boolean isFailure)1129     private void onContextualSearchRequestNavigation(boolean isFailure) {
1130         if (mSearchRequest == null) return;
1131 
1132         if (mSearchRequest.isUsingLowPriority()) {
1133             ContextualSearchUma.logLowPrioritySearchRequestOutcome(isFailure);
1134         } else {
1135             ContextualSearchUma.logNormalPrioritySearchRequestOutcome(isFailure);
1136             if (mSearchRequest.getHasFailed()) {
1137                 ContextualSearchUma.logFallbackSearchRequestOutcome(isFailure);
1138             }
1139         }
1140 
1141         if (isFailure && mSearchRequest.isUsingLowPriority()) {
1142             // We're navigating to an error page, so we want to stop and retry.
1143             // Stop loading the page that displays the error to the user.
1144             if (getSearchPanelWebContents() != null) {
1145                 // When running tests the Content View might not exist.
1146                 mNetworkCommunicator.stopPanelContentsNavigation();
1147             }
1148             mSearchRequest.setHasFailed();
1149             mSearchRequest.setNormalPriority();
1150             // If the content view is showing, load at normal priority now.
1151             if (mSearchPanel != null && mSearchPanel.isContentShowing()) {
1152                 // NOTE: we must reuse the existing content view because we're called from within
1153                 // a WebContentsObserver.  If we don't reuse the content view then the WebContents
1154                 // being observed will be deleted.  We notify of the failure to trigger the reuse.
1155                 // See crbug.com/682953 for details.
1156                 mSearchPanel.onLoadUrlFailed();
1157                 loadSearchUrl();
1158             } else {
1159                 mDidStartLoadingResolvedSearchRequest = false;
1160             }
1161         }
1162     }
1163 
1164     // ============================================================================================
1165     // ContextualSearchManagementDelegate Overrides
1166     // ============================================================================================
1167 
1168     @Override
logCurrentState()1169     public void logCurrentState() {
1170         if (ContextualSearchFieldTrial.isEnabled()) mPolicy.logCurrentState();
1171     }
1172 
1173     /** @return Whether the given HTTP result code represents a failure or not. */
isHttpFailureCode(int httpResultCode)1174     private boolean isHttpFailureCode(int httpResultCode) {
1175         return httpResultCode <= 0 || httpResultCode >= 400;
1176     }
1177 
1178     /** @return whether a navigation in the search content view should promote to a separate tab. */
shouldPromoteSearchNavigation()1179     private boolean shouldPromoteSearchNavigation() {
1180         // A navigation can be due to us loading a URL, or a touch in the search content view.
1181         // Require a touch, but no recent loading, in order to promote to a separate tab.
1182         // Note that tapping the opt-in button requires checking for recent loading.
1183         assert mSearchPanel != null;
1184         return mSearchPanel.didTouchContent() && !mSearchPanel.isProcessingPendingNavigation();
1185     }
1186 
1187     /**
1188      * Called to check if an external navigation is being done and take the appropriate action:
1189      * Auto-promotes the panel into a separate tab if that's not already being done.
1190      * @param url The URL we are navigating to.
1191      */
onExternalNavigation(String url)1192     public void onExternalNavigation(String url) {
1193         if (!mDidPromoteSearchNavigation && mSearchPanel != null && !BLACKLISTED_URL.equals(url)
1194                 && !url.startsWith(INTENT_URL_PREFIX) && shouldPromoteSearchNavigation()) {
1195             // Do not promote to a regular tab if we're loading our Resolved Search
1196             // URL, otherwise we'll promote it when prefetching the Serp.
1197             // Don't promote URLs when they are navigating to an intent - this is
1198             // handled by the InterceptNavigationDelegate which uses a faster
1199             // maximizing animation.
1200             mDidPromoteSearchNavigation = true;
1201             mSearchPanel.maximizePanelThenPromoteToTab(StateChangeReason.SERP_NAVIGATION);
1202         }
1203     }
1204 
1205     @Override
openResolvedSearchUrlInNewTab()1206     public void openResolvedSearchUrlInNewTab() {
1207         if (mSearchRequest != null && mSearchRequest.getSearchUrlForPromotion() != null) {
1208             TabModelSelector tabModelSelector = mActivity.getTabModelSelector();
1209             tabModelSelector.openNewTab(
1210                     new LoadUrlParams(mSearchRequest.getSearchUrlForPromotion()),
1211                     TabLaunchType.FROM_LINK,
1212                     tabModelSelector.getCurrentTab(),
1213                     tabModelSelector.isIncognitoSelected());
1214         }
1215     }
1216 
1217     @Override
isRunningInCompatibilityMode()1218     public boolean isRunningInCompatibilityMode() {
1219         return SysUtils.isLowEndDevice();
1220     }
1221 
1222     @Override
promoteToTab()1223     public void promoteToTab() {
1224         assert mSearchPanel != null;
1225         // TODO(pedrosimonetti): Consider removing this member.
1226         mIsPromotingToTab = true;
1227 
1228         // If the request object is null that means that a Contextual Search has just started
1229         // and the Search Term Resolution response hasn't arrived yet. In this case, promoting
1230         // the Panel to a Tab will result in creating a new tab with URL about:blank. To prevent
1231         // this problem, we are ignoring tap gestures in the Search Bar if we don't know what
1232         // to search for.
1233         if (mSearchRequest != null && getSearchPanelWebContents() != null) {
1234             String url = getContentViewUrl(getSearchPanelWebContents());
1235 
1236             // If it's a search URL, format it so the SearchBox becomes visible.
1237             if (mSearchRequest.isContextualSearchUrl(url)) {
1238                 url = mSearchRequest.getSearchUrlForPromotion();
1239             }
1240 
1241             if (url != null) {
1242                 mTabPromotionDelegate.createContextualSearchTab(url);
1243                 mSearchPanel.closePanel(StateChangeReason.TAB_PROMOTION, false);
1244             }
1245         }
1246         mIsPromotingToTab = false;
1247     }
1248 
1249     /**
1250      * Gets the currently loading or loaded URL in a WebContents.
1251      *
1252      * @param searchWebContents The given WebContents.
1253      * @return The current loaded URL.
1254      */
getContentViewUrl(WebContents searchWebContents)1255     private String getContentViewUrl(WebContents searchWebContents) {
1256         // First, check the pending navigation entry, because there might be an navigation
1257         // not yet committed being processed. Otherwise, get the URL from the WebContents.
1258         NavigationEntry entry = searchWebContents.getNavigationController().getPendingEntry();
1259         return entry != null ? entry.getUrl() : searchWebContents.getLastCommittedUrl();
1260     }
1261 
1262     @Override
dismissContextualSearchBar()1263     public void dismissContextualSearchBar() {
1264         hideContextualSearch(StateChangeReason.UNKNOWN);
1265     }
1266 
1267     @Override
onPanelFinishedShowing()1268     public void onPanelFinishedShowing() {
1269         Profile profile = Profile.getLastUsedRegularProfile();
1270         mInProductHelp.onPanelFinishedShowing(mWasActivatedByTap, profile);
1271         // Try to figure out the language of the selection and show an IPH if a translation
1272         // is needed.
1273         if (mContext != null && mPolicy.isUserUndecided()
1274                 && mTranslateController.needsTranslation(mContext.getDetectedLanguage())) {
1275             mInProductHelp.onTranslationNeeded(profile);
1276         }
1277     }
1278 
1279     @Override
onPanelResized()1280     public void onPanelResized() {
1281         mInProductHelp.updateBubblePosition();
1282     }
1283 
1284     @Override
getScrimCoordinator()1285     public ScrimCoordinator getScrimCoordinator() {
1286         return mScrimCoordinator;
1287     }
1288 
1289     @Override
onPromoOptIn()1290     public void onPromoOptIn() {
1291         mInProductHelp.doUserOptedInNotifications(Profile.getLastUsedRegularProfile());
1292     }
1293 
1294     /** @return The {@link SelectionClient} used by Contextual Search. */
getContextualSearchSelectionClient()1295     SelectionClient getContextualSearchSelectionClient() {
1296         return mContextualSearchSelectionClient;
1297     }
1298 
1299     /**
1300      * Implements the {@link SelectionClient} interface for Contextual Search.
1301      * Handles messages from Content about selection changes.  These are the key drivers of
1302      * Contextual Search logic.
1303      */
1304     private class ContextualSearchSelectionClient implements SelectionClient {
1305         @Override
onSelectionChanged(String selection)1306         public void onSelectionChanged(String selection) {
1307             if (mSearchPanel != null) {
1308                 mSelectionController.handleSelectionChanged(selection);
1309                 mSearchPanel.updateBrowserControlsState(BrowserControlsState.BOTH, true);
1310             }
1311         }
1312 
1313         @Override
onSelectionEvent( @electionEventType int eventType, float posXPix, float posYPix)1314         public void onSelectionEvent(
1315                 @SelectionEventType int eventType, float posXPix, float posYPix) {
1316             mSelectionController.handleSelectionEvent(eventType, posXPix, posYPix);
1317         }
1318 
1319         @Override
selectWordAroundCaretAck(boolean didSelect, int startAdjust, int endAdjust)1320         public void selectWordAroundCaretAck(boolean didSelect, int startAdjust, int endAdjust) {
1321             if (mSelectWordAroundCaretCounter > 0) mSelectWordAroundCaretCounter--;
1322             if (mSelectWordAroundCaretCounter > 0
1323                     || !mInternalStateController.isStillWorkingOn(
1324                                InternalState.START_SHOWING_TAP_UI)) {
1325                 return;
1326             }
1327 
1328             // Process normally unless something went wrong with the selection or an IPH triggered
1329             // on tap when promoting longpress, otherwise just finish up.
1330             if (didSelect && !mInProductHelp.isShowingForTappedButShouldLongpress()) {
1331                 assert mContext != null;
1332                 mContext.onSelectionAdjusted(startAdjust, endAdjust);
1333                 // There's a race condition when we select the word between this Ack response and
1334                 // the onSelectionChanged call.  Update the selection in case this method won the
1335                 // race so we ensure that there's a valid selected word.
1336                 // See https://crbug.com/889657 for details.
1337                 String adjustedSelection = mContext.getSelection();
1338                 if (!TextUtils.isEmpty(adjustedSelection)) {
1339                     mSelectionController.setSelectedText(adjustedSelection);
1340                 }
1341                 showSelectionAsSearchInBar(mSelectionController.getSelectedText());
1342                 mInternalStateController.notifyFinishedWorkOn(InternalState.START_SHOWING_TAP_UI);
1343             } else {
1344                 hideContextualSearch(StateChangeReason.UNKNOWN);
1345             }
1346         }
1347 
1348         @Override
requestSelectionPopupUpdates(boolean shouldSuggest)1349         public boolean requestSelectionPopupUpdates(boolean shouldSuggest) {
1350             return false;
1351         }
1352 
1353         @Override
cancelAllRequests()1354         public void cancelAllRequests() {}
1355     }
1356 
1357     /** Shows the Unhandled Tap UI.  Called by {@link ContextualSearchTabHelper}. */
onShowUnhandledTapUIIfNeeded(int x, int y, int fontSizeDips, int textRunLength)1358     void onShowUnhandledTapUIIfNeeded(int x, int y, int fontSizeDips, int textRunLength) {
1359         mSelectionController.handleShowUnhandledTapUIIfNeeded(x, y, fontSizeDips, textRunLength);
1360     }
1361 
1362     // ============================================================================================
1363     // Selection
1364     // ============================================================================================
1365 
1366     /**
1367      * Returns a new {@code GestureStateListener} that will listen for events in the Base Page.
1368      * This listener will handle all Contextual Search-related interactions that go through the
1369      * listener.
1370      */
getGestureStateListener()1371     public GestureStateListener getGestureStateListener() {
1372         return mSelectionController.getGestureStateListener();
1373     }
1374 
1375     @Override
handleScrollStart()1376     public void handleScrollStart() {
1377         if (isSuppressed()) return;
1378 
1379         hideContextualSearch(StateChangeReason.BASE_PAGE_SCROLL);
1380     }
1381 
1382     @Override
handleScrollEnd()1383     public void handleScrollEnd() {
1384         if (mSelectionController.getSelectionType() == SelectionType.RESOLVING_LONG_PRESS) {
1385             mSearchPanel.showPanel(StateChangeReason.BASE_PAGE_SCROLL);
1386         }
1387     }
1388 
1389     @Override
handleInvalidTap()1390     public void handleInvalidTap() {
1391         if (isSuppressed()) return;
1392 
1393         hideContextualSearch(StateChangeReason.BASE_PAGE_TAP);
1394     }
1395 
1396     @Override
handleSuppressedTap()1397     public void handleSuppressedTap() {
1398         if (isSuppressed()) return;
1399 
1400         hideContextualSearch(StateChangeReason.TAP_SUPPRESS);
1401     }
1402 
1403     @Override
handleNonSuppressedTap(long tapTimeNanoseconds)1404     public void handleNonSuppressedTap(long tapTimeNanoseconds) {
1405         if (isSuppressed()) return;
1406 
1407         // If there's a wait-after-tap experiment then we may want to delay a bit longer for
1408         // the user to take an action like scrolling that will reset our internal state.
1409         long delayBeforeFinishingWorkMs = 0;
1410         if (ContextualSearchFieldTrial.getValue(ContextualSearchSetting.WAIT_AFTER_TAP_DELAY_MS) > 0
1411                 && tapTimeNanoseconds > 0) {
1412             delayBeforeFinishingWorkMs = ContextualSearchFieldTrial.getValue(
1413                                                  ContextualSearchSetting.WAIT_AFTER_TAP_DELAY_MS)
1414                     - (System.nanoTime() - tapTimeNanoseconds)
1415                             / TimeUtils.NANOSECONDS_PER_MILLISECOND;
1416         }
1417 
1418         // Finish work on the current state, either immediately or with a delay.
1419         if (delayBeforeFinishingWorkMs <= 0) {
1420             finishSuppressionDecision();
1421         } else {
1422             new Handler().postDelayed(new Runnable() {
1423                 @Override
1424                 public void run() {
1425                     finishSuppressionDecision();
1426                 }
1427             }, delayBeforeFinishingWorkMs);
1428         }
1429     }
1430 
1431     /**
1432      * Finishes work on the suppression decision if that work is still in progress.
1433      * If no longer working on the suppression decision then resets the Ranker-logger.
1434      */
finishSuppressionDecision()1435     private void finishSuppressionDecision() {
1436         if (mInternalStateController.isStillWorkingOn(InternalState.DECIDING_SUPPRESSION)) {
1437             mInternalStateController.notifyFinishedWorkOn(InternalState.DECIDING_SUPPRESSION);
1438         } else {
1439             mInteractionRecorder.reset();
1440         }
1441     }
1442 
1443     @Override
handleMetricsForWouldSuppressTap(ContextualSearchHeuristics tapHeuristics)1444     public void handleMetricsForWouldSuppressTap(ContextualSearchHeuristics tapHeuristics) {
1445         mQuickAnswersHeuristic = tapHeuristics.getQuickAnswersHeuristic();
1446         if (mSearchPanel != null) {
1447             mSearchPanel.getPanelMetrics().setResultsSeenExperiments(tapHeuristics);
1448         }
1449     }
1450 
1451     @Override
handleValidTap(int x, int y)1452     public void handleValidTap(int x, int y) {
1453         if (isSuppressed()) return;
1454 
1455         if (!mPolicy.isTapSupported() && mPolicy.canResolveLongpress()) {
1456             // User tapped when Longpress is needed.  Convert location to screen coordinates, and
1457             // put up some in-product help.
1458             int yOffset = (int) mActivity.getBrowserControlsManager().getTopVisibleContentOffset();
1459             int parentScreenXy[] = new int[2];
1460             mParentView.getLocationInWindow(parentScreenXy);
1461             mInProductHelp.onNonTriggeringTap(Profile.getLastUsedRegularProfile(),
1462                     new Point(x + parentScreenXy[0], y + yOffset + parentScreenXy[1]),
1463                     new CtrSuppression().getPrevious28DayCtr() > 0,
1464                     () -> mSelectionController.clearSelection());
1465         }
1466 
1467         // This will synchronously advance to the next state (and possibly others) before
1468         // returning.
1469         mInternalStateController.enter(InternalState.TAP_RECOGNIZED);
1470     }
1471 
1472     @Override
handleValidResolvingLongpress()1473     public void handleValidResolvingLongpress() {
1474         if (isSuppressed() || !mPolicy.canResolveLongpress()) return;
1475 
1476         mInternalStateController.enter(InternalState.RESOLVING_LONG_PRESS_RECOGNIZED);
1477     }
1478 
1479     /**
1480      * Notifies this class that the selection has changed. This may be due to the user moving the
1481      * selection handles after a long-press, or after a Tap gesture has called selectWordAroundCaret
1482      * to expand the selection to a whole word.
1483      */
1484     @Override
handleSelection( String selection, boolean selectionValid, @SelectionType int type, float x, float y)1485     public void handleSelection(
1486             String selection, boolean selectionValid, @SelectionType int type, float x, float y) {
1487         if (isSuppressed()) return;
1488 
1489         if (!selection.isEmpty()) {
1490             ContextualSearchUma.logSelectionIsValid(selectionValid);
1491 
1492             if (selectionValid && mSearchPanel != null) {
1493                 mSearchPanel.updateBasePageSelectionYPx(y);
1494                 if (!mSearchPanel.isShowing()) {
1495                     mSearchPanel.getPanelMetrics().onSelectionEstablished(selection);
1496                 }
1497                 showSelectionAsSearchInBar(selection);
1498 
1499                 if (type == SelectionType.LONG_PRESS) {
1500                     mInternalStateController.enter(InternalState.LONG_PRESS_RECOGNIZED);
1501                 } else if (type == SelectionType.RESOLVING_LONG_PRESS) {
1502                     mInternalStateController.enter(InternalState.RESOLVING_LONG_PRESS_RECOGNIZED);
1503                 }
1504             } else {
1505                 hideContextualSearch(StateChangeReason.INVALID_SELECTION);
1506             }
1507         }
1508     }
1509 
1510     @Override
handleSelectionDismissal()1511     public void handleSelectionDismissal() {
1512         if (isSuppressed()) return;
1513 
1514         if (isSearchPanelShowing()
1515                 && !mIsPromotingToTab
1516                 // If the selection is dismissed when the Panel is not peeking anymore,
1517                 // which means the Panel is at least partially expanded, then it means
1518                 // the selection was cleared by an external source (like JavaScript),
1519                 // so we should not dismiss the UI in here.
1520                 // See crbug.com/516665
1521                 && mSearchPanel.isPeeking()) {
1522             hideContextualSearch(StateChangeReason.CLEARED_SELECTION);
1523         }
1524     }
1525 
1526     @Override
handleSelectionModification( String selection, boolean selectionValid, float x, float y)1527     public void handleSelectionModification(
1528             String selection, boolean selectionValid, float x, float y) {
1529         if (isSuppressed()) return;
1530 
1531         if (isSearchPanelShowing()) {
1532             if (selectionValid) {
1533                 mSearchPanel.setSearchTerm(selection);
1534             } else {
1535                 hideContextualSearch(StateChangeReason.INVALID_SELECTION);
1536             }
1537         }
1538     }
1539 
1540     @Override
handleSelectionCleared()1541     public void handleSelectionCleared() {
1542         // The selection was just cleared, so we'll want to remove our UX unless it was due to
1543         // another Tap while the Bar is showing.
1544         mInternalStateController.enter(InternalState.SELECTION_CLEARED_RECOGNIZED);
1545     }
1546 
1547     @Override
logNonHeuristicFeatures(ContextualSearchInteractionRecorder rankerLogger)1548     public void logNonHeuristicFeatures(ContextualSearchInteractionRecorder rankerLogger) {
1549         boolean didOptIn = !mPolicy.isUserUndecided();
1550         rankerLogger.logFeature(ContextualSearchInteractionRecorder.Feature.DID_OPT_IN, didOptIn);
1551         boolean isHttp = mPolicy.isBasePageHTTP(getBasePageURL());
1552         rankerLogger.logFeature(ContextualSearchInteractionRecorder.Feature.IS_HTTP, isHttp);
1553         String contentLanguage = mContext.getDetectedLanguage();
1554         boolean isLanguageMismatch = mTranslateController.needsTranslation(contentLanguage);
1555         rankerLogger.logFeature(ContextualSearchInteractionRecorder.Feature.IS_LANGUAGE_MISMATCH,
1556                 isLanguageMismatch);
1557     }
1558 
1559     /** Shows the given selection as the Search Term in the Bar. */
showSelectionAsSearchInBar(String selection)1560     private void showSelectionAsSearchInBar(String selection) {
1561         if (isSearchPanelShowing()) mSearchPanel.setSearchTerm(selection);
1562     }
1563 
1564     // ============================================================================================
1565     // ContextualSearchInternalStateHandler implementation.
1566     // ============================================================================================
1567 
1568     @VisibleForTesting
getContextualSearchInternalStateHandler()1569     ContextualSearchInternalStateHandler getContextualSearchInternalStateHandler() {
1570         return new ContextualSearchInternalStateHandler() {
1571             @Override
1572             public void hideContextualSearchUi(@StateChangeReason int reason) {
1573                 // Called when the IDLE state has been entered.
1574                 if (mContext != null) mContext.destroy();
1575                 mContext = null;
1576                 if (mSearchPanel == null) return;
1577 
1578                 // Make sure we write to Ranker and reset at the end of every search, even if the
1579                 // panel was not showing because it was a suppressed tap.
1580                 mSearchPanel.getPanelMetrics().writeInteractionOutcomesAndReset();
1581                 if (isSearchPanelShowing()) {
1582                     mSearchPanel.closePanel(reason, false);
1583                 } else {
1584                     // Also clear any tap-based selection unless the Tap IPH is showing. In the
1585                     // latter case we preserve the selection so the help bubble has something to
1586                     // point to.
1587                     if (!mPolicy.isLiteralSearchTapEnabled()
1588                             && mSelectionController.getSelectionType() == SelectionType.TAP
1589                             && !mInProductHelp.isShowingForTappedButShouldLongpress()) {
1590                         mSelectionController.clearSelection();
1591                     }
1592                 }
1593             }
1594 
1595             @Override
1596             public void gatherSurroundingText() {
1597                 if (mContext != null) mContext.destroy();
1598                 mContext = new ContextualSearchContext() {
1599                     @Override
1600                     void onSelectionChanged() {
1601                         notifyObserversOfContextSelectionChanged();
1602                     }
1603                 };
1604 
1605                 boolean isResolvingGesture = mPolicy.isResolvingGesture();
1606                 if (isResolvingGesture && mPolicy.shouldPreviousGestureResolve()) {
1607                     ContextualSearchInteractionPersister.PersistedInteraction interaction =
1608                             mInteractionRecorder.getInteractionPersister()
1609                                     .getAndClearPersistedInteraction();
1610                     String targetLanguage =
1611                             mTranslateController.getTranslateServiceTargetLanguage();
1612                     targetLanguage = targetLanguage != null ? targetLanguage : "";
1613                     String fluentLanguages =
1614                             mTranslateController.getTranslateServiceFluentLanguages();
1615                     fluentLanguages = fluentLanguages != null ? fluentLanguages : "";
1616                     mContext.setResolveProperties(mPolicy.getHomeCountry(mActivity),
1617                             mPolicy.doSendBasePageUrl(), interaction.getEventId(),
1618                             interaction.getEncodedUserInteractions(), targetLanguage,
1619                             fluentLanguages);
1620                 }
1621                 WebContents webContents = getBaseWebContents();
1622                 if (webContents != null) {
1623                     mInternalStateController.notifyStartingWorkOn(
1624                             InternalState.GATHERING_SURROUNDINGS);
1625                     ContextualSearchManagerJni.get().gatherSurroundingText(
1626                             mNativeContextualSearchManagerPtr, ContextualSearchManager.this,
1627                             mContext, webContents);
1628                 } else {
1629                     mInternalStateController.reset(StateChangeReason.UNKNOWN);
1630                 }
1631             }
1632 
1633             /** First step where we're committed to processing the current Tap gesture. */
1634             @Override
1635             public void tapGestureCommit() {
1636                 mInternalStateController.notifyStartingWorkOn(InternalState.TAP_GESTURE_COMMIT);
1637                 if (!mPolicy.isTapSupported()
1638                                 && !mInProductHelp.isShowingForTappedButShouldLongpress()
1639                         || mSelectionController.getSelectionType()
1640                                 == SelectionType.RESOLVING_LONG_PRESS) {
1641                     hideContextualSearch(StateChangeReason.UNKNOWN);
1642                     return;
1643                 }
1644                 // We may be processing a chained search (aka a retap -- a tap near a previous tap).
1645                 // If it's chained we need to log the outcomes and reset, because we won't be hiding
1646                 // the panel at the end of the previous search (we'll update it to the new Search).
1647                 if (isSearchPanelShowing()) {
1648                     mSearchPanel.getPanelMetrics().writeInteractionOutcomesAndReset();
1649                 }
1650                 // Set up the next batch of Ranker logging.
1651                 mInteractionRecorder.setupLoggingForPage(getBaseWebContents());
1652                 mSearchPanel.getPanelMetrics().setInteractionRecorder(mInteractionRecorder);
1653                 ContextualSearchUma.logRankerFeaturesAvailable(false);
1654                 mInternalStateController.notifyFinishedWorkOn(InternalState.TAP_GESTURE_COMMIT);
1655             }
1656 
1657             /** Starts the process of deciding if we'll suppress the current Tap gesture or not. */
1658             @Override
1659             public void decideSuppression() {
1660                 mInternalStateController.notifyStartingWorkOn(InternalState.DECIDING_SUPPRESSION);
1661 
1662                 // We may have gotten here even without Tap being supported if an IPH for Tap
1663                 // is active.  In that case we want to be sure to show, so skip the suppression
1664                 // decision.
1665                 if (mInProductHelp.isShowingForTappedButShouldLongpress()) {
1666                     mInternalStateController.notifyFinishedWorkOn(
1667                             InternalState.DECIDING_SUPPRESSION);
1668                     return;
1669                 }
1670 
1671                 // TODO(donnd): Move handleShouldSuppressTap out of the Selection Controller.
1672                 mSelectionController.handleShouldSuppressTap(mContext, mInteractionRecorder);
1673             }
1674 
1675             /** Starts showing the Tap UI by selecting a word around the current caret. */
1676             @Override
1677             public void startShowingTapUi() {
1678                 WebContents baseWebContents = getBaseWebContents();
1679                 if (baseWebContents != null) {
1680                     mInternalStateController.notifyStartingWorkOn(
1681                             InternalState.START_SHOWING_TAP_UI);
1682                     mSelectWordAroundCaretCounter++;
1683                     baseWebContents.selectWordAroundCaret();
1684                     // Let the policy know that a valid tap gesture has been received.
1685                     mPolicy.registerTap();
1686                 } else {
1687                     mInternalStateController.reset(StateChangeReason.UNKNOWN);
1688                 }
1689             }
1690 
1691             /**
1692              * Waits for possible Tap gesture that's near enough to the previous tap to be
1693              * considered a "re-tap". We've done some work on the previous Tap and we just saw the
1694              * selection get cleared (probably due to a Tap that may or may not be valid).
1695              * If it's invalid we'll want to hide the UI.  If it's valid we'll want to just update
1696              * the UI rather than having the Bar hide and re-show.
1697              */
1698             @Override
1699             public void waitForPossibleTapNearPrevious() {
1700                 mInternalStateController.notifyStartingWorkOn(
1701                         InternalState.WAITING_FOR_POSSIBLE_TAP_NEAR_PREVIOUS);
1702                 new Handler().postDelayed(new Runnable() {
1703                     @Override
1704                     public void run() {
1705                         mInternalStateController.notifyFinishedWorkOn(
1706                                 InternalState.WAITING_FOR_POSSIBLE_TAP_NEAR_PREVIOUS);
1707                     }
1708                 }, TAP_NEAR_PREVIOUS_DETECTION_DELAY_MS);
1709             }
1710 
1711             /**
1712              * Waits for possible Tap gesture that's on a previously established tap-selection.
1713              * If the current Tap was on the previous tap-selection then this selection will become
1714              * a Long-press selection and we'll recognize that gesture and start processing it.
1715              * If that doesn't happen within our time window (which is the common case) then we'll
1716              * advance to the next state in normal Tap processing.
1717              */
1718             @Override
1719             public void waitForPossibleTapOnTapSelection() {
1720                 mInternalStateController.notifyStartingWorkOn(
1721                         InternalState.WAITING_FOR_POSSIBLE_TAP_ON_TAP_SELECTION);
1722                 new Handler().postDelayed(new Runnable() {
1723                     @Override
1724                     public void run() {
1725                         mInternalStateController.notifyFinishedWorkOn(
1726                                 InternalState.WAITING_FOR_POSSIBLE_TAP_ON_TAP_SELECTION);
1727                     }
1728                 }, TAP_ON_TAP_SELECTION_DELAY_MS);
1729             }
1730 
1731             /** Starts a Resolve request to our server for the best Search Term. */
1732             @Override
1733             public void resolveSearchTerm() {
1734                 mInternalStateController.notifyStartingWorkOn(InternalState.RESOLVING);
1735 
1736                 String selection = mSelectionController.getSelectedText();
1737                 assert !TextUtils.isEmpty(selection);
1738                 mNetworkCommunicator.startSearchTermResolutionRequest(
1739                         selection, mSelectionController.isAdjustedSelection());
1740                 // If the we were unable to start the resolve, we've hidden the UI and set the
1741                 // context to null.
1742                 if (mContext == null || mSearchPanel == null) return;
1743 
1744                 // Update the UI to show the resolve is in progress.
1745                 mSearchPanel.setContextDetails(
1746                         selection, mContext.getTextContentFollowingSelection());
1747             }
1748 
1749             @Override
1750             public void showContextualSearchResolvingUi() {
1751                 if (mSelectionController.getSelectionType() == SelectionType.UNDETERMINED) {
1752                     mInternalStateController.reset(StateChangeReason.INVALID_SELECTION);
1753                 } else {
1754                     mInternalStateController.notifyStartingWorkOn(InternalState.SHOW_RESOLVING_UI);
1755                     boolean isTap = mSelectionController.getSelectionType() == SelectionType.TAP;
1756                     showContextualSearch(isTap ? StateChangeReason.TEXT_SELECT_TAP
1757                                                : StateChangeReason.TEXT_SELECT_LONG_PRESS);
1758                     if (isTap) ContextualSearchUma.logRankerFeaturesAvailable(true);
1759                     mInternalStateController.notifyFinishedWorkOn(InternalState.SHOW_RESOLVING_UI);
1760                 }
1761             }
1762 
1763             @Override
1764             public void showContextualSearchLiteralSearchUi() {
1765                 mInternalStateController.notifyStartingWorkOn(InternalState.SHOWING_LITERAL_SEARCH);
1766                 showContextualSearch(
1767                         mSelectionController.getSelectionType() == SelectionType.LONG_PRESS
1768                                 ? StateChangeReason.TEXT_SELECT_LONG_PRESS
1769                                 : StateChangeReason.TEXT_SELECT_TAP);
1770                 mInternalStateController.notifyFinishedWorkOn(InternalState.SHOWING_LITERAL_SEARCH);
1771             }
1772         };
1773     }
1774 
1775     /**
1776      * @param reporter A context reporter for the feature to report the current selection when
1777      *                 triggered.
1778      */
1779     public void enableContextReporting(ContextReporterDelegate reporter) {
1780         mContextReportingObserver = new ContextualSearchObserver() {
1781             @Override
1782             public void onShowContextualSearch(GSAContextDisplaySelection contextSelection) {
1783                 if (contextSelection != null) reporter.reportDisplaySelection(contextSelection);
1784             }
1785 
1786             @Override
1787             public void onHideContextualSearch() {
1788                 reporter.reportDisplaySelection(null);
1789             }
1790         };
1791         addObserver(mContextReportingObserver);
1792     }
1793 
1794     /**
1795      * Disable context reporting for Contextual Search.
1796      */
1797     public void disableContextReporting() {
1798         removeObserver(mContextReportingObserver);
1799         mContextReportingObserver = null;
1800     }
1801 
1802     /**
1803      * @return Whether the Contextual Search feature was disabled by the user explicitly.
1804      */
1805     public static boolean isContextualSearchDisabled() {
1806         return getPrefService()
1807                 .getString(Pref.CONTEXTUAL_SEARCH_ENABLED)
1808                 .equals(CONTEXTUAL_SEARCH_DISABLED);
1809     }
1810 
1811     /**
1812      * @return Whether the Contextual Search feature is disabled by policy.
1813      */
1814     public static boolean isContextualSearchDisabledByPolicy() {
1815         return getPrefService().isManagedPreference(Pref.CONTEXTUAL_SEARCH_ENABLED)
1816                 && isContextualSearchDisabled();
1817     }
1818 
1819     /**
1820      * @return Whether the Contextual Search feature is uninitialized (preference unset by the
1821      *         user).
1822      */
1823     public static boolean isContextualSearchUninitialized() {
1824         return getPrefService().getString(Pref.CONTEXTUAL_SEARCH_ENABLED).isEmpty();
1825     }
1826 
1827     /**
1828      * @param enabled Whether Contextual Search should be enabled.
1829      */
1830     public static void setContextualSearchState(boolean enabled) {
1831         getPrefService().setString(Pref.CONTEXTUAL_SEARCH_ENABLED,
1832                 enabled ? CONTEXTUAL_SEARCH_ENABLED : CONTEXTUAL_SEARCH_DISABLED);
1833     }
1834 
1835     // Private helper functions
1836 
1837     /** @return The language of the base page being viewed by the user. */
1838     private String getBasePageLanguage() {
1839         return mContext.getDetectedLanguage();
1840     }
1841 
1842     private static PrefService getPrefService() {
1843         return UserPrefs.get(Profile.getLastUsedRegularProfile());
1844     }
1845 
1846     // ============================================================================================
1847     // Test helpers
1848     // ============================================================================================
1849 
1850     /**
1851      * Sets the {@link ContextualSearchNetworkCommunicator} to use for server requests.
1852      * @param networkCommunicator The communicator for all future requests.
1853      */
1854     @VisibleForTesting
1855     void setNetworkCommunicator(ContextualSearchNetworkCommunicator networkCommunicator) {
1856         mNetworkCommunicator = networkCommunicator;
1857         mPolicy.setNetworkCommunicator(mNetworkCommunicator);
1858     }
1859 
1860     /** @return The ContextualSearchPolicy currently being used. */
1861     @VisibleForTesting
1862     ContextualSearchPolicy getContextualSearchPolicy() {
1863         return mPolicy;
1864     }
1865 
1866     /** @param policy The {@link ContextualSearchPolicy} for testing. */
1867     @VisibleForTesting
1868     void setContextualSearchPolicy(ContextualSearchPolicy policy) {
1869         mPolicy = policy;
1870     }
1871 
1872     /** @return The {@link ContextualSearchPanel}, for testing purposes only. */
1873     @VisibleForTesting
1874     ContextualSearchPanel getContextualSearchPanel() {
1875         return mSearchPanel;
1876     }
1877 
1878     /** @return The selection controller, for testing purposes. */
1879     @VisibleForTesting
1880     ContextualSearchSelectionController getSelectionController() {
1881         return mSelectionController;
1882     }
1883 
1884     /** @param controller The {@link ContextualSearchSelectionController}, for testing purposes. */
1885     @VisibleForTesting
1886     void setSelectionController(ContextualSearchSelectionController controller) {
1887         mSelectionController = controller;
1888     }
1889 
1890     /** @return The current search request, or {@code null} if there is none, for testing. */
1891     @VisibleForTesting
1892     ContextualSearchRequest getRequest() {
1893         return mSearchRequest;
1894     }
1895 
1896     @VisibleForTesting
1897     ContextualSearchTabPromotionDelegate getTabPromotionDelegate() {
1898         return mTabPromotionDelegate;
1899     }
1900 
1901     @VisibleForTesting
1902     void setContextualSearchInternalStateController(
1903             ContextualSearchInternalStateController controller) {
1904         mInternalStateController = controller;
1905     }
1906 
1907     @VisibleForTesting
1908     protected ContextualSearchInternalStateController getContextualSearchInternalStateController() {
1909         return mInternalStateController;
1910     }
1911 
1912     @VisibleForTesting
1913     ContextualSearchInteractionRecorder getRankerLogger() {
1914         return mInteractionRecorder;
1915     }
1916 
1917     @VisibleForTesting
1918     ContextualSearchContext getContext() {
1919         return mContext;
1920     }
1921 
1922     @VisibleForTesting
1923     public boolean isSuppressed() {
1924         return mIsBottomSheetVisible || mIsAccessibilityModeEnabled;
1925     }
1926 
1927     @NativeMethods
1928     interface Natives {
1929         long init(ContextualSearchManager caller);
1930         void destroy(long nativeContextualSearchManager, ContextualSearchManager caller);
1931         void startSearchTermResolutionRequest(long nativeContextualSearchManager,
1932                 ContextualSearchManager caller, ContextualSearchContext contextualSearchContext,
1933                 WebContents baseWebContents);
1934         void gatherSurroundingText(long nativeContextualSearchManager,
1935                 ContextualSearchManager caller, ContextualSearchContext contextualSearchContext,
1936                 WebContents baseWebContents);
1937         void whitelistContextualSearchJsApiUrl(
1938                 long nativeContextualSearchManager, ContextualSearchManager caller, String url);
1939         void enableContextualSearchJsApiForWebContents(long nativeContextualSearchManager,
1940                 ContextualSearchManager caller, WebContents overlayWebContents);
1941     }
1942 }
1943