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; | |
17 import org.chromium.chrome.browser.preferences.ChromePreferenceManager; | 13 import org.chromium.chrome.browser.preferences.ChromePreferenceManager; |
18 import org.chromium.chrome.browser.tab.Tab; | 14 import org.chromium.chrome.browser.tab.Tab; |
19 import org.chromium.content.browser.ContentViewCore; | 15 import org.chromium.content.browser.ContentViewCore; |
20 import org.chromium.content_public.browser.GestureStateListener; | 16 import org.chromium.content_public.browser.GestureStateListener; |
21 import org.chromium.content_public.browser.WebContents; | |
22 import org.chromium.ui.touch_selection.SelectionEventType; | 17 import org.chromium.ui.touch_selection.SelectionEventType; |
23 | 18 |
24 import java.util.regex.Matcher; | 19 import java.util.regex.Matcher; |
25 import java.util.regex.Pattern; | 20 import java.util.regex.Pattern; |
26 | 21 |
27 /** | 22 /** |
28 * Controls selection gesture interaction for Contextual Search. | 23 * Controls selection gesture interaction for Contextual Search. |
29 */ | 24 */ |
30 public class ContextualSearchSelectionController implements SearchGestureHost { | 25 public class ContextualSearchSelectionController { |
| 26 |
31 /** | 27 /** |
32 * The type of selection made by the user. | 28 * The type of selection made by the user. |
33 */ | 29 */ |
34 public enum SelectionType { | 30 public enum SelectionType { |
35 UNDETERMINED, | 31 UNDETERMINED, |
36 TAP, | 32 TAP, |
37 LONG_PRESS | 33 LONG_PRESS |
38 } | 34 } |
39 | 35 |
40 // The number of milliseconds to wait for a selection change after a tap bef
ore considering | 36 // 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... |
76 private boolean mShouldHandleSelectionModification; | 72 private boolean mShouldHandleSelectionModification; |
77 private boolean mDidExpandSelection; | 73 private boolean mDidExpandSelection; |
78 | 74 |
79 // Position of the selection. | 75 // Position of the selection. |
80 private float mX; | 76 private float mX; |
81 private float mY; | 77 private float mY; |
82 | 78 |
83 // The time of the most last scroll activity, or 0 if none. | 79 // The time of the most last scroll activity, or 0 if none. |
84 private long mLastScrollTimeNs; | 80 private long mLastScrollTimeNs; |
85 | 81 |
86 // When the last tap gesture happened. | |
87 private long mTapTimeNanoseconds; | |
88 | |
89 // Tracks whether a Context Menu has just been shown and the UX has been dis
missed. | 82 // Tracks whether a Context Menu has just been shown and the UX has been dis
missed. |
90 // The selection may be unreliable until the next reset. See crbug.com/6284
36. | 83 // The selection may be unreliable until the next reset. See crbug.com/6284
36. |
91 private boolean mIsContextMenuShown; | 84 private boolean mIsContextMenuShown; |
92 | 85 |
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 | |
99 private class ContextualSearchGestureStateListener extends GestureStateListe
ner { | 86 private class ContextualSearchGestureStateListener extends GestureStateListe
ner { |
100 @Override | 87 @Override |
101 public void onScrollStarted(int scrollOffsetY, int scrollExtentY) { | 88 public void onScrollStarted(int scrollOffsetY, int scrollExtentY) { |
102 mHandler.handleScroll(); | 89 mHandler.handleScroll(); |
103 } | 90 } |
104 | 91 |
105 @Override | 92 @Override |
106 public void onScrollEnded(int scrollOffsetY, int scrollExtentY) { | 93 public void onScrollEnded(int scrollOffsetY, int scrollExtentY) { |
107 mLastScrollTimeNs = System.nanoTime(); | 94 mLastScrollTimeNs = System.nanoTime(); |
108 } | 95 } |
(...skipping 10 matching lines...) Expand all Loading... |
119 // notification in this case. | 106 // notification in this case. |
120 // See crbug.com/444114. | 107 // See crbug.com/444114. |
121 @Override | 108 @Override |
122 public void onSingleTap(boolean consumed, int x, int y) { | 109 public void onSingleTap(boolean consumed, int x, int y) { |
123 // We may be notified that a tap has happened even when the system c
onsumed the event. | 110 // We may be notified that a tap has happened even when the system c
onsumed the event. |
124 // This is being used to support tapping on an existing selection to
show the selection | 111 // This is being used to support tapping on an existing selection to
show the selection |
125 // handles. We should process this tap unless we have already shown
the selection | 112 // handles. We should process this tap unless we have already shown
the selection |
126 // handles (have a long-press selection) and the tap was consumed. | 113 // handles (have a long-press selection) and the tap was consumed. |
127 if (!(consumed && mSelectionType == SelectionType.LONG_PRESS)) { | 114 if (!(consumed && mSelectionType == SelectionType.LONG_PRESS)) { |
128 scheduleInvalidTapNotification(); | 115 scheduleInvalidTapNotification(); |
129 | |
130 if (mHasIdentifiedUnhandledTap) createSearchAction(); | |
131 } | 116 } |
132 } | 117 } |
133 } | 118 } |
134 | 119 |
135 /** | 120 /** |
136 * Constructs a new Selection controller for the given activity. Callbacks
will be issued | 121 * Constructs a new Selection controller for the given activity. Callbacks
will be issued |
137 * through the given selection handler. | 122 * through the given selection handler. |
138 * @param activity The {@link ChromeActivity} to control. | 123 * @param activity The {@link ChromeActivity} to control. |
139 * @param handler The handler for callbacks. | 124 * @param handler The handler for callbacks. |
140 */ | 125 */ |
(...skipping 193 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
334 } | 319 } |
335 | 320 |
336 /** | 321 /** |
337 * Re-enables selection modification handling and invokes | 322 * Re-enables selection modification handling and invokes |
338 * ContextualSearchSelectionHandler.handleSelection(). | 323 * ContextualSearchSelectionHandler.handleSelection(). |
339 * @param selection The text that was selected. | 324 * @param selection The text that was selected. |
340 * @param type The type of selection made by the user. | 325 * @param type The type of selection made by the user. |
341 */ | 326 */ |
342 private void handleSelection(String selection, SelectionType type) { | 327 private void handleSelection(String selection, SelectionType type) { |
343 mShouldHandleSelectionModification = true; | 328 mShouldHandleSelectionModification = true; |
344 destroySearchAction(); | |
345 boolean isValidSelection = validateSelectionSuppression(selection); | 329 boolean isValidSelection = validateSelectionSuppression(selection); |
346 mHandler.handleSelection(selection, isValidSelection, type, mX, mY); | 330 mHandler.handleSelection(selection, isValidSelection, type, mX, mY); |
347 } | 331 } |
348 | 332 |
349 /** | 333 /** |
350 * Resets all internal state of this class, including the tap state. | 334 * Resets all internal state of this class, including the tap state. |
351 */ | 335 */ |
352 private void resetAllStates() { | 336 private void resetAllStates() { |
353 resetSelectionStates(); | 337 resetSelectionStates(); |
354 mLastTapState = null; | 338 mLastTapState = null; |
355 mLastScrollTimeNs = 0; | 339 mLastScrollTimeNs = 0; |
356 mIsContextMenuShown = false; | 340 mIsContextMenuShown = false; |
357 mHasIdentifiedUnhandledTap = false; | |
358 mTapTimeNanoseconds = 0; | |
359 } | 341 } |
360 | 342 |
361 /** | 343 /** |
362 * Resets all of the internal state of this class that handles the selection
. | 344 * Resets all of the internal state of this class that handles the selection
. |
363 */ | 345 */ |
364 private void resetSelectionStates() { | 346 private void resetSelectionStates() { |
365 mSelectionType = SelectionType.UNDETERMINED; | 347 mSelectionType = SelectionType.UNDETERMINED; |
366 mSelectedText = null; | 348 mSelectedText = null; |
367 | 349 |
368 mWasTapGestureDetected = false; | 350 mWasTapGestureDetected = false; |
369 } | 351 } |
370 | 352 |
371 /** | 353 /** |
372 * Should be called when a new Tab is selected. | 354 * Should be called when a new Tab is selected. |
373 * Resets all of the internal state of this class. | 355 * Resets all of the internal state of this class. |
374 */ | 356 */ |
375 void onTabSelected() { | 357 void onTabSelected() { |
376 resetAllStates(); | 358 resetAllStates(); |
377 } | 359 } |
378 | 360 |
379 /** | 361 /** |
380 * Handles an unhandled tap gesture. | 362 * Handles an unhandled tap gesture. |
381 * @param x The x coordinate. | |
382 * @param y The y coordinate. | |
383 */ | 363 */ |
384 void handleShowUnhandledTapUIIfNeeded(int x, int y) { | 364 void handleShowUnhandledTapUIIfNeeded(int x, int y) { |
385 mWasTapGestureDetected = false; | 365 mWasTapGestureDetected = false; |
386 // TODO(donnd): shouldn't we check == TAP here instead of LONG_PRESS? | 366 // TODO(donnd): shouldn't we check == TAP here instead of LONG_PRESS? |
387 // TODO(donnd): refactor to avoid needing a new handler API method as su
ggested by Pedro. | 367 // TODO(donnd): refactor to avoid needing a new handler API method as su
ggested by Pedro. |
388 if (mSelectionType != SelectionType.LONG_PRESS) { | 368 if (mSelectionType != SelectionType.LONG_PRESS) { |
389 mWasTapGestureDetected = true; | 369 mWasTapGestureDetected = true; |
390 mTapTimeNanoseconds = System.nanoTime(); | 370 long tapTimeNanoseconds = System.nanoTime(); |
391 | 371 // TODO(donnd): add a policy method to get adjusted tap count. |
392 // NOTE(donnd): We first acknowledge that a unhandled tap was identi
fied, but | 372 ChromePreferenceManager prefs = ChromePreferenceManager.getInstance(
mActivity); |
393 // we don't do anything now. Instead we'll wait for the onSingleTap(
) event to fire, | 373 int adjustedTapsSinceOpen = prefs.getContextualSearchTapCount() |
394 // and only do something when an unhandled tap was identified. onSin
gleTap() will | 374 - prefs.getContextualSearchTapQuickAnswerCount(); |
395 // always get fired, as opposed to showUnhandledTapUIIfNeeded(). | 375 TapSuppressionHeuristics tapHeuristics = |
396 mHasIdentifiedUnhandledTap = true; | 376 new TapSuppressionHeuristics(this, mLastTapState, x, y, adju
stedTapsSinceOpen); |
397 | 377 // TODO(donnd): Move to be called when the panel closes to work with
states that change. |
| 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); |
398 mX = x; | 382 mX = x; |
399 mY = y; | 383 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); |
400 } else { | 401 } else { |
401 // Long press; reset last tap state. | 402 // Long press; reset last tap state. |
402 mLastTapState = null; | 403 mLastTapState = null; |
403 mHandler.handleInvalidTap(); | 404 mHandler.handleInvalidTap(); |
404 } | 405 } |
405 } | 406 } |
406 | 407 |
407 /** | 408 /** |
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 /** | |
494 * @return The Base Page's {@link ContentViewCore}, or {@code null} if there
is no current tab. | 409 * @return The Base Page's {@link ContentViewCore}, or {@code null} if there
is no current tab. |
495 */ | 410 */ |
496 ContentViewCore getBaseContentView() { | 411 ContentViewCore getBaseContentView() { |
497 // TODO(donnd): switch to using WebContents over ContentViewCore. | |
498 Tab currentTab = mActivity.getActivityTab(); | 412 Tab currentTab = mActivity.getActivityTab(); |
499 return currentTab != null ? currentTab.getContentViewCore() : null; | 413 return currentTab != null ? currentTab.getContentViewCore() : null; |
500 } | 414 } |
501 | 415 |
502 /** | 416 /** |
503 * Expands the current selection by the specified amounts. | 417 * Expands the current selection by the specified amounts. |
504 * @param selectionStartAdjust The start offset adjustment of the selection
to use to highlight | 418 * @param selectionStartAdjust The start offset adjustment of the selection
to use to highlight |
505 * the search term. | 419 * the search term. |
506 * @param selectionEndAdjust The end offset adjustment of the selection to u
se to highlight | 420 * @param selectionEndAdjust The end offset adjustment of the selection to u
se to highlight |
507 * the search term. | 421 * the search term. |
(...skipping 175 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
683 // Starts are inclusive and ends are non-inclusive for both GSAContext &
matcher. | 597 // Starts are inclusive and ends are non-inclusive for both GSAContext &
matcher. |
684 while (matcher.find()) { | 598 while (matcher.find()) { |
685 if (startOffset >= matcher.start() && endOffset <= matcher.end()) { | 599 if (startOffset >= matcher.start() && endOffset <= matcher.end()) { |
686 return true; | 600 return true; |
687 } | 601 } |
688 } | 602 } |
689 | 603 |
690 return false; | 604 return false; |
691 } | 605 } |
692 } | 606 } |
OLD | NEW |