PointerTracker.java revision baf83886be975d804eda3e1519b7255026e5163e
1/* 2 * Copyright (C) 2010 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.keyboard; 18 19import com.android.inputmethod.keyboard.KeyboardView.UIHandler; 20import com.android.inputmethod.latin.R; 21 22import android.content.res.Resources; 23import android.util.Log; 24import android.view.MotionEvent; 25 26import java.util.Arrays; 27 28public class PointerTracker { 29 private static final String TAG = PointerTracker.class.getSimpleName(); 30 private static final boolean ENABLE_ASSERTION = false; 31 private static final boolean DEBUG_EVENT = false; 32 private static final boolean DEBUG_MOVE_EVENT = false; 33 private static final boolean DEBUG_LISTENER = false; 34 35 public interface UIProxy { 36 public void invalidateKey(Key key); 37 public void showPreview(int keyIndex, PointerTracker tracker); 38 public boolean hasDistinctMultitouch(); 39 } 40 41 public final int mPointerId; 42 43 // Timing constants 44 private final int mDelayBeforeKeyRepeatStart; 45 private final int mLongPressKeyTimeout; 46 private final int mLongPressShiftKeyTimeout; 47 48 // Miscellaneous constants 49 private static final int NOT_A_KEY = KeyDetector.NOT_A_KEY; 50 51 private final UIProxy mProxy; 52 private final UIHandler mHandler; 53 private final KeyDetector mKeyDetector; 54 private KeyboardActionListener mListener = EMPTY_LISTENER; 55 private final KeyboardSwitcher mKeyboardSwitcher; 56 private final boolean mHasDistinctMultitouch; 57 private final boolean mConfigSlidingKeyInputEnabled; 58 59 private final int mTouchNoiseThresholdMillis; 60 private final int mTouchNoiseThresholdDistanceSquared; 61 62 private Keyboard mKeyboard; 63 private Key[] mKeys; 64 private int mKeyHysteresisDistanceSquared = -1; 65 66 private final PointerTrackerKeyState mKeyState; 67 68 // true if event is already translated to a key action (long press or mini-keyboard) 69 private boolean mKeyAlreadyProcessed; 70 71 // true if this pointer is repeatable key 72 private boolean mIsRepeatableKey; 73 74 // true if this pointer is in sliding key input 75 private boolean mIsInSlidingKeyInput; 76 77 // true if sliding key is allowed. 78 private boolean mIsAllowedSlidingKeyInput; 79 80 // pressed key 81 private int mPreviousKey = NOT_A_KEY; 82 83 // Empty {@link KeyboardActionListener} 84 private static final KeyboardActionListener EMPTY_LISTENER = new KeyboardActionListener() { 85 @Override 86 public void onPress(int primaryCode) {} 87 @Override 88 public void onRelease(int primaryCode) {} 89 @Override 90 public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {} 91 @Override 92 public void onTextInput(CharSequence text) {} 93 @Override 94 public void onCancelInput() {} 95 @Override 96 public void onSwipeDown() {} 97 }; 98 99 public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy, 100 Resources res) { 101 if (proxy == null || handler == null || keyDetector == null) 102 throw new NullPointerException(); 103 mPointerId = id; 104 mProxy = proxy; 105 mHandler = handler; 106 mKeyDetector = keyDetector; 107 mKeyboardSwitcher = KeyboardSwitcher.getInstance(); 108 mKeyState = new PointerTrackerKeyState(keyDetector); 109 mHasDistinctMultitouch = proxy.hasDistinctMultitouch(); 110 mConfigSlidingKeyInputEnabled = res.getBoolean(R.bool.config_sliding_key_input_enabled); 111 mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start); 112 mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout); 113 mLongPressShiftKeyTimeout = res.getInteger(R.integer.config_long_press_shift_key_timeout); 114 mTouchNoiseThresholdMillis = res.getInteger(R.integer.config_touch_noise_threshold_millis); 115 final float touchNoiseThresholdDistance = res.getDimension( 116 R.dimen.config_touch_noise_threshold_distance); 117 mTouchNoiseThresholdDistanceSquared = (int)( 118 touchNoiseThresholdDistance * touchNoiseThresholdDistance); 119 } 120 121 public void setOnKeyboardActionListener(KeyboardActionListener listener) { 122 mListener = listener; 123 } 124 125 private void callListenerOnPress(int primaryCode) { 126 if (DEBUG_LISTENER) 127 Log.d(TAG, "onPress : " + keyCodePrintable(primaryCode)); 128 mListener.onPress(primaryCode); 129 } 130 131 private void callListenerOnCodeInput(int primaryCode, int[] keyCodes, int x, int y) { 132 if (DEBUG_LISTENER) 133 Log.d(TAG, "onCodeInput: " + keyCodePrintable(primaryCode) 134 + " codes="+ Arrays.toString(keyCodes) + " x=" + x + " y=" + y); 135 mListener.onCodeInput(primaryCode, keyCodes, x, y); 136 } 137 138 private void callListenerOnTextInput(CharSequence text) { 139 if (DEBUG_LISTENER) 140 Log.d(TAG, "onTextInput: text=" + text); 141 mListener.onTextInput(text); 142 } 143 144 private void callListenerOnRelease(int primaryCode) { 145 if (DEBUG_LISTENER) 146 Log.d(TAG, "onRelease : " + keyCodePrintable(primaryCode)); 147 mListener.onRelease(primaryCode); 148 } 149 150 private void callListenerOnCancelInput() { 151 if (DEBUG_LISTENER) 152 Log.d(TAG, "onCancelInput"); 153 mListener.onCancelInput(); 154 } 155 156 public void setKeyboard(Keyboard keyboard, Key[] keys, float keyHysteresisDistance) { 157 if (keyboard == null || keys == null || keyHysteresisDistance < 0) 158 throw new IllegalArgumentException(); 159 mKeyboard = keyboard; 160 mKeys = keys; 161 mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance); 162 // Update current key index because keyboard layout has been changed. 163 mKeyState.onSetKeyboard(); 164 } 165 166 public boolean isInSlidingKeyInput() { 167 return mIsInSlidingKeyInput; 168 } 169 170 private boolean isValidKeyIndex(int keyIndex) { 171 return keyIndex >= 0 && keyIndex < mKeys.length; 172 } 173 174 public Key getKey(int keyIndex) { 175 return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null; 176 } 177 178 private static boolean isModifierCode(int primaryCode) { 179 return primaryCode == Keyboard.CODE_SHIFT 180 || primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL; 181 } 182 183 private boolean isModifierInternal(int keyIndex) { 184 final Key key = getKey(keyIndex); 185 return key == null ? false : isModifierCode(key.mCode); 186 } 187 188 public boolean isModifier() { 189 return isModifierInternal(mKeyState.getKeyIndex()); 190 } 191 192 private boolean isOnModifierKey(int x, int y) { 193 return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null)); 194 } 195 196 public boolean isOnShiftKey(int x, int y) { 197 final Key key = getKey(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null)); 198 return key != null && key.mCode == Keyboard.CODE_SHIFT; 199 } 200 201 public boolean isSpaceKey(int keyIndex) { 202 Key key = getKey(keyIndex); 203 return key != null && key.mCode == Keyboard.CODE_SPACE; 204 } 205 206 public void releaseKey() { 207 updateKeyGraphics(NOT_A_KEY); 208 } 209 210 private void updateKeyGraphics(int keyIndex) { 211 int oldKeyIndex = mPreviousKey; 212 mPreviousKey = keyIndex; 213 if (keyIndex != oldKeyIndex) { 214 if (isValidKeyIndex(oldKeyIndex)) { 215 // if new key index is not a key, old key was just released inside of the key. 216 final boolean inside = (keyIndex == NOT_A_KEY); 217 mKeys[oldKeyIndex].onReleased(inside); 218 mProxy.invalidateKey(mKeys[oldKeyIndex]); 219 } 220 if (isValidKeyIndex(keyIndex)) { 221 mKeys[keyIndex].onPressed(); 222 mProxy.invalidateKey(mKeys[keyIndex]); 223 } 224 } 225 } 226 227 public void setAlreadyProcessed() { 228 mKeyAlreadyProcessed = true; 229 } 230 231 private void checkAssertion(PointerTrackerQueue queue) { 232 if (mHasDistinctMultitouch && queue == null) 233 throw new RuntimeException( 234 "PointerTrackerQueue must be passed on distinct multi touch device"); 235 if (!mHasDistinctMultitouch && queue != null) 236 throw new RuntimeException( 237 "PointerTrackerQueue must be null on non-distinct multi touch device"); 238 } 239 240 public void onTouchEvent(int action, int x, int y, long eventTime, PointerTrackerQueue queue) { 241 switch (action) { 242 case MotionEvent.ACTION_MOVE: 243 onMoveEvent(x, y, eventTime, queue); 244 break; 245 case MotionEvent.ACTION_DOWN: 246 case MotionEvent.ACTION_POINTER_DOWN: 247 onDownEvent(x, y, eventTime, queue); 248 break; 249 case MotionEvent.ACTION_UP: 250 case MotionEvent.ACTION_POINTER_UP: 251 onUpEvent(x, y, eventTime, queue); 252 break; 253 case MotionEvent.ACTION_CANCEL: 254 onCancelEvent(x, y, eventTime, queue); 255 break; 256 } 257 } 258 259 public void onDownEvent(int x, int y, long eventTime, PointerTrackerQueue queue) { 260 if (ENABLE_ASSERTION) checkAssertion(queue); 261 if (DEBUG_EVENT) 262 printTouchEvent("onDownEvent:", x, y, eventTime); 263 264 // TODO: up-to-down filter, if (down-up) is less than threshold, removeMessage(UP, this) in 265 // Handler, and just ignore this down event. 266 // TODO: down-to-up filter, just record down time. do not enqueue pointer now. 267 268 // Naive up-to-down noise filter. 269 final long deltaT = eventTime - mKeyState.getUpTime(); 270 if (deltaT < mTouchNoiseThresholdMillis) { 271 final int dx = x - mKeyState.getLastX(); 272 final int dy = y - mKeyState.getLastY(); 273 final int distanceSquared = (dx * dx + dy * dy); 274 if (distanceSquared < mTouchNoiseThresholdDistanceSquared) { 275 Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT 276 + " distance=" + distanceSquared); 277 setAlreadyProcessed(); 278 return; 279 } 280 } 281 282 if (queue != null) { 283 if (isOnModifierKey(x, y)) { 284 // Before processing a down event of modifier key, all pointers already being 285 // tracked should be released. 286 queue.releaseAllPointers(eventTime); 287 } 288 queue.add(this); 289 } 290 onDownEventInternal(x, y, eventTime); 291 } 292 293 private void onDownEventInternal(int x, int y, long eventTime) { 294 int keyIndex = mKeyState.onDownKey(x, y, eventTime); 295 // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding 296 // from modifier key, or 3) this pointer is on mini-keyboard. 297 mIsAllowedSlidingKeyInput = mConfigSlidingKeyInputEnabled || isModifierInternal(keyIndex) 298 || mKeyDetector instanceof MiniKeyboardKeyDetector; 299 mKeyAlreadyProcessed = false; 300 mIsRepeatableKey = false; 301 mIsInSlidingKeyInput = false; 302 if (isValidKeyIndex(keyIndex)) { 303 callListenerOnPress(mKeys[keyIndex].mCode); 304 // This onPress call may have changed keyboard layout and have updated mKeyIndex. 305 // If that's the case, mKeyIndex has been updated in setKeyboard(). 306 keyIndex = mKeyState.getKeyIndex(); 307 } 308 if (isValidKeyIndex(keyIndex)) { 309 if (mKeys[keyIndex].mRepeatable) { 310 repeatKey(keyIndex); 311 mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this); 312 mIsRepeatableKey = true; 313 } 314 startLongPressTimer(keyIndex); 315 } 316 showKeyPreviewAndUpdateKeyGraphics(keyIndex); 317 } 318 319 public void onMoveEvent(int x, int y, long eventTime, PointerTrackerQueue queue) { 320 if (ENABLE_ASSERTION) checkAssertion(queue); 321 if (DEBUG_MOVE_EVENT) 322 printTouchEvent("onMoveEvent:", x, y, eventTime); 323 if (mKeyAlreadyProcessed) 324 return; 325 final PointerTrackerKeyState keyState = mKeyState; 326 327 // TODO: down-to-up filter, if (eventTime-downTime) is less than threshold, just ignore 328 // this move event. Otherwise fire {@link onDownEventInternal} and continue. 329 330 final int keyIndex = keyState.onMoveKey(x, y); 331 final Key oldKey = getKey(keyState.getKeyIndex()); 332 if (isValidKeyIndex(keyIndex)) { 333 if (oldKey == null) { 334 // The pointer has been slid in to the new key, but the finger was not on any keys. 335 // In this case, we must call onPress() to notify that the new key is being pressed. 336 callListenerOnPress(getKey(keyIndex).mCode); 337 keyState.onMoveToNewKey(keyIndex, x, y); 338 startLongPressTimer(keyIndex); 339 } else if (!isMinorMoveBounce(x, y, keyIndex)) { 340 // The pointer has been slid in to the new key from the previous key, we must call 341 // onRelease() first to notify that the previous key has been released, then call 342 // onPress() to notify that the new key is being pressed. 343 mIsInSlidingKeyInput = true; 344 callListenerOnRelease(oldKey.mCode); 345 mHandler.cancelLongPressTimers(); 346 if (mIsAllowedSlidingKeyInput) { 347 callListenerOnPress(getKey(keyIndex).mCode); 348 keyState.onMoveToNewKey(keyIndex, x, y); 349 startLongPressTimer(keyIndex); 350 } else { 351 setAlreadyProcessed(); 352 showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); 353 return; 354 } 355 } 356 } else { 357 if (oldKey != null && !isMinorMoveBounce(x, y, keyIndex)) { 358 // The pointer has been slid out from the previous key, we must call onRelease() to 359 // notify that the previous key has been released. 360 mIsInSlidingKeyInput = true; 361 callListenerOnRelease(oldKey.mCode); 362 mHandler.cancelLongPressTimers(); 363 if (mIsAllowedSlidingKeyInput) { 364 keyState.onMoveToNewKey(keyIndex, x ,y); 365 } else { 366 setAlreadyProcessed(); 367 showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); 368 return; 369 } 370 } 371 } 372 showKeyPreviewAndUpdateKeyGraphics(mKeyState.getKeyIndex()); 373 } 374 375 // TODO: up-to-down filter, if delayed UP message is fired, invoke {@link onUpEventInternal}. 376 377 public void onUpEvent(int x, int y, long eventTime, PointerTrackerQueue queue) { 378 if (ENABLE_ASSERTION) checkAssertion(queue); 379 if (DEBUG_EVENT) 380 printTouchEvent("onUpEvent :", x, y, eventTime); 381 382 // TODO: up-to-down filter, just sendDelayedMessage(UP, this) to Handler. 383 // TODO: down-to-up filter, if (eventTime-downTime) is less than threshold, just ignore 384 // this up event. Otherwise fire {@link onDownEventInternal} and {@link onUpEventInternal}. 385 386 if (queue != null) { 387 if (isModifier()) { 388 // Before processing an up event of modifier key, all pointers already being 389 // tracked should be released. 390 queue.releaseAllPointersExcept(this, eventTime); 391 } else { 392 queue.releaseAllPointersOlderThan(this, eventTime); 393 } 394 queue.remove(this); 395 } 396 onUpEventInternal(x, y, eventTime); 397 } 398 399 public void onUpEventForRelease(int x, int y, long eventTime) { 400 onUpEventInternal(x, y, eventTime); 401 } 402 403 private void onUpEventInternal(int pointX, int pointY, long eventTime) { 404 int x = pointX; 405 int y = pointY; 406 mHandler.cancelKeyTimers(); 407 mHandler.cancelPopupPreview(); 408 showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); 409 mIsInSlidingKeyInput = false; 410 if (mKeyAlreadyProcessed) 411 return; 412 final PointerTrackerKeyState keyState = mKeyState; 413 int keyIndex = keyState.onUpKey(x, y, eventTime); 414 if (isMinorMoveBounce(x, y, keyIndex)) { 415 // Use previous fixed key index and coordinates. 416 keyIndex = keyState.getKeyIndex(); 417 x = keyState.getKeyX(); 418 y = keyState.getKeyY(); 419 } 420 if (!mIsRepeatableKey) { 421 detectAndSendKey(keyIndex, x, y); 422 } 423 424 if (isValidKeyIndex(keyIndex)) 425 mProxy.invalidateKey(mKeys[keyIndex]); 426 } 427 428 public void onCancelEvent(int x, int y, long eventTime, PointerTrackerQueue queue) { 429 if (ENABLE_ASSERTION) checkAssertion(queue); 430 if (DEBUG_EVENT) 431 printTouchEvent("onCancelEvt:", x, y, eventTime); 432 433 if (queue != null) 434 queue.remove(this); 435 onCancelEventInternal(); 436 } 437 438 private void onCancelEventInternal() { 439 mHandler.cancelKeyTimers(); 440 mHandler.cancelPopupPreview(); 441 showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); 442 mIsInSlidingKeyInput = false; 443 int keyIndex = mKeyState.getKeyIndex(); 444 if (isValidKeyIndex(keyIndex)) 445 mProxy.invalidateKey(mKeys[keyIndex]); 446 } 447 448 public void repeatKey(int keyIndex) { 449 Key key = getKey(keyIndex); 450 if (key != null) { 451 detectAndSendKey(keyIndex, key.mX, key.mY); 452 } 453 } 454 455 public int getLastX() { 456 return mKeyState.getLastX(); 457 } 458 459 public int getLastY() { 460 return mKeyState.getLastY(); 461 } 462 463 public long getDownTime() { 464 return mKeyState.getDownTime(); 465 } 466 467 // These package scope methods are only for debugging purpose. 468 /* package */ int getStartX() { 469 return mKeyState.getStartX(); 470 } 471 472 /* package */ int getStartY() { 473 return mKeyState.getStartY(); 474 } 475 476 private boolean isMinorMoveBounce(int x, int y, int newKey) { 477 if (mKeys == null || mKeyHysteresisDistanceSquared < 0) 478 throw new IllegalStateException("keyboard and/or hysteresis not set"); 479 int curKey = mKeyState.getKeyIndex(); 480 if (newKey == curKey) { 481 return true; 482 } else if (isValidKeyIndex(curKey)) { 483 return mKeys[curKey].squaredDistanceToEdge(x, y) < mKeyHysteresisDistanceSquared; 484 } else { 485 return false; 486 } 487 } 488 489 private void showKeyPreviewAndUpdateKeyGraphics(int keyIndex) { 490 updateKeyGraphics(keyIndex); 491 // The modifier key, such as shift key, should not be shown as preview when multi-touch is 492 // supported. On the other hand, if multi-touch is not supported, the modifier key should 493 // be shown as preview. 494 if (mHasDistinctMultitouch && isModifier()) { 495 mProxy.showPreview(NOT_A_KEY, this); 496 } else { 497 mProxy.showPreview(keyIndex, this); 498 } 499 } 500 501 private void startLongPressTimer(int keyIndex) { 502 Key key = getKey(keyIndex); 503 if (key.mCode == Keyboard.CODE_SHIFT) { 504 mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this); 505 } else if (key.mManualTemporaryUpperCaseCode != Keyboard.CODE_DUMMY 506 && mKeyboard.isManualTemporaryUpperCase()) { 507 // We need not start long press timer on the key which has manual temporary upper case 508 // code defined and the keyboard is in manual temporary upper case mode. 509 return; 510 } else if (mKeyboardSwitcher.isInMomentaryAutoModeSwitchState()) { 511 // We use longer timeout for sliding finger input started from the symbols mode key. 512 mHandler.startLongPressTimer(mLongPressKeyTimeout * 2, keyIndex, this); 513 } else { 514 mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this); 515 } 516 } 517 518 private void detectAndSendKey(int index, int x, int y) { 519 final Key key = getKey(index); 520 if (key == null) { 521 callListenerOnCancelInput(); 522 return; 523 } 524 if (key.mOutputText != null) { 525 callListenerOnTextInput(key.mOutputText); 526 callListenerOnRelease(key.mCode); 527 } else { 528 int code = key.mCode; 529 final int[] codes = mKeyDetector.newCodeArray(); 530 mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes); 531 532 // If keyboard is in manual temporary upper case state and key has manual temporary 533 // shift code, alternate character code should be sent. 534 if (mKeyboard.isManualTemporaryUpperCase() 535 && key.mManualTemporaryUpperCaseCode != Keyboard.CODE_DUMMY) { 536 code = key.mManualTemporaryUpperCaseCode; 537 codes[0] = code; 538 } 539 540 // Swap the first and second values in the codes array if the primary code is not the 541 // first value but the second value in the array. This happens when key debouncing is 542 // in effect. 543 if (codes.length >= 2 && codes[0] != code && codes[1] == code) { 544 codes[1] = codes[0]; 545 codes[0] = code; 546 } 547 callListenerOnCodeInput(code, codes, x, y); 548 callListenerOnRelease(code); 549 } 550 } 551 552 public CharSequence getPreviewText(Key key) { 553 return key.mLabel; 554 } 555 556 private long mPreviousEventTime; 557 558 private void printTouchEvent(String title, int x, int y, long eventTime) { 559 final int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null); 560 final Key key = getKey(keyIndex); 561 final String code = (key == null) ? "----" : keyCodePrintable(key.mCode); 562 final long delta = eventTime - mPreviousEventTime; 563 Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %3d(%s)", title, 564 (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, keyIndex, code)); 565 mPreviousEventTime = eventTime; 566 } 567 568 private static String keyCodePrintable(int primaryCode) { 569 final String modifier = isModifierCode(primaryCode) ? " modifier" : ""; 570 return String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode) + modifier; 571 } 572} 573