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 193 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
319 } | 334 } |
320 | 335 |
321 /** | 336 /** |
322 * Re-enables selection modification handling and invokes | 337 * Re-enables selection modification handling and invokes |
323 * ContextualSearchSelectionHandler.handleSelection(). | 338 * ContextualSearchSelectionHandler.handleSelection(). |
324 * @param selection The text that was selected. | 339 * @param selection The text that was selected. |
325 * @param type The type of selection made by the user. | 340 * @param type The type of selection made by the user. |
326 */ | 341 */ |
327 private void handleSelection(String selection, SelectionType type) { | 342 private void handleSelection(String selection, SelectionType type) { |
328 mShouldHandleSelectionModification = true; | 343 mShouldHandleSelectionModification = true; |
| 344 destroySearchAction(); |
329 boolean isValidSelection = validateSelectionSuppression(selection); | 345 boolean isValidSelection = validateSelectionSuppression(selection); |
330 mHandler.handleSelection(selection, isValidSelection, type, mX, mY); | 346 mHandler.handleSelection(selection, isValidSelection, type, mX, mY); |
331 } | 347 } |
332 | 348 |
333 /** | 349 /** |
334 * Resets all internal state of this class, including the tap state. | 350 * Resets all internal state of this class, including the tap state. |
335 */ | 351 */ |
336 private void resetAllStates() { | 352 private void resetAllStates() { |
337 resetSelectionStates(); | 353 resetSelectionStates(); |
338 mLastTapState = null; | 354 mLastTapState = null; |
339 mLastScrollTimeNs = 0; | 355 mLastScrollTimeNs = 0; |
340 mIsContextMenuShown = false; | 356 mIsContextMenuShown = false; |
| 357 mHasIdentifiedUnhandledTap = false; |
| 358 mTapTimeNanoseconds = 0; |
341 } | 359 } |
342 | 360 |
343 /** | 361 /** |
344 * Resets all of the internal state of this class that handles the selection
. | 362 * Resets all of the internal state of this class that handles the selection
. |
345 */ | 363 */ |
346 private void resetSelectionStates() { | 364 private void resetSelectionStates() { |
347 mSelectionType = SelectionType.UNDETERMINED; | 365 mSelectionType = SelectionType.UNDETERMINED; |
348 mSelectedText = null; | 366 mSelectedText = null; |
349 | 367 |
350 mWasTapGestureDetected = false; | 368 mWasTapGestureDetected = false; |
351 } | 369 } |
352 | 370 |
353 /** | 371 /** |
354 * Should be called when a new Tab is selected. | 372 * Should be called when a new Tab is selected. |
355 * Resets all of the internal state of this class. | 373 * Resets all of the internal state of this class. |
356 */ | 374 */ |
357 void onTabSelected() { | 375 void onTabSelected() { |
358 resetAllStates(); | 376 resetAllStates(); |
359 } | 377 } |
360 | 378 |
361 /** | 379 /** |
362 * Handles an unhandled tap gesture. | 380 * Handles an unhandled tap gesture. |
| 381 * @param x The x coordinate. |
| 382 * @param y The y coordinate. |
363 */ | 383 */ |
364 void handleShowUnhandledTapUIIfNeeded(int x, int y) { | 384 void handleShowUnhandledTapUIIfNeeded(int x, int y) { |
365 mWasTapGestureDetected = false; | 385 mWasTapGestureDetected = false; |
366 // TODO(donnd): shouldn't we check == TAP here instead of LONG_PRESS? | 386 // 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. | 387 // TODO(donnd): refactor to avoid needing a new handler API method as su
ggested by Pedro. |
368 if (mSelectionType != SelectionType.LONG_PRESS) { | 388 if (mSelectionType != SelectionType.LONG_PRESS) { |
369 mWasTapGestureDetected = true; | 389 mWasTapGestureDetected = true; |
370 long tapTimeNanoseconds = System.nanoTime(); | 390 mTapTimeNanoseconds = System.nanoTime(); |
371 // TODO(donnd): add a policy method to get adjusted tap count. | 391 |
372 ChromePreferenceManager prefs = ChromePreferenceManager.getInstance(
mActivity); | 392 // NOTE(donnd): We first acknowledge that a unhandled tap was identi
fied, but |
373 int adjustedTapsSinceOpen = prefs.getContextualSearchTapCount() | 393 // we don't do anything now. Instead we'll wait for the onSingleTap(
) event to fire, |
374 - prefs.getContextualSearchTapQuickAnswerCount(); | 394 // and only do something when an unhandled tap was identified. onSin
gleTap() will |
375 TapSuppressionHeuristics tapHeuristics = | 395 // always get fired, as opposed to showUnhandledTapUIIfNeeded(). |
376 new TapSuppressionHeuristics(this, mLastTapState, x, y, adju
stedTapsSinceOpen); | 396 mHasIdentifiedUnhandledTap = true; |
377 // TODO(donnd): Move to be called when the panel closes to work with
states that change. | 397 |
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; | 398 mX = x; |
383 mY = y; | 399 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 { | 400 } else { |
402 // Long press; reset last tap state. | 401 // Long press; reset last tap state. |
403 mLastTapState = null; | 402 mLastTapState = null; |
404 mHandler.handleInvalidTap(); | 403 mHandler.handleInvalidTap(); |
405 } | 404 } |
406 } | 405 } |
407 | 406 |
408 /** | 407 /** |
| 408 * Processes a {@link SearchAction}. |
| 409 * This should be called when the associated {@code SearchAction} has built
its context (by |
| 410 * gathering surrounding text if needed, etc) but before showing any UX. |
| 411 * @param searchAction The {@link SearchAction} for this Tap gesture. |
| 412 * @param x The x coordinate. |
| 413 * @param y The y coordinate. |
| 414 */ |
| 415 void processSearchAction(SearchAction searchAction, int x, int y) { |
| 416 // TODO(donnd): consider using the supplied searchAction, or remove if u
sed from native! |
| 417 // TODO(donnd): add a policy method to get adjusted tap count. |
| 418 ChromePreferenceManager prefs = ChromePreferenceManager.getInstance(mAct
ivity); |
| 419 int adjustedTapsSinceOpen = prefs.getContextualSearchTapCount() |
| 420 - prefs.getContextualSearchTapQuickAnswerCount(); |
| 421 TapSuppressionHeuristics tapHeuristics = |
| 422 new TapSuppressionHeuristics(this, mLastTapState, x, y, adjusted
TapsSinceOpen); |
| 423 // TODO(donnd): Move to be called when the panel closes to work with sta
tes that change. |
| 424 tapHeuristics.logConditionState(); |
| 425 // Tell the manager what it needs in order to log metrics on whether the
tap would have |
| 426 // been suppressed if each of the heuristics were satisfied. |
| 427 mHandler.handleMetricsForWouldSuppressTap(tapHeuristics); |
| 428 |
| 429 boolean shouldSuppressTap = tapHeuristics.shouldSuppressTap(); |
| 430 if (shouldSuppressTap) { |
| 431 mHandler.handleSuppressedTap(); |
| 432 } else { |
| 433 // TODO(donnd): Find a better way to determine that a navigation wil
l be triggered |
| 434 // by the tap, or merge with other time-consuming actions like gathe
ring surrounding |
| 435 // text or detecting page mutations. |
| 436 new Handler().postDelayed(new Runnable() { |
| 437 @Override |
| 438 public void run() { |
| 439 mHandler.handleValidTap(); |
| 440 } |
| 441 }, TAP_NAVIGATION_DETECTION_DELAY); |
| 442 } |
| 443 if (mTapTimeNanoseconds == 0) throw new RuntimeException("Tap time not s
et!"); |
| 444 // Remember the tap state for subsequent tap evaluation. |
| 445 mLastTapState = new ContextualSearchTapState(x, y, mTapTimeNanoseconds,
shouldSuppressTap); |
| 446 } |
| 447 |
| 448 /** |
| 449 * Creates the current {@link SearchAction}. |
| 450 */ |
| 451 void createSearchAction() { |
| 452 destroySearchAction(); |
| 453 mSearchAction = new ResolvedSearchAction(new SearchActionListener() { |
| 454 |
| 455 @Override |
| 456 protected void onContextReady(SearchAction action) { |
| 457 processSearchAction(action, (int) mX, (int) mY); |
| 458 } |
| 459 }, this); |
| 460 |
| 461 mSearchAction.extractContext(); |
| 462 } |
| 463 |
| 464 /** |
| 465 * Destroys the current {@link SearchAction}. |
| 466 */ |
| 467 private void destroySearchAction() { |
| 468 if (mSearchAction == null) return; |
| 469 |
| 470 mSearchAction.destroyAction(); |
| 471 mSearchAction = null; |
| 472 } |
| 473 |
| 474 // =========================================================================
=================== |
| 475 // SearchGestureHost |
| 476 // =========================================================================
=================== |
| 477 |
| 478 @Override |
| 479 public WebContents getTabWebContents() { |
| 480 Tab currentTab = mActivity.getActivityTab(); |
| 481 return currentTab != null ? currentTab.getWebContents() : null; |
| 482 } |
| 483 |
| 484 @Override |
| 485 public void dismissGesture() { |
| 486 destroySearchAction(); |
| 487 } |
| 488 |
| 489 // =========================================================================
=================== |
| 490 // Utilities |
| 491 // =========================================================================
=================== |
| 492 |
| 493 /** |
409 * @return The Base Page's {@link ContentViewCore}, or {@code null} if there
is no current tab. | 494 * @return The Base Page's {@link ContentViewCore}, or {@code null} if there
is no current tab. |
410 */ | 495 */ |
411 ContentViewCore getBaseContentView() { | 496 ContentViewCore getBaseContentView() { |
| 497 // TODO(donnd): switch to using WebContents over ContentViewCore. |
412 Tab currentTab = mActivity.getActivityTab(); | 498 Tab currentTab = mActivity.getActivityTab(); |
413 return currentTab != null ? currentTab.getContentViewCore() : null; | 499 return currentTab != null ? currentTab.getContentViewCore() : null; |
414 } | 500 } |
415 | 501 |
416 /** | 502 /** |
417 * Expands the current selection by the specified amounts. | 503 * Expands the current selection by the specified amounts. |
418 * @param selectionStartAdjust The start offset adjustment of the selection
to use to highlight | 504 * @param selectionStartAdjust The start offset adjustment of the selection
to use to highlight |
419 * the search term. | 505 * the search term. |
420 * @param selectionEndAdjust The end offset adjustment of the selection to u
se to highlight | 506 * @param selectionEndAdjust The end offset adjustment of the selection to u
se to highlight |
421 * the search term. | 507 * 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. | 683 // Starts are inclusive and ends are non-inclusive for both GSAContext &
matcher. |
598 while (matcher.find()) { | 684 while (matcher.find()) { |
599 if (startOffset >= matcher.start() && endOffset <= matcher.end()) { | 685 if (startOffset >= matcher.start() && endOffset <= matcher.end()) { |
600 return true; | 686 return true; |
601 } | 687 } |
602 } | 688 } |
603 | 689 |
604 return false; | 690 return false; |
605 } | 691 } |
606 } | 692 } |
OLD | NEW |