OLD | NEW |
(Empty) | |
| 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.os.Handler; |
| 8 |
| 9 import org.chromium.base.VisibleForTesting; |
| 10 import org.chromium.chrome.browser.ChromeActivity; |
| 11 import org.chromium.chrome.browser.Tab; |
| 12 import org.chromium.content.browser.ContentViewCore; |
| 13 import org.chromium.content_public.browser.GestureStateListener; |
| 14 import org.chromium.ui.touch_selection.SelectionEventType; |
| 15 |
| 16 /** |
| 17 * Controls selection gesture interaction for Contextual Search. |
| 18 */ |
| 19 public class ContextualSearchSelectionController { |
| 20 |
| 21 /** |
| 22 * The type of selection made by the user. |
| 23 */ |
| 24 public enum SelectionType { |
| 25 UNDETERMINED, |
| 26 TAP, |
| 27 LONG_PRESS |
| 28 } |
| 29 |
| 30 // The number of milliseconds to wait for a selection change after a tap bef
ore considering |
| 31 // the tap invalid. This can't be too small or the subsequent taps may not
have established |
| 32 // a new selection in time. This is because selectWordAroundCaret doesn't a
lways select. |
| 33 // TODO(donnd): Fix in Blink, crbug.com/435778. |
| 34 private static final int INVALID_IF_NO_SELECTION_CHANGE_AFTER_TAP_MS = 50; |
| 35 private static final double RETAP_DISTANCE_SQUARED_DP = Math.pow(75, 2); |
| 36 |
| 37 private final ChromeActivity mActivity; |
| 38 private final ContextualSearchSelectionHandler mHandler; |
| 39 private final Runnable mHandleInvalidTapRunnable; |
| 40 private final Handler mRunnableHandler; |
| 41 private final float mPxToDp; |
| 42 |
| 43 private String mSelectedText; |
| 44 private SelectionType mSelectionType; |
| 45 private boolean mWasTapGestureDetected; |
| 46 private boolean mIsSelectionBeingModified; |
| 47 private boolean mWasLastTapValid; |
| 48 private boolean mIsWaitingForInvalidTapDetection; |
| 49 |
| 50 private float mX; |
| 51 private float mY; |
| 52 |
| 53 private class ContextualSearchGestureStateListener extends GestureStateListe
ner { |
| 54 @Override |
| 55 public void onScrollStarted(int scrollOffsetY, int scrollExtentY) { |
| 56 mHandler.handleScroll(); |
| 57 } |
| 58 |
| 59 // TODO(donnd): Remove this once we get notification of the selection ch
anging |
| 60 // after a tap-select gets a subsequent tap nearby. Currently there's n
o |
| 61 // notification in this case. |
| 62 // See crbug.com/444114. |
| 63 @Override |
| 64 public void onSingleTap(boolean consumed, int x, int y) { |
| 65 // We may be notified that a tap has happened even when the system c
onsumed the event. |
| 66 // This is being considered for support for tapping an existing sele
ction to show the |
| 67 // pins. We should only process this tap if it has not been consume
d by the system. |
| 68 if (!consumed) scheduleInvalidTapNotification(); |
| 69 } |
| 70 } |
| 71 |
| 72 /** |
| 73 * Constructs a new Selection controller for the given activity. Callbacks
will be issued |
| 74 * through the given selection handler. |
| 75 * @param activity The {@link ChromeActivity} to control. |
| 76 * @param handler The handler for callbacks. |
| 77 */ |
| 78 public ContextualSearchSelectionController(ChromeActivity activity, |
| 79 ContextualSearchSelectionHandler handler) { |
| 80 mActivity = activity; |
| 81 mHandler = handler; |
| 82 mPxToDp = 1.f / mActivity.getResources().getDisplayMetrics().density; |
| 83 |
| 84 mRunnableHandler = new Handler(); |
| 85 mHandleInvalidTapRunnable = new Runnable() { |
| 86 @Override |
| 87 public void run() { |
| 88 onInvalidTapDetectionTimeout(); |
| 89 } |
| 90 }; |
| 91 } |
| 92 |
| 93 /** |
| 94 * Returns a new {@code GestureStateListener} that will listen for events in
the Base Page. |
| 95 * This listener will handle all Contextual Search-related interactions that
go through the |
| 96 * listener. |
| 97 */ |
| 98 public ContextualSearchGestureStateListener getGestureStateListener() { |
| 99 return new ContextualSearchGestureStateListener(); |
| 100 } |
| 101 |
| 102 /** |
| 103 * @return the type of the selection. |
| 104 */ |
| 105 SelectionType getSelectionType() { |
| 106 return mSelectionType; |
| 107 } |
| 108 |
| 109 /** |
| 110 * @return the selected text. |
| 111 */ |
| 112 String getSelectedText() { |
| 113 return mSelectedText; |
| 114 } |
| 115 |
| 116 /** |
| 117 * Clears the selection. |
| 118 */ |
| 119 void clearSelection() { |
| 120 ContentViewCore baseContentView = getBaseContentView(); |
| 121 if (baseContentView != null) { |
| 122 baseContentView.clearSelection(); |
| 123 } |
| 124 mHandler.onClearSelection(); |
| 125 |
| 126 resetAllStates(); |
| 127 } |
| 128 |
| 129 /** |
| 130 * Handles a change in the current Selection. |
| 131 * @param selection The selection portion of the context. |
| 132 */ |
| 133 void handleSelectionChanged(String selection) { |
| 134 if (selection == null || selection.isEmpty()) { |
| 135 scheduleInvalidTapNotification(); |
| 136 // When the user taps on the page it will place the caret in that po
sition, which |
| 137 // will trigger a onSelectionChanged event with an empty string. |
| 138 if (mSelectionType == SelectionType.TAP) { |
| 139 // Since we mostly ignore a selection that's empty, we only need
to partially reset. |
| 140 resetSelectionStates(); |
| 141 return; |
| 142 } |
| 143 } |
| 144 if (selection != null && !selection.isEmpty()) { |
| 145 unscheduleInvalidTapNotification(); |
| 146 } |
| 147 if (mIsSelectionBeingModified) { |
| 148 mSelectedText = selection; |
| 149 mHandler.handleSelectionModification(selection, mX, mY); |
| 150 } else if (mWasTapGestureDetected) { |
| 151 mSelectedText = selection; |
| 152 mSelectionType = SelectionType.TAP; |
| 153 mHandler.handleSelection(selection, mSelectionType, mX, mY); |
| 154 mWasTapGestureDetected = false; |
| 155 } |
| 156 } |
| 157 |
| 158 /** |
| 159 * Handles a notification that a selection event took place. |
| 160 * @param eventType The type of event that took place. |
| 161 * @param posXPix The x coordinate of the selection start handle. |
| 162 * @param posYPix The y coordinate of the selection start handle. |
| 163 */ |
| 164 void handleSelectionEvent(int eventType, float posXPix, float posYPix) { |
| 165 boolean shouldHandleSelection = false; |
| 166 switch (eventType) { |
| 167 case SelectionEventType.SELECTION_SHOWN: |
| 168 mWasTapGestureDetected = false; |
| 169 mSelectionType = SelectionType.LONG_PRESS; |
| 170 shouldHandleSelection = true; |
| 171 break; |
| 172 case SelectionEventType.SELECTION_CLEARED: |
| 173 mHandler.onClearSelection(); |
| 174 resetAllStates(); |
| 175 break; |
| 176 case SelectionEventType.SELECTION_DRAG_STARTED: |
| 177 mIsSelectionBeingModified = true; |
| 178 break; |
| 179 case SelectionEventType.SELECTION_DRAG_STOPPED: |
| 180 mIsSelectionBeingModified = false; |
| 181 shouldHandleSelection = true; |
| 182 break; |
| 183 default: |
| 184 } |
| 185 |
| 186 if (shouldHandleSelection) { |
| 187 ContentViewCore baseContentView = getBaseContentView(); |
| 188 if (baseContentView != null) { |
| 189 String selection = baseContentView.getSelectedText(); |
| 190 if (selection != null) { |
| 191 mX = posXPix; |
| 192 mY = posYPix; |
| 193 mSelectedText = selection; |
| 194 mHandler.handleSelection(selection, SelectionType.LONG_PRESS
, mX, mY); |
| 195 } |
| 196 } |
| 197 } |
| 198 } |
| 199 |
| 200 /** |
| 201 * Resets all internal state of this class, including the tap state. |
| 202 */ |
| 203 private void resetAllStates() { |
| 204 resetSelectionStates(); |
| 205 mWasLastTapValid = false; |
| 206 } |
| 207 |
| 208 /** |
| 209 * Resets all of the internal state of this class that handles the selection
. |
| 210 */ |
| 211 private void resetSelectionStates() { |
| 212 mSelectionType = SelectionType.UNDETERMINED; |
| 213 mSelectedText = null; |
| 214 |
| 215 mWasTapGestureDetected = false; |
| 216 mIsSelectionBeingModified = false; |
| 217 } |
| 218 |
| 219 /** |
| 220 * Handles an unhandled tap gesture. |
| 221 */ |
| 222 void handleShowUnhandledTapUIIfNeeded(int x, int y) { |
| 223 mWasTapGestureDetected = false; |
| 224 if (mSelectionType != SelectionType.LONG_PRESS && shouldHandleTap(x, y))
{ |
| 225 mX = x; |
| 226 mY = y; |
| 227 mWasLastTapValid = true; |
| 228 mWasTapGestureDetected = true; |
| 229 // TODO(donnd): Find a better way to determine that a navigation wil
l be triggered |
| 230 // by the tap, or merge with other time-consuming actions like gathe
ring surrounding |
| 231 // text or detecting page mutations. |
| 232 new Handler().postDelayed(new Runnable() { |
| 233 @Override |
| 234 public void run() { |
| 235 mHandler.handleValidTap(); |
| 236 } |
| 237 }, ContextualSearchFieldTrial.getNavigationDetectionDelay()); |
| 238 } |
| 239 if (!mWasTapGestureDetected) { |
| 240 mWasLastTapValid = false; |
| 241 mHandler.handleInvalidTap(); |
| 242 } |
| 243 } |
| 244 |
| 245 /** |
| 246 * @return The Base Page's {@link ContentViewCore}, or {@code null} if there
is no current tab. |
| 247 */ |
| 248 ContentViewCore getBaseContentView() { |
| 249 Tab currentTab = mActivity.getActivityTab(); |
| 250 return currentTab != null ? currentTab.getContentViewCore() : null; |
| 251 } |
| 252 |
| 253 /** |
| 254 * @return whether a tap at the given coordinates should be handled or not. |
| 255 */ |
| 256 private boolean shouldHandleTap(int x, int y) { |
| 257 return !mWasLastTapValid || wasTapCloseToPreviousTap(x, y); |
| 258 } |
| 259 |
| 260 /** |
| 261 * Determines whether a tap at the given coordinates is considered "close" t
o the previous |
| 262 * tap. |
| 263 */ |
| 264 private boolean wasTapCloseToPreviousTap(int x, int y) { |
| 265 float deltaXDp = (mX - x) * mPxToDp; |
| 266 float deltaYDp = (mY - y) * mPxToDp; |
| 267 float distanceSquaredDp = deltaXDp * deltaXDp + deltaYDp * deltaYDp; |
| 268 return distanceSquaredDp <= RETAP_DISTANCE_SQUARED_DP; |
| 269 } |
| 270 |
| 271 /** |
| 272 * Schedules a notification to check if the tap was invalid. |
| 273 * When we call selectWordAroundCaret it selects nothing in cases where the
tap was invalid. |
| 274 * We have no way to know other than scheduling a notification to check late
r. |
| 275 * This allows us to hide the bar when there's no selection. |
| 276 */ |
| 277 private void scheduleInvalidTapNotification() { |
| 278 // TODO(donnd): Fix selectWordAroundCaret to we can tell if it selects,
instead |
| 279 // of using a timer here! See crbug.com/435778. |
| 280 mRunnableHandler.postDelayed(mHandleInvalidTapRunnable, |
| 281 INVALID_IF_NO_SELECTION_CHANGE_AFTER_TAP_MS); |
| 282 } |
| 283 |
| 284 /** |
| 285 * Un-schedules all pending notifications to check if a tap was invalid. |
| 286 */ |
| 287 private void unscheduleInvalidTapNotification() { |
| 288 mRunnableHandler.removeCallbacks(mHandleInvalidTapRunnable); |
| 289 mIsWaitingForInvalidTapDetection = true; |
| 290 } |
| 291 |
| 292 /** |
| 293 * Notify's the system that tap gesture has been completed. |
| 294 */ |
| 295 private void onInvalidTapDetectionTimeout() { |
| 296 mHandler.handleInvalidTap(); |
| 297 mIsWaitingForInvalidTapDetection = false; |
| 298 } |
| 299 |
| 300 /** |
| 301 * @return whether a tap gesture has been detected, for testing. |
| 302 */ |
| 303 @VisibleForTesting |
| 304 boolean wasAnyTapGestureDetected() { |
| 305 return mIsWaitingForInvalidTapDetection; |
| 306 } |
| 307 } |
OLD | NEW |