Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package org.chromium.chrome.browser.contextualsearch; | 5 package org.chromium.chrome.browser.contextualsearch; |
| 6 | 6 |
| 7 import android.os.Handler; | 7 import android.os.Handler; |
| 8 | 8 |
| 9 import org.chromium.base.VisibleForTesting; | 9 import org.chromium.base.VisibleForTesting; |
| 10 import org.chromium.chrome.browser.ChromeActivity; | 10 import org.chromium.chrome.browser.ChromeActivity; |
| 11 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel; | 11 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel; |
| 12 import org.chromium.chrome.browser.contextualsearch.ContextualSearchBlacklist.Bl acklistReason; | 12 import org.chromium.chrome.browser.contextualsearch.ContextualSearchBlacklist.Bl acklistReason; |
| 13 import org.chromium.chrome.browser.contextualsearch.action.ResolvedSearchAction; | |
| 14 import org.chromium.chrome.browser.contextualsearch.action.SearchAction; | |
| 15 import org.chromium.chrome.browser.contextualsearch.action.SearchActionListener; | |
| 16 import org.chromium.chrome.browser.contextualsearch.gesture.SearchGestureHost; | |
| 13 import org.chromium.chrome.browser.preferences.ChromePreferenceManager; | 17 import org.chromium.chrome.browser.preferences.ChromePreferenceManager; |
| 14 import org.chromium.chrome.browser.tab.Tab; | 18 import org.chromium.chrome.browser.tab.Tab; |
| 15 import org.chromium.content.browser.ContentViewCore; | 19 import org.chromium.content.browser.ContentViewCore; |
| 16 import org.chromium.content_public.browser.GestureStateListener; | 20 import org.chromium.content_public.browser.GestureStateListener; |
| 21 import org.chromium.content_public.browser.WebContents; | |
| 17 import org.chromium.ui.touch_selection.SelectionEventType; | 22 import org.chromium.ui.touch_selection.SelectionEventType; |
| 18 | 23 |
| 19 import java.util.regex.Matcher; | 24 import java.util.regex.Matcher; |
| 20 import java.util.regex.Pattern; | 25 import java.util.regex.Pattern; |
| 21 | 26 |
| 22 /** | 27 /** |
| 23 * Controls selection gesture interaction for Contextual Search. | 28 * Controls selection gesture interaction for Contextual Search. |
| 24 */ | 29 */ |
| 25 public class ContextualSearchSelectionController { | 30 public class ContextualSearchSelectionController implements SearchGestureHost { |
| 26 | |
| 27 /** | 31 /** |
| 28 * The type of selection made by the user. | 32 * The type of selection made by the user. |
| 29 */ | 33 */ |
| 30 public enum SelectionType { | 34 public enum SelectionType { |
| 31 UNDETERMINED, | 35 UNDETERMINED, |
| 32 TAP, | 36 TAP, |
| 33 LONG_PRESS | 37 LONG_PRESS |
| 34 } | 38 } |
| 35 | 39 |
| 36 // The number of milliseconds to wait for a selection change after a tap bef ore considering | 40 // The number of milliseconds to wait for a selection change after a tap bef ore considering |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 72 private boolean mShouldHandleSelectionModification; | 76 private boolean mShouldHandleSelectionModification; |
| 73 private boolean mDidExpandSelection; | 77 private boolean mDidExpandSelection; |
| 74 | 78 |
| 75 // Position of the selection. | 79 // Position of the selection. |
| 76 private float mX; | 80 private float mX; |
| 77 private float mY; | 81 private float mY; |
| 78 | 82 |
| 79 // The time of the most last scroll activity, or 0 if none. | 83 // The time of the most last scroll activity, or 0 if none. |
| 80 private long mLastScrollTimeNs; | 84 private long mLastScrollTimeNs; |
| 81 | 85 |
| 86 // When the last tap gesture happened. | |
| 87 private long mTapTimeNanoseconds; | |
| 88 | |
| 82 // Tracks whether a Context Menu has just been shown and the UX has been dis missed. | 89 // Tracks whether a Context Menu has just been shown and the UX has been dis missed. |
| 83 // The selection may be unreliable until the next reset. See crbug.com/6284 36. | 90 // The selection may be unreliable until the next reset. See crbug.com/6284 36. |
| 84 private boolean mIsContextMenuShown; | 91 private boolean mIsContextMenuShown; |
| 85 | 92 |
| 93 // Set to true when we have identified that a pending tap has not been handl ed by Blink. | |
| 94 private boolean mHasIdentifiedUnhandledTap; | |
| 95 | |
| 96 // The current Search action. | |
| 97 private SearchAction mSearchAction; | |
| 98 | |
| 86 private class ContextualSearchGestureStateListener extends GestureStateListe ner { | 99 private class ContextualSearchGestureStateListener extends GestureStateListe ner { |
| 87 @Override | 100 @Override |
| 88 public void onScrollStarted(int scrollOffsetY, int scrollExtentY) { | 101 public void onScrollStarted(int scrollOffsetY, int scrollExtentY) { |
| 89 mHandler.handleScroll(); | 102 mHandler.handleScroll(); |
| 90 } | 103 } |
| 91 | 104 |
| 92 @Override | 105 @Override |
| 93 public void onScrollEnded(int scrollOffsetY, int scrollExtentY) { | 106 public void onScrollEnded(int scrollOffsetY, int scrollExtentY) { |
| 94 mLastScrollTimeNs = System.nanoTime(); | 107 mLastScrollTimeNs = System.nanoTime(); |
| 95 } | 108 } |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 106 // notification in this case. | 119 // notification in this case. |
| 107 // See crbug.com/444114. | 120 // See crbug.com/444114. |
| 108 @Override | 121 @Override |
| 109 public void onSingleTap(boolean consumed, int x, int y) { | 122 public void onSingleTap(boolean consumed, int x, int y) { |
| 110 // We may be notified that a tap has happened even when the system c onsumed the event. | 123 // We may be notified that a tap has happened even when the system c onsumed the event. |
| 111 // This is being used to support tapping on an existing selection to show the selection | 124 // This is being used to support tapping on an existing selection to show the selection |
| 112 // handles. We should process this tap unless we have already shown the selection | 125 // handles. We should process this tap unless we have already shown the selection |
| 113 // handles (have a long-press selection) and the tap was consumed. | 126 // handles (have a long-press selection) and the tap was consumed. |
| 114 if (!(consumed && mSelectionType == SelectionType.LONG_PRESS)) { | 127 if (!(consumed && mSelectionType == SelectionType.LONG_PRESS)) { |
| 115 scheduleInvalidTapNotification(); | 128 scheduleInvalidTapNotification(); |
| 129 | |
| 130 if (mHasIdentifiedUnhandledTap) createSearchAction(); | |
| 116 } | 131 } |
| 117 } | 132 } |
| 118 } | 133 } |
| 119 | 134 |
| 120 /** | 135 /** |
| 121 * Constructs a new Selection controller for the given activity. Callbacks will be issued | 136 * Constructs a new Selection controller for the given activity. Callbacks will be issued |
| 122 * through the given selection handler. | 137 * through the given selection handler. |
| 123 * @param activity The {@link ChromeActivity} to control. | 138 * @param activity The {@link ChromeActivity} to control. |
| 124 * @param handler The handler for callbacks. | 139 * @param handler The handler for callbacks. |
| 125 */ | 140 */ |
| (...skipping 205 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 331 } | 346 } |
| 332 | 347 |
| 333 /** | 348 /** |
| 334 * Resets all internal state of this class, including the tap state. | 349 * Resets all internal state of this class, including the tap state. |
| 335 */ | 350 */ |
| 336 private void resetAllStates() { | 351 private void resetAllStates() { |
| 337 resetSelectionStates(); | 352 resetSelectionStates(); |
| 338 mLastTapState = null; | 353 mLastTapState = null; |
| 339 mLastScrollTimeNs = 0; | 354 mLastScrollTimeNs = 0; |
| 340 mIsContextMenuShown = false; | 355 mIsContextMenuShown = false; |
| 356 mHasIdentifiedUnhandledTap = false; | |
| 357 mTapTimeNanoseconds = 0; | |
| 341 } | 358 } |
| 342 | 359 |
| 343 /** | 360 /** |
| 344 * Resets all of the internal state of this class that handles the selection . | 361 * Resets all of the internal state of this class that handles the selection . |
| 345 */ | 362 */ |
| 346 private void resetSelectionStates() { | 363 private void resetSelectionStates() { |
| 347 mSelectionType = SelectionType.UNDETERMINED; | 364 mSelectionType = SelectionType.UNDETERMINED; |
| 348 mSelectedText = null; | 365 mSelectedText = null; |
| 349 | 366 |
| 350 mWasTapGestureDetected = false; | 367 mWasTapGestureDetected = false; |
| 351 } | 368 } |
| 352 | 369 |
| 353 /** | 370 /** |
| 354 * Should be called when a new Tab is selected. | 371 * Should be called when a new Tab is selected. |
| 355 * Resets all of the internal state of this class. | 372 * Resets all of the internal state of this class. |
| 356 */ | 373 */ |
| 357 void onTabSelected() { | 374 void onTabSelected() { |
| 358 resetAllStates(); | 375 resetAllStates(); |
| 359 } | 376 } |
| 360 | 377 |
| 361 /** | 378 /** |
| 362 * Handles an unhandled tap gesture. | 379 * Handles an unhandled tap gesture. |
| 380 * @param x The x coordinate. | |
| 381 * @param y The y coordinate. | |
| 363 */ | 382 */ |
| 364 void handleShowUnhandledTapUIIfNeeded(int x, int y) { | 383 void handleShowUnhandledTapUIIfNeeded(int x, int y) { |
| 365 mWasTapGestureDetected = false; | 384 mWasTapGestureDetected = false; |
| 366 // TODO(donnd): shouldn't we check == TAP here instead of LONG_PRESS? | 385 // TODO(donnd): shouldn't we check == TAP here instead of LONG_PRESS? |
| 367 // TODO(donnd): refactor to avoid needing a new handler API method as su ggested by Pedro. | 386 // TODO(donnd): refactor to avoid needing a new handler API method as su ggested by Pedro. |
| 368 if (mSelectionType != SelectionType.LONG_PRESS) { | 387 if (mSelectionType != SelectionType.LONG_PRESS) { |
| 369 mWasTapGestureDetected = true; | 388 mWasTapGestureDetected = true; |
| 370 long tapTimeNanoseconds = System.nanoTime(); | 389 mTapTimeNanoseconds = System.nanoTime(); |
| 371 // TODO(donnd): add a policy method to get adjusted tap count. | 390 |
| 372 ChromePreferenceManager prefs = ChromePreferenceManager.getInstance( mActivity); | 391 // NOTE(donnd): We first acknowledge that a unhandled tap was identi fied, but |
| 373 int adjustedTapsSinceOpen = prefs.getContextualSearchTapCount() | 392 // we don't do anything now. Instead we'll wait for the onSingleTap( ) event to fire, |
| 374 - prefs.getContextualSearchTapQuickAnswerCount(); | 393 // and only do something when an unhandled tap was identified. onSin gleTap() will |
| 375 TapSuppressionHeuristics tapHeuristics = | 394 // always get fired, as opposed to showUnhandledTapUIIfNeeded(). |
| 376 new TapSuppressionHeuristics(this, mLastTapState, x, y, adju stedTapsSinceOpen); | 395 mHasIdentifiedUnhandledTap = true; |
| 377 // TODO(donnd): Move to be called when the panel closes to work with states that change. | 396 |
| 378 tapHeuristics.logConditionState(); | |
| 379 // Tell the manager what it needs in order to log metrics on whether the tap would have | |
| 380 // been suppressed if each of the heuristics were satisfied. | |
| 381 mHandler.handleMetricsForWouldSuppressTap(tapHeuristics); | |
| 382 mX = x; | 397 mX = x; |
| 383 mY = y; | 398 mY = y; |
| 384 boolean shouldSuppressTap = tapHeuristics.shouldSuppressTap(); | |
| 385 if (shouldSuppressTap) { | |
| 386 mHandler.handleSuppressedTap(); | |
| 387 } else { | |
| 388 // TODO(donnd): Find a better way to determine that a navigation will be triggered | |
| 389 // by the tap, or merge with other time-consuming actions like g athering surrounding | |
| 390 // text or detecting page mutations. | |
| 391 new Handler().postDelayed(new Runnable() { | |
| 392 @Override | |
| 393 public void run() { | |
| 394 mHandler.handleValidTap(); | |
| 395 } | |
| 396 }, TAP_NAVIGATION_DETECTION_DELAY); | |
| 397 } | |
| 398 // Remember the tap state for subsequent tap evaluation. | |
| 399 mLastTapState = | |
| 400 new ContextualSearchTapState(x, y, tapTimeNanoseconds, shoul dSuppressTap); | |
| 401 } else { | 399 } else { |
| 402 // Long press; reset last tap state. | 400 // Long press; reset last tap state. |
| 403 mLastTapState = null; | 401 mLastTapState = null; |
| 404 mHandler.handleInvalidTap(); | 402 mHandler.handleInvalidTap(); |
| 405 } | 403 } |
| 406 } | 404 } |
| 407 | 405 |
| 408 /** | 406 /** |
| 407 * Processes a {@link SearchAction}. | |
| 408 * This should be called when the associated {@code SearchAction} has built its context (by | |
| 409 * gathering surrounding text if needed, etc). | |
|
Theresa
2016/09/01 16:49:36
nit: ..., etc) but before a selection has been est
Donn Denman
2016/09/02 16:40:43
Done. Now reads: "This should be called when the
| |
| 410 * @param searchAction The {@link SearchAction} for this Tap gesture. | |
| 411 * @param x The x coordinate. | |
| 412 * @param y The y coordinate. | |
| 413 */ | |
| 414 void processSearchAction(SearchAction searchAction, int x, int y) { | |
| 415 // TODO(donnd): consider using the supplied searchAction, or remove if u sed from native! | |
| 416 // TODO(donnd): add a policy method to get adjusted tap count. | |
| 417 ChromePreferenceManager prefs = ChromePreferenceManager.getInstance(mAct ivity); | |
| 418 int adjustedTapsSinceOpen = prefs.getContextualSearchTapCount() | |
| 419 - prefs.getContextualSearchTapQuickAnswerCount(); | |
| 420 TapSuppressionHeuristics tapHeuristics = | |
| 421 new TapSuppressionHeuristics(this, mLastTapState, x, y, adjusted TapsSinceOpen); | |
| 422 // TODO(donnd): Move to be called when the panel closes to work with sta tes that change. | |
| 423 tapHeuristics.logConditionState(); | |
| 424 // Tell the manager what it needs in order to log metrics on whether the tap would have | |
| 425 // been suppressed if each of the heuristics were satisfied. | |
| 426 mHandler.handleMetricsForWouldSuppressTap(tapHeuristics); | |
| 427 | |
| 428 boolean shouldSuppressTap = tapHeuristics.shouldSuppressTap(); | |
| 429 if (shouldSuppressTap) { | |
| 430 mHandler.handleSuppressedTap(); | |
| 431 } else { | |
| 432 // TODO(donnd): Find a better way to determine that a navigation wil l be triggered | |
| 433 // by the tap, or merge with other time-consuming actions like gathe ring surrounding | |
| 434 // text or detecting page mutations. | |
| 435 new Handler().postDelayed(new Runnable() { | |
| 436 @Override | |
| 437 public void run() { | |
| 438 mHandler.handleValidTap(); | |
| 439 } | |
| 440 }, TAP_NAVIGATION_DETECTION_DELAY); | |
| 441 } | |
| 442 if (mTapTimeNanoseconds == 0) throw new RuntimeException("Tap time not s et!"); | |
| 443 // Remember the tap state for subsequent tap evaluation. | |
| 444 mLastTapState = new ContextualSearchTapState(x, y, mTapTimeNanoseconds, shouldSuppressTap); | |
| 445 } | |
| 446 | |
| 447 /** | |
| 448 * Creates the current {@link SearchAction}. | |
| 449 */ | |
| 450 void createSearchAction() { | |
|
pedro (no code reviews)
2016/08/31 22:38:22
The SearchAction should also be destroyed when a l
Donn Denman
2016/09/02 16:40:43
Done: Added a destroySearchAction to handleSelecti
| |
| 451 if (mSearchAction != null) destroySearchAction(); | |
|
Theresa
2016/09/01 16:49:36
nit: destroySearchAction() checks if mSearchAction
Donn Denman
2016/09/02 16:40:43
Done.
| |
| 452 mSearchAction = new ResolvedSearchAction(new SearchActionListener() { | |
| 453 | |
| 454 @Override | |
| 455 protected void onContextReady(SearchAction action) { | |
| 456 processSearchAction(action, (int) mX, (int) mY); | |
| 457 } | |
| 458 }, this); | |
| 459 | |
| 460 mSearchAction.extractContext(); | |
| 461 } | |
| 462 | |
| 463 /** | |
| 464 * Destroys the current {@link SearchAction}. | |
| 465 */ | |
| 466 private void destroySearchAction() { | |
| 467 if (mSearchAction == null) return; | |
| 468 | |
| 469 mSearchAction.destroyAction(); | |
| 470 mSearchAction = null; | |
| 471 } | |
| 472 | |
| 473 // ========================================================================= =================== | |
| 474 // SearchGestureHost | |
| 475 // ========================================================================= =================== | |
| 476 | |
| 477 @Override | |
| 478 public WebContents getTabWebContents() { | |
| 479 Tab currentTab = mActivity.getActivityTab(); | |
| 480 return currentTab != null ? currentTab.getWebContents() : null; | |
| 481 } | |
| 482 | |
| 483 @Override | |
| 484 public void dismissGesture() { | |
| 485 destroySearchAction(); | |
| 486 } | |
| 487 | |
| 488 // ========================================================================= =================== | |
| 489 // Utilities | |
| 490 // ========================================================================= =================== | |
| 491 | |
| 492 /** | |
| 409 * @return The Base Page's {@link ContentViewCore}, or {@code null} if there is no current tab. | 493 * @return The Base Page's {@link ContentViewCore}, or {@code null} if there is no current tab. |
| 410 */ | 494 */ |
| 411 ContentViewCore getBaseContentView() { | 495 ContentViewCore getBaseContentView() { |
| 496 // TODO(donnd): switch to using WebContents over ContentViewCore. | |
| 412 Tab currentTab = mActivity.getActivityTab(); | 497 Tab currentTab = mActivity.getActivityTab(); |
| 413 return currentTab != null ? currentTab.getContentViewCore() : null; | 498 return currentTab != null ? currentTab.getContentViewCore() : null; |
| 414 } | 499 } |
| 415 | 500 |
| 416 /** | 501 /** |
| 417 * Expands the current selection by the specified amounts. | 502 * Expands the current selection by the specified amounts. |
| 418 * @param selectionStartAdjust The start offset adjustment of the selection to use to highlight | 503 * @param selectionStartAdjust The start offset adjustment of the selection to use to highlight |
| 419 * the search term. | 504 * the search term. |
| 420 * @param selectionEndAdjust The end offset adjustment of the selection to u se to highlight | 505 * @param selectionEndAdjust The end offset adjustment of the selection to u se to highlight |
| 421 * the search term. | 506 * the search term. |
| (...skipping 175 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 597 // Starts are inclusive and ends are non-inclusive for both GSAContext & matcher. | 682 // Starts are inclusive and ends are non-inclusive for both GSAContext & matcher. |
| 598 while (matcher.find()) { | 683 while (matcher.find()) { |
| 599 if (startOffset >= matcher.start() && endOffset <= matcher.end()) { | 684 if (startOffset >= matcher.start() && endOffset <= matcher.end()) { |
| 600 return true; | 685 return true; |
| 601 } | 686 } |
| 602 } | 687 } |
| 603 | 688 |
| 604 return false; | 689 return false; |
| 605 } | 690 } |
| 606 } | 691 } |
| OLD | NEW |