Index: chrome/android/java_staging/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchSelectionController.java |
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchSelectionController.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchSelectionController.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4ed4ea7976765bf587fad0606b0d2b2bb97a470c |
--- /dev/null |
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchSelectionController.java |
@@ -0,0 +1,307 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chrome.browser.contextualsearch; |
+ |
+import android.os.Handler; |
+ |
+import org.chromium.base.VisibleForTesting; |
+import org.chromium.chrome.browser.ChromeActivity; |
+import org.chromium.chrome.browser.Tab; |
+import org.chromium.content.browser.ContentViewCore; |
+import org.chromium.content_public.browser.GestureStateListener; |
+import org.chromium.ui.touch_selection.SelectionEventType; |
+ |
+/** |
+ * Controls selection gesture interaction for Contextual Search. |
+ */ |
+public class ContextualSearchSelectionController { |
+ |
+ /** |
+ * The type of selection made by the user. |
+ */ |
+ public enum SelectionType { |
+ UNDETERMINED, |
+ TAP, |
+ LONG_PRESS |
+ } |
+ |
+ // The number of milliseconds to wait for a selection change after a tap before considering |
+ // the tap invalid. This can't be too small or the subsequent taps may not have established |
+ // a new selection in time. This is because selectWordAroundCaret doesn't always select. |
+ // TODO(donnd): Fix in Blink, crbug.com/435778. |
+ private static final int INVALID_IF_NO_SELECTION_CHANGE_AFTER_TAP_MS = 50; |
+ private static final double RETAP_DISTANCE_SQUARED_DP = Math.pow(75, 2); |
+ |
+ private final ChromeActivity mActivity; |
+ private final ContextualSearchSelectionHandler mHandler; |
+ private final Runnable mHandleInvalidTapRunnable; |
+ private final Handler mRunnableHandler; |
+ private final float mPxToDp; |
+ |
+ private String mSelectedText; |
+ private SelectionType mSelectionType; |
+ private boolean mWasTapGestureDetected; |
+ private boolean mIsSelectionBeingModified; |
+ private boolean mWasLastTapValid; |
+ private boolean mIsWaitingForInvalidTapDetection; |
+ |
+ private float mX; |
+ private float mY; |
+ |
+ private class ContextualSearchGestureStateListener extends GestureStateListener { |
+ @Override |
+ public void onScrollStarted(int scrollOffsetY, int scrollExtentY) { |
+ mHandler.handleScroll(); |
+ } |
+ |
+ // TODO(donnd): Remove this once we get notification of the selection changing |
+ // after a tap-select gets a subsequent tap nearby. Currently there's no |
+ // notification in this case. |
+ // See crbug.com/444114. |
+ @Override |
+ public void onSingleTap(boolean consumed, int x, int y) { |
+ // We may be notified that a tap has happened even when the system consumed the event. |
+ // This is being considered for support for tapping an existing selection to show the |
+ // pins. We should only process this tap if it has not been consumed by the system. |
+ if (!consumed) scheduleInvalidTapNotification(); |
+ } |
+ } |
+ |
+ /** |
+ * Constructs a new Selection controller for the given activity. Callbacks will be issued |
+ * through the given selection handler. |
+ * @param activity The {@link ChromeActivity} to control. |
+ * @param handler The handler for callbacks. |
+ */ |
+ public ContextualSearchSelectionController(ChromeActivity activity, |
+ ContextualSearchSelectionHandler handler) { |
+ mActivity = activity; |
+ mHandler = handler; |
+ mPxToDp = 1.f / mActivity.getResources().getDisplayMetrics().density; |
+ |
+ mRunnableHandler = new Handler(); |
+ mHandleInvalidTapRunnable = new Runnable() { |
+ @Override |
+ public void run() { |
+ onInvalidTapDetectionTimeout(); |
+ } |
+ }; |
+ } |
+ |
+ /** |
+ * Returns a new {@code GestureStateListener} that will listen for events in the Base Page. |
+ * This listener will handle all Contextual Search-related interactions that go through the |
+ * listener. |
+ */ |
+ public ContextualSearchGestureStateListener getGestureStateListener() { |
+ return new ContextualSearchGestureStateListener(); |
+ } |
+ |
+ /** |
+ * @return the type of the selection. |
+ */ |
+ SelectionType getSelectionType() { |
+ return mSelectionType; |
+ } |
+ |
+ /** |
+ * @return the selected text. |
+ */ |
+ String getSelectedText() { |
+ return mSelectedText; |
+ } |
+ |
+ /** |
+ * Clears the selection. |
+ */ |
+ void clearSelection() { |
+ ContentViewCore baseContentView = getBaseContentView(); |
+ if (baseContentView != null) { |
+ baseContentView.clearSelection(); |
+ } |
+ mHandler.onClearSelection(); |
+ |
+ resetAllStates(); |
+ } |
+ |
+ /** |
+ * Handles a change in the current Selection. |
+ * @param selection The selection portion of the context. |
+ */ |
+ void handleSelectionChanged(String selection) { |
+ if (selection == null || selection.isEmpty()) { |
+ scheduleInvalidTapNotification(); |
+ // When the user taps on the page it will place the caret in that position, which |
+ // will trigger a onSelectionChanged event with an empty string. |
+ if (mSelectionType == SelectionType.TAP) { |
+ // Since we mostly ignore a selection that's empty, we only need to partially reset. |
+ resetSelectionStates(); |
+ return; |
+ } |
+ } |
+ if (selection != null && !selection.isEmpty()) { |
+ unscheduleInvalidTapNotification(); |
+ } |
+ if (mIsSelectionBeingModified) { |
+ mSelectedText = selection; |
+ mHandler.handleSelectionModification(selection, mX, mY); |
+ } else if (mWasTapGestureDetected) { |
+ mSelectedText = selection; |
+ mSelectionType = SelectionType.TAP; |
+ mHandler.handleSelection(selection, mSelectionType, mX, mY); |
+ mWasTapGestureDetected = false; |
+ } |
+ } |
+ |
+ /** |
+ * Handles a notification that a selection event took place. |
+ * @param eventType The type of event that took place. |
+ * @param posXPix The x coordinate of the selection start handle. |
+ * @param posYPix The y coordinate of the selection start handle. |
+ */ |
+ void handleSelectionEvent(int eventType, float posXPix, float posYPix) { |
+ boolean shouldHandleSelection = false; |
+ switch (eventType) { |
+ case SelectionEventType.SELECTION_SHOWN: |
+ mWasTapGestureDetected = false; |
+ mSelectionType = SelectionType.LONG_PRESS; |
+ shouldHandleSelection = true; |
+ break; |
+ case SelectionEventType.SELECTION_CLEARED: |
+ mHandler.onClearSelection(); |
+ resetAllStates(); |
+ break; |
+ case SelectionEventType.SELECTION_DRAG_STARTED: |
+ mIsSelectionBeingModified = true; |
+ break; |
+ case SelectionEventType.SELECTION_DRAG_STOPPED: |
+ mIsSelectionBeingModified = false; |
+ shouldHandleSelection = true; |
+ break; |
+ default: |
+ } |
+ |
+ if (shouldHandleSelection) { |
+ ContentViewCore baseContentView = getBaseContentView(); |
+ if (baseContentView != null) { |
+ String selection = baseContentView.getSelectedText(); |
+ if (selection != null) { |
+ mX = posXPix; |
+ mY = posYPix; |
+ mSelectedText = selection; |
+ mHandler.handleSelection(selection, SelectionType.LONG_PRESS, mX, mY); |
+ } |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * Resets all internal state of this class, including the tap state. |
+ */ |
+ private void resetAllStates() { |
+ resetSelectionStates(); |
+ mWasLastTapValid = false; |
+ } |
+ |
+ /** |
+ * Resets all of the internal state of this class that handles the selection. |
+ */ |
+ private void resetSelectionStates() { |
+ mSelectionType = SelectionType.UNDETERMINED; |
+ mSelectedText = null; |
+ |
+ mWasTapGestureDetected = false; |
+ mIsSelectionBeingModified = false; |
+ } |
+ |
+ /** |
+ * Handles an unhandled tap gesture. |
+ */ |
+ void handleShowUnhandledTapUIIfNeeded(int x, int y) { |
+ mWasTapGestureDetected = false; |
+ if (mSelectionType != SelectionType.LONG_PRESS && shouldHandleTap(x, y)) { |
+ mX = x; |
+ mY = y; |
+ mWasLastTapValid = true; |
+ mWasTapGestureDetected = true; |
+ // TODO(donnd): Find a better way to determine that a navigation will be triggered |
+ // by the tap, or merge with other time-consuming actions like gathering surrounding |
+ // text or detecting page mutations. |
+ new Handler().postDelayed(new Runnable() { |
+ @Override |
+ public void run() { |
+ mHandler.handleValidTap(); |
+ } |
+ }, ContextualSearchFieldTrial.getNavigationDetectionDelay()); |
+ } |
+ if (!mWasTapGestureDetected) { |
+ mWasLastTapValid = false; |
+ mHandler.handleInvalidTap(); |
+ } |
+ } |
+ |
+ /** |
+ * @return The Base Page's {@link ContentViewCore}, or {@code null} if there is no current tab. |
+ */ |
+ ContentViewCore getBaseContentView() { |
+ Tab currentTab = mActivity.getActivityTab(); |
+ return currentTab != null ? currentTab.getContentViewCore() : null; |
+ } |
+ |
+ /** |
+ * @return whether a tap at the given coordinates should be handled or not. |
+ */ |
+ private boolean shouldHandleTap(int x, int y) { |
+ return !mWasLastTapValid || wasTapCloseToPreviousTap(x, y); |
+ } |
+ |
+ /** |
+ * Determines whether a tap at the given coordinates is considered "close" to the previous |
+ * tap. |
+ */ |
+ private boolean wasTapCloseToPreviousTap(int x, int y) { |
+ float deltaXDp = (mX - x) * mPxToDp; |
+ float deltaYDp = (mY - y) * mPxToDp; |
+ float distanceSquaredDp = deltaXDp * deltaXDp + deltaYDp * deltaYDp; |
+ return distanceSquaredDp <= RETAP_DISTANCE_SQUARED_DP; |
+ } |
+ |
+ /** |
+ * Schedules a notification to check if the tap was invalid. |
+ * When we call selectWordAroundCaret it selects nothing in cases where the tap was invalid. |
+ * We have no way to know other than scheduling a notification to check later. |
+ * This allows us to hide the bar when there's no selection. |
+ */ |
+ private void scheduleInvalidTapNotification() { |
+ // TODO(donnd): Fix selectWordAroundCaret to we can tell if it selects, instead |
+ // of using a timer here! See crbug.com/435778. |
+ mRunnableHandler.postDelayed(mHandleInvalidTapRunnable, |
+ INVALID_IF_NO_SELECTION_CHANGE_AFTER_TAP_MS); |
+ } |
+ |
+ /** |
+ * Un-schedules all pending notifications to check if a tap was invalid. |
+ */ |
+ private void unscheduleInvalidTapNotification() { |
+ mRunnableHandler.removeCallbacks(mHandleInvalidTapRunnable); |
+ mIsWaitingForInvalidTapDetection = true; |
+ } |
+ |
+ /** |
+ * Notify's the system that tap gesture has been completed. |
+ */ |
+ private void onInvalidTapDetectionTimeout() { |
+ mHandler.handleInvalidTap(); |
+ mIsWaitingForInvalidTapDetection = false; |
+ } |
+ |
+ /** |
+ * @return whether a tap gesture has been detected, for testing. |
+ */ |
+ @VisibleForTesting |
+ boolean wasAnyTapGestureDetected() { |
+ return mIsWaitingForInvalidTapDetection; |
+ } |
+} |