PointerTracker.java revision 212165b0b8308802a461a6a526d367ba67b5567a
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.keyboard; 18 19import android.content.res.Resources; 20import android.content.res.TypedArray; 21import android.os.SystemClock; 22import android.util.DisplayMetrics; 23import android.util.Log; 24import android.view.MotionEvent; 25 26import com.android.inputmethod.accessibility.AccessibilityUtils; 27import com.android.inputmethod.keyboard.internal.GestureStroke; 28import com.android.inputmethod.keyboard.internal.GestureStroke.GestureStrokeParams; 29import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints; 30import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints.GestureStrokePreviewParams; 31import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; 32import com.android.inputmethod.latin.Constants; 33import com.android.inputmethod.latin.InputPointers; 34import com.android.inputmethod.latin.LatinImeLogger; 35import com.android.inputmethod.latin.R; 36import com.android.inputmethod.latin.define.ProductionFlag; 37import com.android.inputmethod.latin.settings.Settings; 38import com.android.inputmethod.latin.utils.CollectionUtils; 39import com.android.inputmethod.latin.utils.CoordinateUtils; 40import com.android.inputmethod.latin.utils.ResourceUtils; 41import com.android.inputmethod.research.ResearchLogger; 42 43import java.util.ArrayList; 44 45public final class PointerTracker implements PointerTrackerQueue.Element { 46 private static final String TAG = PointerTracker.class.getSimpleName(); 47 private static final boolean DEBUG_EVENT = false; 48 private static final boolean DEBUG_MOVE_EVENT = false; 49 private static final boolean DEBUG_LISTENER = false; 50 private static boolean DEBUG_MODE = LatinImeLogger.sDBG || DEBUG_EVENT; 51 52 /** True if {@link PointerTracker}s should handle gesture events. */ 53 private static boolean sShouldHandleGesture = false; 54 private static boolean sMainDictionaryAvailable = false; 55 private static boolean sGestureHandlingEnabledByInputField = false; 56 private static boolean sGestureHandlingEnabledByUser = false; 57 58 public interface KeyEventHandler { 59 /** 60 * Get KeyDetector object that is used for this PointerTracker. 61 * @return the KeyDetector object that is used for this PointerTracker 62 */ 63 public KeyDetector getKeyDetector(); 64 65 /** 66 * Get KeyboardActionListener object that is used to register key code and so on. 67 * @return the KeyboardActionListner for this PointerTracker 68 */ 69 public KeyboardActionListener getKeyboardActionListener(); 70 71 /** 72 * Get DrawingProxy object that is used for this PointerTracker. 73 * @return the DrawingProxy object that is used for this PointerTracker 74 */ 75 public DrawingProxy getDrawingProxy(); 76 77 /** 78 * Get TimerProxy object that handles key repeat and long press timer event for this 79 * PointerTracker. 80 * @return the TimerProxy object that handles key repeat and long press timer event. 81 */ 82 public TimerProxy getTimerProxy(); 83 } 84 85 public interface DrawingProxy { 86 public void invalidateKey(Key key); 87 public void showKeyPreview(PointerTracker tracker); 88 public void dismissKeyPreview(PointerTracker tracker); 89 public void showSlidingKeyInputPreview(PointerTracker tracker); 90 public void dismissSlidingKeyInputPreview(); 91 public void showGestureTrail(PointerTracker tracker, boolean showsFloatingPreviewText); 92 } 93 94 public interface TimerProxy { 95 public void startTypingStateTimer(Key typedKey); 96 public boolean isTypingState(); 97 public void startKeyRepeatTimer(PointerTracker tracker); 98 public void startLongPressTimer(PointerTracker tracker, int delay); 99 public void cancelLongPressTimer(); 100 public void startDoubleTapShiftKeyTimer(); 101 public void cancelDoubleTapShiftKeyTimer(); 102 public boolean isInDoubleTapShiftKeyTimeout(); 103 public void cancelKeyTimers(); 104 public void startUpdateBatchInputTimer(PointerTracker tracker); 105 public void cancelUpdateBatchInputTimer(PointerTracker tracker); 106 public void cancelAllUpdateBatchInputTimers(); 107 108 public static class Adapter implements TimerProxy { 109 @Override 110 public void startTypingStateTimer(Key typedKey) {} 111 @Override 112 public boolean isTypingState() { return false; } 113 @Override 114 public void startKeyRepeatTimer(PointerTracker tracker) {} 115 @Override 116 public void startLongPressTimer(PointerTracker tracker, int delay) {} 117 @Override 118 public void cancelLongPressTimer() {} 119 @Override 120 public void startDoubleTapShiftKeyTimer() {} 121 @Override 122 public void cancelDoubleTapShiftKeyTimer() {} 123 @Override 124 public boolean isInDoubleTapShiftKeyTimeout() { return false; } 125 @Override 126 public void cancelKeyTimers() {} 127 @Override 128 public void startUpdateBatchInputTimer(PointerTracker tracker) {} 129 @Override 130 public void cancelUpdateBatchInputTimer(PointerTracker tracker) {} 131 @Override 132 public void cancelAllUpdateBatchInputTimers() {} 133 } 134 } 135 136 static final class PointerTrackerParams { 137 public final boolean mSlidingKeyInputEnabled; 138 public final int mTouchNoiseThresholdTime; 139 public final int mTouchNoiseThresholdDistance; 140 public final int mSuppressKeyPreviewAfterBatchInputDuration; 141 public final int mLongPressShiftLockTimeout; 142 143 public static final PointerTrackerParams DEFAULT = new PointerTrackerParams(); 144 145 private PointerTrackerParams() { 146 mSlidingKeyInputEnabled = false; 147 mTouchNoiseThresholdTime = 0; 148 mTouchNoiseThresholdDistance = 0; 149 mSuppressKeyPreviewAfterBatchInputDuration = 0; 150 mLongPressShiftLockTimeout = 0; 151 } 152 153 public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) { 154 mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean( 155 R.styleable.MainKeyboardView_slidingKeyInputEnable, false); 156 mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt( 157 R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0); 158 mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimensionPixelSize( 159 R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0); 160 mSuppressKeyPreviewAfterBatchInputDuration = mainKeyboardViewAttr.getInt( 161 R.styleable.MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration, 0); 162 mLongPressShiftLockTimeout = mainKeyboardViewAttr.getInt( 163 R.styleable.MainKeyboardView_longPressShiftLockTimeout, 0); 164 } 165 } 166 167 // Parameters for pointer handling. 168 private static PointerTrackerParams sParams; 169 private static GestureStrokeParams sGestureStrokeParams; 170 private static GestureStrokePreviewParams sGesturePreviewParams; 171 private static boolean sNeedsPhantomSuddenMoveEventHack; 172 // Move this threshold to resource. 173 // TODO: Device specific parameter would be better for device specific hack? 174 private static final float PHANTOM_SUDDEN_MOVE_THRESHOLD = 0.25f; // in keyWidth 175 // This hack is applied to certain classes of tablets. 176 // See {@link #needsProximateBogusDownMoveUpEventHack(Resources)}. 177 private static boolean sNeedsProximateBogusDownMoveUpEventHack; 178 179 private static final ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList(); 180 private static final PointerTrackerQueue sPointerTrackerQueue = new PointerTrackerQueue(); 181 182 public final int mPointerId; 183 184 private DrawingProxy mDrawingProxy; 185 private TimerProxy mTimerProxy; 186 private KeyDetector mKeyDetector; 187 private KeyboardActionListener mListener = KeyboardActionListener.EMPTY_LISTENER; 188 189 private Keyboard mKeyboard; 190 private int mPhantonSuddenMoveThreshold; 191 private final BogusMoveEventDetector mBogusMoveEventDetector = new BogusMoveEventDetector(); 192 193 private boolean mIsDetectingGesture = false; // per PointerTracker. 194 private static boolean sInGesture = false; 195 private static long sGestureFirstDownTime; 196 private static TimeRecorder sTimeRecorder; 197 private static final InputPointers sAggregratedPointers = new InputPointers( 198 GestureStroke.DEFAULT_CAPACITY); 199 private static int sLastRecognitionPointSize = 0; // synchronized using sAggregratedPointers 200 private static long sLastRecognitionTime = 0; // synchronized using sAggregratedPointers 201 202 static final class BogusMoveEventDetector { 203 // Move these thresholds to resource. 204 // These thresholds' unit is a diagonal length of a key. 205 private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f; 206 private static final float BOGUS_MOVE_RADIUS_THRESHOLD = 1.14f; 207 208 private int mAccumulatedDistanceThreshold; 209 private int mRadiusThreshold; 210 211 // Accumulated distance from actual and artificial down keys. 212 /* package */ int mAccumulatedDistanceFromDownKey; 213 private int mActualDownX; 214 private int mActualDownY; 215 216 public void setKeyboardGeometry(final int keyWidth, final int keyHeight) { 217 final float keyDiagonal = (float)Math.hypot(keyWidth, keyHeight); 218 mAccumulatedDistanceThreshold = (int)( 219 keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD); 220 mRadiusThreshold = (int)(keyDiagonal * BOGUS_MOVE_RADIUS_THRESHOLD); 221 } 222 223 public void onActualDownEvent(final int x, final int y) { 224 mActualDownX = x; 225 mActualDownY = y; 226 } 227 228 public void onDownKey() { 229 mAccumulatedDistanceFromDownKey = 0; 230 } 231 232 public void onMoveKey(final int distance) { 233 mAccumulatedDistanceFromDownKey += distance; 234 } 235 236 public boolean hasTraveledLongDistance(final int x, final int y) { 237 final int dx = Math.abs(x - mActualDownX); 238 final int dy = Math.abs(y - mActualDownY); 239 // A bogus move event should be a horizontal movement. A vertical movement might be 240 // a sloppy typing and should be ignored. 241 return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold; 242 } 243 244 /* package */ int getDistanceFromDownEvent(final int x, final int y) { 245 return getDistance(x, y, mActualDownX, mActualDownY); 246 } 247 248 public boolean isCloseToActualDownEvent(final int x, final int y) { 249 return getDistanceFromDownEvent(x, y) < mRadiusThreshold; 250 } 251 } 252 253 static final class TimeRecorder { 254 private final int mSuppressKeyPreviewAfterBatchInputDuration; 255 private final int mStaticTimeThresholdAfterFastTyping; // msec 256 private long mLastTypingTime; 257 private long mLastLetterTypingTime; 258 private long mLastBatchInputTime; 259 260 public TimeRecorder(final PointerTrackerParams pointerTrackerParams, 261 final GestureStrokeParams gestureStrokeParams) { 262 mSuppressKeyPreviewAfterBatchInputDuration = 263 pointerTrackerParams.mSuppressKeyPreviewAfterBatchInputDuration; 264 mStaticTimeThresholdAfterFastTyping = 265 gestureStrokeParams.mStaticTimeThresholdAfterFastTyping; 266 } 267 268 public boolean isInFastTyping(final long eventTime) { 269 final long elapsedTimeSinceLastLetterTyping = eventTime - mLastLetterTypingTime; 270 return elapsedTimeSinceLastLetterTyping < mStaticTimeThresholdAfterFastTyping; 271 } 272 273 private boolean wasLastInputTyping() { 274 return mLastTypingTime >= mLastBatchInputTime; 275 } 276 277 public void onCodeInput(final int code, final long eventTime) { 278 // Record the letter typing time when 279 // 1. Letter keys are typed successively without any batch input in between. 280 // 2. A letter key is typed within the threshold time since the last any key typing. 281 // 3. A non-letter key is typed within the threshold time since the last letter key 282 // typing. 283 if (Character.isLetter(code)) { 284 if (wasLastInputTyping() 285 || eventTime - mLastTypingTime < mStaticTimeThresholdAfterFastTyping) { 286 mLastLetterTypingTime = eventTime; 287 } 288 } else { 289 if (eventTime - mLastLetterTypingTime < mStaticTimeThresholdAfterFastTyping) { 290 // This non-letter typing should be treated as a part of fast typing. 291 mLastLetterTypingTime = eventTime; 292 } 293 } 294 mLastTypingTime = eventTime; 295 } 296 297 public void onEndBatchInput(final long eventTime) { 298 mLastBatchInputTime = eventTime; 299 } 300 301 public long getLastLetterTypingTime() { 302 return mLastLetterTypingTime; 303 } 304 305 public boolean needsToSuppressKeyPreviewPopup(final long eventTime) { 306 return !wasLastInputTyping() 307 && eventTime - mLastBatchInputTime < mSuppressKeyPreviewAfterBatchInputDuration; 308 } 309 } 310 311 // The position and time at which first down event occurred. 312 private long mDownTime; 313 private int[] mDownCoordinates = CoordinateUtils.newInstance(); 314 private long mUpTime; 315 316 // The current key where this pointer is. 317 private Key mCurrentKey = null; 318 // The position where the current key was recognized for the first time. 319 private int mKeyX; 320 private int mKeyY; 321 322 // Last pointer position. 323 private int mLastX; 324 private int mLastY; 325 326 // true if keyboard layout has been changed. 327 private boolean mKeyboardLayoutHasBeenChanged; 328 329 // true if this pointer is no longer triggering any action because it has been canceled. 330 private boolean mIsTrackingForActionDisabled; 331 332 // the more keys panel currently being shown. equals null if no panel is active. 333 private MoreKeysPanel mMoreKeysPanel; 334 335 private static final int MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT = 3; 336 // true if this pointer is in a sliding key input. 337 boolean mIsInSlidingKeyInput; 338 // true if this pointer is in a sliding key input from a modifier key, 339 // so that further modifier keys should be ignored. 340 boolean mIsInSlidingKeyInputFromModifier; 341 342 // true if a sliding key input is allowed. 343 private boolean mIsAllowedSlidingKeyInput; 344 345 private final GestureStrokeWithPreviewPoints mGestureStrokeWithPreviewPoints; 346 347 private static final int SMALL_TABLET_SMALLEST_WIDTH = 600; // dp 348 private static final int LARGE_TABLET_SMALLEST_WIDTH = 768; // dp 349 350 private static boolean needsProximateBogusDownMoveUpEventHack(final Resources res) { 351 // The proximate bogus down move up event hack is needed for a device such like, 352 // 1) is large tablet, or 2) is small tablet and the screen density is less than hdpi. 353 // Though it seems odd to use screen density as criteria of the quality of the touch 354 // screen, the small table that has a less density screen than hdpi most likely has been 355 // made with the touch screen that needs the hack. 356 final int sw = res.getConfiguration().smallestScreenWidthDp; 357 final boolean isLargeTablet = (sw >= LARGE_TABLET_SMALLEST_WIDTH); 358 final boolean isSmallTablet = 359 (sw >= SMALL_TABLET_SMALLEST_WIDTH && sw < LARGE_TABLET_SMALLEST_WIDTH); 360 final int densityDpi = res.getDisplayMetrics().densityDpi; 361 final boolean hasLowDensityScreen = (densityDpi < DisplayMetrics.DENSITY_HIGH); 362 final boolean needsTheHack = isLargeTablet || (isSmallTablet && hasLowDensityScreen); 363 if (DEBUG_MODE) { 364 Log.d(TAG, "needsProximateBogusDownMoveUpEventHack=" + needsTheHack 365 + " smallestScreenWidthDp=" + sw + " densityDpi=" + densityDpi); 366 } 367 return needsTheHack; 368 } 369 370 public static void init(final Resources res) { 371 sNeedsPhantomSuddenMoveEventHack = Boolean.parseBoolean( 372 ResourceUtils.getDeviceOverrideValue( 373 res, R.array.phantom_sudden_move_event_device_list)); 374 sNeedsProximateBogusDownMoveUpEventHack = needsProximateBogusDownMoveUpEventHack(res); 375 sParams = PointerTrackerParams.DEFAULT; 376 sGestureStrokeParams = GestureStrokeParams.DEFAULT; 377 sGesturePreviewParams = GestureStrokePreviewParams.DEFAULT; 378 sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams); 379 } 380 381 public static void setParameters(final TypedArray mainKeyboardViewAttr) { 382 sParams = new PointerTrackerParams(mainKeyboardViewAttr); 383 sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr); 384 sGesturePreviewParams = new GestureStrokePreviewParams(mainKeyboardViewAttr); 385 sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams); 386 } 387 388 private static void updateGestureHandlingMode() { 389 sShouldHandleGesture = sMainDictionaryAvailable 390 && sGestureHandlingEnabledByInputField 391 && sGestureHandlingEnabledByUser 392 && !AccessibilityUtils.getInstance().isTouchExplorationEnabled(); 393 } 394 395 // Note that this method is called from a non-UI thread. 396 public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { 397 sMainDictionaryAvailable = mainDictionaryAvailable; 398 updateGestureHandlingMode(); 399 } 400 401 public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { 402 sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser; 403 updateGestureHandlingMode(); 404 } 405 406 public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) { 407 final ArrayList<PointerTracker> trackers = sTrackers; 408 409 // Create pointer trackers until we can get 'id+1'-th tracker, if needed. 410 for (int i = trackers.size(); i <= id; i++) { 411 final PointerTracker tracker = new PointerTracker(i, handler); 412 trackers.add(tracker); 413 } 414 415 return trackers.get(id); 416 } 417 418 public static boolean isAnyInSlidingKeyInput() { 419 return sPointerTrackerQueue.isAnyInSlidingKeyInput(); 420 } 421 422 public static void cancelAllPointerTrackers() { 423 sPointerTrackerQueue.cancelAllPointerTrackers(); 424 } 425 426 public static void setKeyboardActionListener(final KeyboardActionListener listener) { 427 final int trackersSize = sTrackers.size(); 428 for (int i = 0; i < trackersSize; ++i) { 429 final PointerTracker tracker = sTrackers.get(i); 430 tracker.mListener = listener; 431 } 432 } 433 434 public static void setKeyDetector(final KeyDetector keyDetector) { 435 final int trackersSize = sTrackers.size(); 436 for (int i = 0; i < trackersSize; ++i) { 437 final PointerTracker tracker = sTrackers.get(i); 438 tracker.setKeyDetectorInner(keyDetector); 439 // Mark that keyboard layout has been changed. 440 tracker.mKeyboardLayoutHasBeenChanged = true; 441 } 442 final Keyboard keyboard = keyDetector.getKeyboard(); 443 sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput(); 444 updateGestureHandlingMode(); 445 } 446 447 public static void setReleasedKeyGraphicsToAllKeys() { 448 final int trackersSize = sTrackers.size(); 449 for (int i = 0; i < trackersSize; ++i) { 450 final PointerTracker tracker = sTrackers.get(i); 451 tracker.setReleasedKeyGraphics(tracker.mCurrentKey); 452 } 453 } 454 455 public static void dismissAllMoreKeysPanels() { 456 final int trackersSize = sTrackers.size(); 457 for (int i = 0; i < trackersSize; ++i) { 458 final PointerTracker tracker = sTrackers.get(i); 459 if (tracker.isShowingMoreKeysPanel()) { 460 tracker.mMoreKeysPanel.dismissMoreKeysPanel(); 461 tracker.mMoreKeysPanel = null; 462 } 463 } 464 } 465 466 private PointerTracker(final int id, final KeyEventHandler handler) { 467 if (handler == null) { 468 throw new NullPointerException(); 469 } 470 mPointerId = id; 471 mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints( 472 id, sGestureStrokeParams, sGesturePreviewParams); 473 setKeyDetectorInner(handler.getKeyDetector()); 474 mListener = handler.getKeyboardActionListener(); 475 mDrawingProxy = handler.getDrawingProxy(); 476 mTimerProxy = handler.getTimerProxy(); 477 } 478 479 // Returns true if keyboard has been changed by this callback. 480 private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) { 481 // While gesture input is going on, this method should be a no-operation. But when gesture 482 // input has been canceled, <code>sInGesture</code> and <code>mIsDetectingGesture</code> 483 // are set to false. To keep this method is a no-operation, 484 // <code>mIsTrackingForActionDisabled</code> should also be taken account of. 485 if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) { 486 return false; 487 } 488 final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); 489 if (DEBUG_LISTENER) { 490 Log.d(TAG, String.format("[%d] onPress : %s%s%s", mPointerId, 491 KeyDetector.printableCode(key), 492 ignoreModifierKey ? " ignoreModifier" : "", 493 key.isEnabled() ? "" : " disabled")); 494 } 495 if (ignoreModifierKey) { 496 return false; 497 } 498 if (key.isEnabled()) { 499 mListener.onPressKey(key.mCode, false /* isRepeatKey */, 500 getActivePointerTrackerCount() == 1); 501 final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged; 502 mKeyboardLayoutHasBeenChanged = false; 503 mTimerProxy.startTypingStateTimer(key); 504 return keyboardLayoutHasBeenChanged; 505 } 506 return false; 507 } 508 509 // Note that we need primaryCode argument because the keyboard may in shifted state and the 510 // primaryCode is different from {@link Key#mCode}. 511 private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x, 512 final int y, final long eventTime) { 513 final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); 514 final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); 515 final int code = altersCode ? key.getAltCode() : primaryCode; 516 if (DEBUG_LISTENER) { 517 final String output = code == Constants.CODE_OUTPUT_TEXT 518 ? key.getOutputText() : Constants.printableCode(code); 519 Log.d(TAG, String.format("[%d] onCodeInput: %4d %4d %s%s%s", mPointerId, x, y, 520 output, ignoreModifierKey ? " ignoreModifier" : "", 521 altersCode ? " altersCode" : "", key.isEnabled() ? "" : " disabled")); 522 } 523 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 524 ResearchLogger.pointerTracker_callListenerOnCodeInput(key, x, y, ignoreModifierKey, 525 altersCode, code); 526 } 527 if (ignoreModifierKey) { 528 return; 529 } 530 // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state. 531 if (key.isEnabled() || altersCode) { 532 sTimeRecorder.onCodeInput(code, eventTime); 533 if (code == Constants.CODE_OUTPUT_TEXT) { 534 mListener.onTextInput(key.getOutputText()); 535 } else if (code != Constants.CODE_UNSPECIFIED) { 536 mListener.onCodeInput(code, x, y); 537 } 538 } 539 } 540 541 // Note that we need primaryCode argument because the keyboard may be in shifted state and the 542 // primaryCode is different from {@link Key#mCode}. 543 private void callListenerOnRelease(final Key key, final int primaryCode, 544 final boolean withSliding) { 545 // See the comment at {@link #callListenerOnPressAndCheckKeyboardLayoutChange(Key}}. 546 if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) { 547 return; 548 } 549 final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); 550 if (DEBUG_LISTENER) { 551 Log.d(TAG, String.format("[%d] onRelease : %s%s%s%s", mPointerId, 552 Constants.printableCode(primaryCode), 553 withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : "", 554 key.isEnabled() ? "": " disabled")); 555 } 556 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 557 ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding, 558 ignoreModifierKey); 559 } 560 if (ignoreModifierKey) { 561 return; 562 } 563 if (key.isEnabled()) { 564 mListener.onReleaseKey(primaryCode, withSliding); 565 } 566 } 567 568 private void callListenerOnFinishSlidingInput() { 569 if (DEBUG_LISTENER) { 570 Log.d(TAG, String.format("[%d] onFinishSlidingInput", mPointerId)); 571 } 572 mListener.onFinishSlidingInput(); 573 } 574 575 private void callListenerOnCancelInput() { 576 if (DEBUG_LISTENER) { 577 Log.d(TAG, String.format("[%d] onCancelInput", mPointerId)); 578 } 579 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 580 ResearchLogger.pointerTracker_callListenerOnCancelInput(); 581 } 582 mListener.onCancelInput(); 583 } 584 585 private void setKeyDetectorInner(final KeyDetector keyDetector) { 586 final Keyboard keyboard = keyDetector.getKeyboard(); 587 if (keyDetector == mKeyDetector && keyboard == mKeyboard) { 588 return; 589 } 590 mKeyDetector = keyDetector; 591 mKeyboard = keyDetector.getKeyboard(); 592 final int keyWidth = mKeyboard.mMostCommonKeyWidth; 593 final int keyHeight = mKeyboard.mMostCommonKeyHeight; 594 mGestureStrokeWithPreviewPoints.setKeyboardGeometry(keyWidth, mKeyboard.mOccupiedHeight); 595 final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY); 596 if (newKey != mCurrentKey) { 597 if (mDrawingProxy != null) { 598 setReleasedKeyGraphics(mCurrentKey); 599 } 600 // Keep {@link #mCurrentKey} that comes from previous keyboard. 601 } 602 mPhantonSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD); 603 mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight); 604 } 605 606 @Override 607 public boolean isInSlidingKeyInput() { 608 return mIsInSlidingKeyInput; 609 } 610 611 public Key getKey() { 612 return mCurrentKey; 613 } 614 615 @Override 616 public boolean isModifier() { 617 return mCurrentKey != null && mCurrentKey.isModifier(); 618 } 619 620 public Key getKeyOn(final int x, final int y) { 621 return mKeyDetector.detectHitKey(x, y); 622 } 623 624 private void setReleasedKeyGraphics(final Key key) { 625 mDrawingProxy.dismissKeyPreview(this); 626 if (key == null) { 627 return; 628 } 629 630 // Even if the key is disabled, update the key release graphics just in case. 631 updateReleaseKeyGraphics(key); 632 633 if (key.isShift()) { 634 for (final Key shiftKey : mKeyboard.mShiftKeys) { 635 if (shiftKey != key) { 636 updateReleaseKeyGraphics(shiftKey); 637 } 638 } 639 } 640 641 if (key.altCodeWhileTyping()) { 642 final int altCode = key.getAltCode(); 643 final Key altKey = mKeyboard.getKey(altCode); 644 if (altKey != null) { 645 updateReleaseKeyGraphics(altKey); 646 } 647 for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) { 648 if (k != key && k.getAltCode() == altCode) { 649 updateReleaseKeyGraphics(k); 650 } 651 } 652 } 653 } 654 655 private static boolean needsToSuppressKeyPreviewPopup(final long eventTime) { 656 if (!sShouldHandleGesture) return false; 657 return sTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime); 658 } 659 660 private void setPressedKeyGraphics(final Key key, final long eventTime) { 661 if (key == null) { 662 return; 663 } 664 665 // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state. 666 final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); 667 final boolean needsToUpdateGraphics = key.isEnabled() || altersCode; 668 if (!needsToUpdateGraphics) { 669 return; 670 } 671 672 if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) { 673 mDrawingProxy.showKeyPreview(this); 674 } 675 updatePressKeyGraphics(key); 676 677 if (key.isShift()) { 678 for (final Key shiftKey : mKeyboard.mShiftKeys) { 679 if (shiftKey != key) { 680 updatePressKeyGraphics(shiftKey); 681 } 682 } 683 } 684 685 if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) { 686 final int altCode = key.getAltCode(); 687 final Key altKey = mKeyboard.getKey(altCode); 688 if (altKey != null) { 689 updatePressKeyGraphics(altKey); 690 } 691 for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) { 692 if (k != key && k.getAltCode() == altCode) { 693 updatePressKeyGraphics(k); 694 } 695 } 696 } 697 } 698 699 private void updateReleaseKeyGraphics(final Key key) { 700 key.onReleased(); 701 mDrawingProxy.invalidateKey(key); 702 } 703 704 private void updatePressKeyGraphics(final Key key) { 705 key.onPressed(); 706 mDrawingProxy.invalidateKey(key); 707 } 708 709 public GestureStrokeWithPreviewPoints getGestureStrokeWithPreviewPoints() { 710 return mGestureStrokeWithPreviewPoints; 711 } 712 713 public void getLastCoordinates(final int[] outCoords) { 714 CoordinateUtils.set(outCoords, mLastX, mLastY); 715 } 716 717 public long getDownTime() { 718 return mDownTime; 719 } 720 721 public void getDownCoordinates(final int[] outCoords) { 722 CoordinateUtils.copy(outCoords, mDownCoordinates); 723 } 724 725 private Key onDownKey(final int x, final int y, final long eventTime) { 726 mDownTime = eventTime; 727 CoordinateUtils.set(mDownCoordinates, x, y); 728 mBogusMoveEventDetector.onDownKey(); 729 return onMoveToNewKey(onMoveKeyInternal(x, y), x, y); 730 } 731 732 static int getDistance(final int x1, final int y1, final int x2, final int y2) { 733 return (int)Math.hypot(x1 - x2, y1 - y2); 734 } 735 736 private Key onMoveKeyInternal(final int x, final int y) { 737 mBogusMoveEventDetector.onMoveKey(getDistance(x, y, mLastX, mLastY)); 738 mLastX = x; 739 mLastY = y; 740 return mKeyDetector.detectHitKey(x, y); 741 } 742 743 private Key onMoveKey(final int x, final int y) { 744 return onMoveKeyInternal(x, y); 745 } 746 747 private Key onMoveToNewKey(final Key newKey, final int x, final int y) { 748 mCurrentKey = newKey; 749 mKeyX = x; 750 mKeyY = y; 751 return newKey; 752 } 753 754 private static int getActivePointerTrackerCount() { 755 return sPointerTrackerQueue.size(); 756 } 757 758 private boolean isOldestTrackerInQueue() { 759 return sPointerTrackerQueue.getOldestElement() == this; 760 } 761 762 private void mayStartBatchInput(final Key key) { 763 if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) { 764 return; 765 } 766 if (key == null || !Character.isLetter(key.mCode)) { 767 return; 768 } 769 if (DEBUG_LISTENER) { 770 Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId)); 771 } 772 sInGesture = true; 773 synchronized (sAggregratedPointers) { 774 sAggregratedPointers.reset(); 775 sLastRecognitionPointSize = 0; 776 sLastRecognitionTime = 0; 777 mListener.onStartBatchInput(); 778 dismissAllMoreKeysPanels(); 779 } 780 mTimerProxy.cancelLongPressTimer(); 781 // A gesture floating preview text will be shown at the oldest pointer/finger on the screen. 782 mDrawingProxy.showGestureTrail( 783 this, isOldestTrackerInQueue() /* showsFloatingPreviewText */); 784 } 785 786 public void updateBatchInputByTimer(final long eventTime) { 787 final int gestureTime = (int)(eventTime - sGestureFirstDownTime); 788 mGestureStrokeWithPreviewPoints.duplicateLastPointWith(gestureTime); 789 updateBatchInput(eventTime); 790 } 791 792 private void mayUpdateBatchInput(final long eventTime, final Key key) { 793 if (key != null) { 794 updateBatchInput(eventTime); 795 } 796 if (mIsTrackingForActionDisabled) { 797 return; 798 } 799 // A gesture floating preview text will be shown at the oldest pointer/finger on the screen. 800 mDrawingProxy.showGestureTrail( 801 this, isOldestTrackerInQueue() /* showsFloatingPreviewText */); 802 } 803 804 private void updateBatchInput(final long eventTime) { 805 synchronized (sAggregratedPointers) { 806 final GestureStroke stroke = mGestureStrokeWithPreviewPoints; 807 stroke.appendIncrementalBatchPoints(sAggregratedPointers); 808 final int size = sAggregratedPointers.getPointerSize(); 809 if (size > sLastRecognitionPointSize 810 && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) { 811 sLastRecognitionPointSize = size; 812 sLastRecognitionTime = eventTime; 813 if (DEBUG_LISTENER) { 814 Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", mPointerId, 815 size)); 816 } 817 mTimerProxy.startUpdateBatchInputTimer(this); 818 mListener.onUpdateBatchInput(sAggregratedPointers); 819 } 820 } 821 } 822 823 private void mayEndBatchInput(final long eventTime) { 824 synchronized (sAggregratedPointers) { 825 mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers); 826 if (getActivePointerTrackerCount() == 1) { 827 sInGesture = false; 828 sTimeRecorder.onEndBatchInput(eventTime); 829 mTimerProxy.cancelAllUpdateBatchInputTimers(); 830 if (!mIsTrackingForActionDisabled) { 831 if (DEBUG_LISTENER) { 832 Log.d(TAG, String.format("[%d] onEndBatchInput : batchPoints=%d", 833 mPointerId, sAggregratedPointers.getPointerSize())); 834 } 835 mListener.onEndBatchInput(sAggregratedPointers); 836 } 837 } 838 } 839 if (mIsTrackingForActionDisabled) { 840 return; 841 } 842 // A gesture floating preview text will be shown at the oldest pointer/finger on the screen. 843 mDrawingProxy.showGestureTrail( 844 this, isOldestTrackerInQueue() /* showsFloatingPreviewText */); 845 } 846 847 private void cancelBatchInput() { 848 cancelAllPointerTrackers(); 849 mIsDetectingGesture = false; 850 if (!sInGesture) { 851 return; 852 } 853 sInGesture = false; 854 if (DEBUG_LISTENER) { 855 Log.d(TAG, String.format("[%d] onCancelBatchInput", mPointerId)); 856 } 857 mListener.onCancelBatchInput(); 858 } 859 860 public void processMotionEvent(final int action, final int x, final int y, final long eventTime, 861 final KeyEventHandler handler) { 862 switch (action) { 863 case MotionEvent.ACTION_DOWN: 864 case MotionEvent.ACTION_POINTER_DOWN: 865 onDownEvent(x, y, eventTime, handler); 866 break; 867 case MotionEvent.ACTION_UP: 868 case MotionEvent.ACTION_POINTER_UP: 869 onUpEvent(x, y, eventTime); 870 break; 871 case MotionEvent.ACTION_MOVE: 872 onMoveEvent(x, y, eventTime, null); 873 break; 874 case MotionEvent.ACTION_CANCEL: 875 onCancelEvent(x, y, eventTime); 876 break; 877 } 878 } 879 880 public void onDownEvent(final int x, final int y, final long eventTime, 881 final KeyEventHandler handler) { 882 if (DEBUG_EVENT) { 883 printTouchEvent("onDownEvent:", x, y, eventTime); 884 } 885 mDrawingProxy = handler.getDrawingProxy(); 886 mTimerProxy = handler.getTimerProxy(); 887 setKeyboardActionListener(handler.getKeyboardActionListener()); 888 setKeyDetectorInner(handler.getKeyDetector()); 889 // Naive up-to-down noise filter. 890 final long deltaT = eventTime - mUpTime; 891 if (deltaT < sParams.mTouchNoiseThresholdTime) { 892 final int distance = getDistance(x, y, mLastX, mLastY); 893 if (distance < sParams.mTouchNoiseThresholdDistance) { 894 if (DEBUG_MODE) 895 Log.w(TAG, String.format("[%d] onDownEvent:" 896 + " ignore potential noise: time=%d distance=%d", 897 mPointerId, deltaT, distance)); 898 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 899 ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance); 900 } 901 cancelTrackingForAction(); 902 return; 903 } 904 } 905 906 final Key key = getKeyOn(x, y); 907 mBogusMoveEventDetector.onActualDownEvent(x, y); 908 if (key != null && key.isModifier()) { 909 // Before processing a down event of modifier key, all pointers already being 910 // tracked should be released. 911 sPointerTrackerQueue.releaseAllPointers(eventTime); 912 } 913 sPointerTrackerQueue.add(this); 914 onDownEventInternal(x, y, eventTime); 915 if (!sShouldHandleGesture) { 916 return; 917 } 918 // A gesture should start only from a non-modifier key. 919 mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard() 920 && key != null && !key.isModifier(); 921 if (mIsDetectingGesture) { 922 if (getActivePointerTrackerCount() == 1) { 923 sGestureFirstDownTime = eventTime; 924 } 925 mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime, 926 sTimeRecorder.getLastLetterTypingTime()); 927 } 928 } 929 930 private boolean isShowingMoreKeysPanel() { 931 return (mMoreKeysPanel != null); 932 } 933 934 private void onDownEventInternal(final int x, final int y, final long eventTime) { 935 Key key = onDownKey(x, y, eventTime); 936 // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding 937 // from modifier key, or 3) this pointer's KeyDetector always allows sliding input. 938 mIsAllowedSlidingKeyInput = sParams.mSlidingKeyInputEnabled 939 || (key != null && key.isModifier()) 940 || mKeyDetector.alwaysAllowsSlidingInput(); 941 mKeyboardLayoutHasBeenChanged = false; 942 mIsTrackingForActionDisabled = false; 943 resetSlidingKeyInput(); 944 if (key != null) { 945 // This onPress call may have changed keyboard layout. Those cases are detected at 946 // {@link #setKeyboard}. In those cases, we should update key according to the new 947 // keyboard layout. 948 if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) { 949 key = onDownKey(x, y, eventTime); 950 } 951 952 startRepeatKey(key); 953 startLongPressTimer(key); 954 setPressedKeyGraphics(key, eventTime); 955 } 956 } 957 958 private void startSlidingKeyInput(final Key key) { 959 if (!mIsInSlidingKeyInput) { 960 mIsInSlidingKeyInputFromModifier = key.isModifier(); 961 } 962 mIsInSlidingKeyInput = true; 963 } 964 965 private void resetSlidingKeyInput() { 966 mIsInSlidingKeyInput = false; 967 mIsInSlidingKeyInputFromModifier = false; 968 mDrawingProxy.dismissSlidingKeyInputPreview(); 969 } 970 971 private void onGestureMoveEvent(final int x, final int y, final long eventTime, 972 final boolean isMajorEvent, final Key key) { 973 final int gestureTime = (int)(eventTime - sGestureFirstDownTime); 974 if (mIsDetectingGesture) { 975 final int beforeLength = mGestureStrokeWithPreviewPoints.getLength(); 976 final boolean onValidArea = mGestureStrokeWithPreviewPoints.addPointOnKeyboard( 977 x, y, gestureTime, isMajorEvent); 978 if (mGestureStrokeWithPreviewPoints.getLength() > beforeLength) { 979 mTimerProxy.startUpdateBatchInputTimer(this); 980 } 981 // If the move event goes out from valid batch input area, cancel batch input. 982 if (!onValidArea) { 983 cancelBatchInput(); 984 return; 985 } 986 // If the MoreKeysPanel is showing then do not attempt to enter gesture mode. However, 987 // the gestured touch points are still being recorded in case the panel is dismissed. 988 if (isShowingMoreKeysPanel()) { 989 return; 990 } 991 mayStartBatchInput(key); 992 if (sInGesture) { 993 mayUpdateBatchInput(eventTime, key); 994 } 995 } 996 } 997 998 public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) { 999 if (DEBUG_MOVE_EVENT) { 1000 printTouchEvent("onMoveEvent:", x, y, eventTime); 1001 } 1002 if (mIsTrackingForActionDisabled) { 1003 return; 1004 } 1005 1006 if (sShouldHandleGesture && me != null) { 1007 // Add historical points to gesture path. 1008 final int pointerIndex = me.findPointerIndex(mPointerId); 1009 final int historicalSize = me.getHistorySize(); 1010 for (int h = 0; h < historicalSize; h++) { 1011 final int historicalX = (int)me.getHistoricalX(pointerIndex, h); 1012 final int historicalY = (int)me.getHistoricalY(pointerIndex, h); 1013 final long historicalTime = me.getHistoricalEventTime(h); 1014 onGestureMoveEvent(historicalX, historicalY, historicalTime, 1015 false /* isMajorEvent */, null); 1016 } 1017 } 1018 1019 if (isShowingMoreKeysPanel()) { 1020 final int translatedX = mMoreKeysPanel.translateX(x); 1021 final int translatedY = mMoreKeysPanel.translateY(y); 1022 mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime); 1023 onMoveKey(x, y); 1024 if (mIsInSlidingKeyInputFromModifier) { 1025 mDrawingProxy.showSlidingKeyInputPreview(this); 1026 } 1027 return; 1028 } 1029 onMoveEventInternal(x, y, eventTime); 1030 } 1031 1032 private void processSlidingKeyInput(final Key newKey, final int x, final int y, 1033 final long eventTime) { 1034 // This onPress call may have changed keyboard layout. Those cases are detected 1035 // at {@link #setKeyboard}. In those cases, we should update key according 1036 // to the new keyboard layout. 1037 Key key = newKey; 1038 if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) { 1039 key = onMoveKey(x, y); 1040 } 1041 onMoveToNewKey(key, x, y); 1042 if (mIsTrackingForActionDisabled) { 1043 return; 1044 } 1045 startLongPressTimer(key); 1046 setPressedKeyGraphics(key, eventTime); 1047 } 1048 1049 private void processPhantomSuddenMoveHack(final Key key, final int x, final int y, 1050 final long eventTime, final Key oldKey, final int lastX, final int lastY) { 1051 if (DEBUG_MODE) { 1052 Log.w(TAG, String.format("[%d] onMoveEvent:" 1053 + " phantom sudden move event (distance=%d) is translated to " 1054 + "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId, 1055 getDistance(x, y, lastX, lastY), 1056 lastX, lastY, Constants.printableCode(oldKey.mCode), 1057 x, y, Constants.printableCode(key.mCode))); 1058 } 1059 // TODO: This should be moved to outside of this nested if-clause? 1060 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1061 ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY); 1062 } 1063 onUpEventInternal(x, y, eventTime); 1064 onDownEventInternal(x, y, eventTime); 1065 } 1066 1067 private void processProximateBogusDownMoveUpEventHack(final Key key, final int x, final int y, 1068 final long eventTime, final Key oldKey, final int lastX, final int lastY) { 1069 if (DEBUG_MODE) { 1070 final float keyDiagonal = (float)Math.hypot( 1071 mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight); 1072 final float radiusRatio = 1073 mBogusMoveEventDetector.getDistanceFromDownEvent(x, y) 1074 / keyDiagonal; 1075 Log.w(TAG, String.format("[%d] onMoveEvent:" 1076 + " bogus down-move-up event (raidus=%.2f key diagonal) is " 1077 + " translated to up[%d,%d,%s]/down[%d,%d,%s] events", 1078 mPointerId, radiusRatio, 1079 lastX, lastY, Constants.printableCode(oldKey.mCode), 1080 x, y, Constants.printableCode(key.mCode))); 1081 } 1082 onUpEventInternal(x, y, eventTime); 1083 onDownEventInternal(x, y, eventTime); 1084 } 1085 1086 private void processSildeOutFromOldKey(final Key oldKey) { 1087 setReleasedKeyGraphics(oldKey); 1088 callListenerOnRelease(oldKey, oldKey.mCode, true /* withSliding */); 1089 startSlidingKeyInput(oldKey); 1090 mTimerProxy.cancelKeyTimers(); 1091 } 1092 1093 private void slideFromOldKeyToNewKey(final Key key, final int x, final int y, 1094 final long eventTime, final Key oldKey, final int lastX, final int lastY) { 1095 // The pointer has been slid in to the new key from the previous key, we must call 1096 // onRelease() first to notify that the previous key has been released, then call 1097 // onPress() to notify that the new key is being pressed. 1098 processSildeOutFromOldKey(oldKey); 1099 startRepeatKey(key); 1100 if (mIsAllowedSlidingKeyInput) { 1101 processSlidingKeyInput(key, x, y, eventTime); 1102 } 1103 // HACK: On some devices, quick successive touches may be reported as a sudden move by 1104 // touch panel firmware. This hack detects such cases and translates the move event to 1105 // successive up and down events. 1106 // TODO: Should find a way to balance gesture detection and this hack. 1107 else if (sNeedsPhantomSuddenMoveEventHack 1108 && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) { 1109 processPhantomSuddenMoveHack(key, x, y, eventTime, oldKey, lastX, lastY); 1110 } 1111 // HACK: On some devices, quick successive proximate touches may be reported as a bogus 1112 // down-move-up event by touch panel firmware. This hack detects such cases and breaks 1113 // these events into separate up and down events. 1114 else if (sNeedsProximateBogusDownMoveUpEventHack && sTimeRecorder.isInFastTyping(eventTime) 1115 && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) { 1116 processProximateBogusDownMoveUpEventHack(key, x, y, eventTime, oldKey, lastX, lastY); 1117 } 1118 // HACK: If there are currently multiple touches, register the key even if the finger 1119 // slides off the key. This defends against noise from some touch panels when there are 1120 // close multiple touches. 1121 // Caveat: When in chording input mode with a modifier key, we don't use this hack. 1122 else if (getActivePointerTrackerCount() > 1 1123 && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) { 1124 if (DEBUG_MODE) { 1125 Log.w(TAG, String.format("[%d] onMoveEvent:" 1126 + " detected sliding finger while multi touching", mPointerId)); 1127 } 1128 onUpEvent(x, y, eventTime); 1129 cancelTrackingForAction(); 1130 setReleasedKeyGraphics(oldKey); 1131 } else { 1132 if (!mIsDetectingGesture) { 1133 cancelTrackingForAction(); 1134 } 1135 setReleasedKeyGraphics(oldKey); 1136 } 1137 } 1138 1139 private void slideOutFromOldKey(final Key oldKey, final int x, final int y) { 1140 // The pointer has been slid out from the previous key, we must call onRelease() to 1141 // notify that the previous key has been released. 1142 processSildeOutFromOldKey(oldKey); 1143 if (mIsAllowedSlidingKeyInput) { 1144 onMoveToNewKey(null, x, y); 1145 } else { 1146 if (!mIsDetectingGesture) { 1147 cancelTrackingForAction(); 1148 } 1149 } 1150 } 1151 1152 private void onMoveEventInternal(final int x, final int y, final long eventTime) { 1153 final int lastX = mLastX; 1154 final int lastY = mLastY; 1155 final Key oldKey = mCurrentKey; 1156 final Key newKey = onMoveKey(x, y); 1157 1158 if (sShouldHandleGesture) { 1159 // Register move event on gesture tracker. 1160 onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey); 1161 if (sInGesture) { 1162 mCurrentKey = null; 1163 setReleasedKeyGraphics(oldKey); 1164 return; 1165 } 1166 } 1167 1168 if (newKey != null) { 1169 if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) { 1170 slideFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY); 1171 } else if (oldKey == null) { 1172 // The pointer has been slid in to the new key, but the finger was not on any keys. 1173 // In this case, we must call onPress() to notify that the new key is being pressed. 1174 processSlidingKeyInput(newKey, x, y, eventTime); 1175 } 1176 } else { // newKey == null 1177 if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) { 1178 slideOutFromOldKey(oldKey, x, y); 1179 } 1180 } 1181 if (mIsInSlidingKeyInputFromModifier) { 1182 mDrawingProxy.showSlidingKeyInputPreview(this); 1183 } 1184 } 1185 1186 public void onUpEvent(final int x, final int y, final long eventTime) { 1187 if (DEBUG_EVENT) { 1188 printTouchEvent("onUpEvent :", x, y, eventTime); 1189 } 1190 1191 mTimerProxy.cancelUpdateBatchInputTimer(this); 1192 if (!sInGesture) { 1193 if (mCurrentKey != null && mCurrentKey.isModifier()) { 1194 // Before processing an up event of modifier key, all pointers already being 1195 // tracked should be released. 1196 sPointerTrackerQueue.releaseAllPointersExcept(this, eventTime); 1197 } else { 1198 sPointerTrackerQueue.releaseAllPointersOlderThan(this, eventTime); 1199 } 1200 } 1201 onUpEventInternal(x, y, eventTime); 1202 sPointerTrackerQueue.remove(this); 1203 } 1204 1205 // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event. 1206 // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a 1207 // "virtual" up event. 1208 @Override 1209 public void onPhantomUpEvent(final long eventTime) { 1210 if (DEBUG_EVENT) { 1211 printTouchEvent("onPhntEvent:", mLastX, mLastY, eventTime); 1212 } 1213 if (isShowingMoreKeysPanel()) { 1214 return; 1215 } 1216 onUpEventInternal(mLastX, mLastY, eventTime); 1217 cancelTrackingForAction(); 1218 } 1219 1220 private void onUpEventInternal(final int x, final int y, final long eventTime) { 1221 mTimerProxy.cancelKeyTimers(); 1222 final boolean isInSlidingKeyInput = mIsInSlidingKeyInput; 1223 final boolean isInSlidingKeyInputFromModifier = mIsInSlidingKeyInputFromModifier; 1224 resetSlidingKeyInput(); 1225 mIsDetectingGesture = false; 1226 final Key currentKey = mCurrentKey; 1227 mCurrentKey = null; 1228 // Release the last pressed key. 1229 setReleasedKeyGraphics(currentKey); 1230 1231 if (isShowingMoreKeysPanel()) { 1232 if (!mIsTrackingForActionDisabled) { 1233 final int translatedX = mMoreKeysPanel.translateX(x); 1234 final int translatedY = mMoreKeysPanel.translateY(y); 1235 mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId, eventTime); 1236 } 1237 mMoreKeysPanel.dismissMoreKeysPanel(); 1238 mMoreKeysPanel = null; 1239 return; 1240 } 1241 1242 if (sInGesture) { 1243 if (currentKey != null) { 1244 callListenerOnRelease(currentKey, currentKey.mCode, true /* withSliding */); 1245 } 1246 mayEndBatchInput(eventTime); 1247 return; 1248 } 1249 1250 if (mIsTrackingForActionDisabled) { 1251 return; 1252 } 1253 if (currentKey != null && currentKey.isRepeatable() && !isInSlidingKeyInput) { 1254 // Repeatable key has been registered in {@link #onDownEventInternal(int,int,long)}. 1255 return; 1256 } 1257 detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime); 1258 if (isInSlidingKeyInputFromModifier) { 1259 callListenerOnFinishSlidingInput(); 1260 } 1261 } 1262 1263 public void onShowMoreKeysPanel(final int translatedX, final int translatedY, 1264 final MoreKeysPanel panel) { 1265 setReleasedKeyGraphics(mCurrentKey); 1266 final long eventTime = SystemClock.uptimeMillis(); 1267 mMoreKeysPanel = panel; 1268 mMoreKeysPanel.onDownEvent(translatedX, translatedY, mPointerId, eventTime); 1269 } 1270 1271 @Override 1272 public void cancelTrackingForAction() { 1273 if (isShowingMoreKeysPanel()) { 1274 return; 1275 } 1276 mIsTrackingForActionDisabled = true; 1277 } 1278 1279 public void onLongPressed() { 1280 resetSlidingKeyInput(); 1281 cancelTrackingForAction(); 1282 setReleasedKeyGraphics(mCurrentKey); 1283 sPointerTrackerQueue.remove(this); 1284 } 1285 1286 public void onCancelEvent(final int x, final int y, final long eventTime) { 1287 if (DEBUG_EVENT) { 1288 printTouchEvent("onCancelEvt:", x, y, eventTime); 1289 } 1290 1291 cancelBatchInput(); 1292 cancelAllPointerTrackers(); 1293 sPointerTrackerQueue.releaseAllPointers(eventTime); 1294 onCancelEventInternal(); 1295 } 1296 1297 private void onCancelEventInternal() { 1298 mTimerProxy.cancelKeyTimers(); 1299 setReleasedKeyGraphics(mCurrentKey); 1300 resetSlidingKeyInput(); 1301 if (isShowingMoreKeysPanel()) { 1302 mMoreKeysPanel.dismissMoreKeysPanel(); 1303 mMoreKeysPanel = null; 1304 } 1305 } 1306 1307 private void startRepeatKey(final Key key) { 1308 if (sInGesture) return; 1309 if (key == null) return; 1310 if (!key.isRepeatable()) return; 1311 // Don't start key repeat when we are in sliding input mode. 1312 if (mIsInSlidingKeyInput) return; 1313 detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis()); 1314 mTimerProxy.startKeyRepeatTimer(this); 1315 } 1316 1317 private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime, 1318 final Key newKey) { 1319 if (mKeyDetector == null) { 1320 throw new NullPointerException("keyboard and/or key detector not set"); 1321 } 1322 final Key curKey = mCurrentKey; 1323 if (newKey == curKey) { 1324 return false; 1325 } 1326 if (curKey == null /* && newKey != null */) { 1327 return true; 1328 } 1329 // Here curKey points to the different key from newKey. 1330 final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared( 1331 mIsInSlidingKeyInputFromModifier); 1332 final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y); 1333 if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) { 1334 if (DEBUG_MODE) { 1335 final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared) 1336 / mKeyboard.mMostCommonKeyWidth; 1337 Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:" 1338 +" %.2f key width from key edge", mPointerId, distanceToEdgeRatio)); 1339 } 1340 return true; 1341 } 1342 if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput 1343 && sTimeRecorder.isInFastTyping(eventTime) 1344 && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) { 1345 if (DEBUG_MODE) { 1346 final float keyDiagonal = (float)Math.hypot( 1347 mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight); 1348 final float lengthFromDownRatio = 1349 mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal; 1350 Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:" 1351 + " %.2f key diagonal from virtual down point", 1352 mPointerId, lengthFromDownRatio)); 1353 } 1354 return true; 1355 } 1356 return false; 1357 } 1358 1359 private void startLongPressTimer(final Key key) { 1360 if (sInGesture) return; 1361 if (key == null) return; 1362 if (!key.isLongPressEnabled()) return; 1363 // Caveat: Please note that isLongPressEnabled() can be true even if the current key 1364 // doesn't have its more keys. (e.g. spacebar, globe key) 1365 // We always need to start the long press timer if the key has its more keys regardless of 1366 // whether or not we are in the sliding input mode. 1367 if (mIsInSlidingKeyInput && key.mMoreKeys == null) return; 1368 final int delay; 1369 switch (key.mCode) { 1370 case Constants.CODE_SHIFT: 1371 delay = sParams.mLongPressShiftLockTimeout; 1372 break; 1373 default: 1374 final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout; 1375 if (mIsInSlidingKeyInputFromModifier) { 1376 // We use longer timeout for sliding finger input started from the modifier key. 1377 delay = longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT; 1378 } else { 1379 delay = longpressTimeout; 1380 } 1381 break; 1382 } 1383 mTimerProxy.startLongPressTimer(this, delay); 1384 } 1385 1386 private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) { 1387 if (key == null) { 1388 callListenerOnCancelInput(); 1389 return; 1390 } 1391 1392 final int code = key.mCode; 1393 callListenerOnCodeInput(key, code, x, y, eventTime); 1394 callListenerOnRelease(key, code, false /* withSliding */); 1395 } 1396 1397 private void printTouchEvent(final String title, final int x, final int y, 1398 final long eventTime) { 1399 final Key key = mKeyDetector.detectHitKey(x, y); 1400 final String code = KeyDetector.printableCode(key); 1401 Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId, 1402 (mIsTrackingForActionDisabled ? "-" : " "), title, x, y, eventTime, code)); 1403 } 1404} 1405