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