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 17 package com.android.inputmethod.latin; 18 19 import com.android.inputmethod.latin.LatinKeyboardBaseView.OnKeyboardActionListener; 20 import com.android.inputmethod.latin.LatinKeyboardBaseView.UIHandler; 21 22 import android.content.res.Resources; 23 import android.inputmethodservice.Keyboard; 24 import android.inputmethodservice.Keyboard.Key; 25 import android.util.Log; 26 import android.view.MotionEvent; 27 28 public class PointerTracker { 29 private static final String TAG = "PointerTracker"; 30 private static final boolean DEBUG = false; 31 private static final boolean DEBUG_MOVE = false; 32 33 public interface UIProxy { invalidateKey(Key key)34 public void invalidateKey(Key key); showPreview(int keyIndex, PointerTracker tracker)35 public void showPreview(int keyIndex, PointerTracker tracker); hasDistinctMultitouch()36 public boolean hasDistinctMultitouch(); 37 } 38 39 public final int mPointerId; 40 41 // Timing constants 42 private final int mDelayBeforeKeyRepeatStart; 43 private final int mLongPressKeyTimeout; 44 private final int mMultiTapKeyTimeout; 45 46 // Miscellaneous constants 47 private static final int NOT_A_KEY = LatinKeyboardBaseView.NOT_A_KEY; 48 private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; 49 50 private final UIProxy mProxy; 51 private final UIHandler mHandler; 52 private final KeyDetector mKeyDetector; 53 private OnKeyboardActionListener mListener; 54 private final KeyboardSwitcher mKeyboardSwitcher; 55 private final boolean mHasDistinctMultitouch; 56 57 private Key[] mKeys; 58 private int mKeyHysteresisDistanceSquared = -1; 59 60 private final KeyState mKeyState; 61 62 // true if keyboard layout has been changed. 63 private boolean mKeyboardLayoutHasBeenChanged; 64 65 // true if event is already translated to a key action (long press or mini-keyboard) 66 private boolean mKeyAlreadyProcessed; 67 68 // true if this pointer is repeatable key 69 private boolean mIsRepeatableKey; 70 71 // true if this pointer is in sliding key input 72 private boolean mIsInSlidingKeyInput; 73 74 // For multi-tap 75 private int mLastSentIndex; 76 private int mTapCount; 77 private long mLastTapTime; 78 private boolean mInMultiTap; 79 private final StringBuilder mPreviewLabel = new StringBuilder(1); 80 81 // pressed key 82 private int mPreviousKey = NOT_A_KEY; 83 84 // This class keeps track of a key index and a position where this pointer is. 85 private static class KeyState { 86 private final KeyDetector mKeyDetector; 87 88 // The position and time at which first down event occurred. 89 private int mStartX; 90 private int mStartY; 91 private long mDownTime; 92 93 // The current key index where this pointer is. 94 private int mKeyIndex = NOT_A_KEY; 95 // The position where mKeyIndex was recognized for the first time. 96 private int mKeyX; 97 private int mKeyY; 98 99 // Last pointer position. 100 private int mLastX; 101 private int mLastY; 102 KeyState(KeyDetector keyDetecor)103 public KeyState(KeyDetector keyDetecor) { 104 mKeyDetector = keyDetecor; 105 } 106 getKeyIndex()107 public int getKeyIndex() { 108 return mKeyIndex; 109 } 110 getKeyX()111 public int getKeyX() { 112 return mKeyX; 113 } 114 getKeyY()115 public int getKeyY() { 116 return mKeyY; 117 } 118 getStartX()119 public int getStartX() { 120 return mStartX; 121 } 122 getStartY()123 public int getStartY() { 124 return mStartY; 125 } 126 getDownTime()127 public long getDownTime() { 128 return mDownTime; 129 } 130 getLastX()131 public int getLastX() { 132 return mLastX; 133 } 134 getLastY()135 public int getLastY() { 136 return mLastY; 137 } 138 onDownKey(int x, int y, long eventTime)139 public int onDownKey(int x, int y, long eventTime) { 140 mStartX = x; 141 mStartY = y; 142 mDownTime = eventTime; 143 144 return onMoveToNewKey(onMoveKeyInternal(x, y), x, y); 145 } 146 onMoveKeyInternal(int x, int y)147 private int onMoveKeyInternal(int x, int y) { 148 mLastX = x; 149 mLastY = y; 150 return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null); 151 } 152 onMoveKey(int x, int y)153 public int onMoveKey(int x, int y) { 154 return onMoveKeyInternal(x, y); 155 } 156 onMoveToNewKey(int keyIndex, int x, int y)157 public int onMoveToNewKey(int keyIndex, int x, int y) { 158 mKeyIndex = keyIndex; 159 mKeyX = x; 160 mKeyY = y; 161 return keyIndex; 162 } 163 onUpKey(int x, int y)164 public int onUpKey(int x, int y) { 165 return onMoveKeyInternal(x, y); 166 } 167 } 168 PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy, Resources res)169 public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy, 170 Resources res) { 171 if (proxy == null || handler == null || keyDetector == null) 172 throw new NullPointerException(); 173 mPointerId = id; 174 mProxy = proxy; 175 mHandler = handler; 176 mKeyDetector = keyDetector; 177 mKeyboardSwitcher = KeyboardSwitcher.getInstance(); 178 mKeyState = new KeyState(keyDetector); 179 mHasDistinctMultitouch = proxy.hasDistinctMultitouch(); 180 mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start); 181 mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout); 182 mMultiTapKeyTimeout = res.getInteger(R.integer.config_multi_tap_key_timeout); 183 resetMultiTap(); 184 } 185 setOnKeyboardActionListener(OnKeyboardActionListener listener)186 public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { 187 mListener = listener; 188 } 189 setKeyboard(Key[] keys, float keyHysteresisDistance)190 public void setKeyboard(Key[] keys, float keyHysteresisDistance) { 191 if (keys == null || keyHysteresisDistance < 0) 192 throw new IllegalArgumentException(); 193 mKeys = keys; 194 mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance); 195 // Mark that keyboard layout has been changed. 196 mKeyboardLayoutHasBeenChanged = true; 197 } 198 isInSlidingKeyInput()199 public boolean isInSlidingKeyInput() { 200 return mIsInSlidingKeyInput; 201 } 202 isValidKeyIndex(int keyIndex)203 private boolean isValidKeyIndex(int keyIndex) { 204 return keyIndex >= 0 && keyIndex < mKeys.length; 205 } 206 getKey(int keyIndex)207 public Key getKey(int keyIndex) { 208 return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null; 209 } 210 isModifierInternal(int keyIndex)211 private boolean isModifierInternal(int keyIndex) { 212 Key key = getKey(keyIndex); 213 if (key == null) 214 return false; 215 int primaryCode = key.codes[0]; 216 return primaryCode == Keyboard.KEYCODE_SHIFT 217 || primaryCode == Keyboard.KEYCODE_MODE_CHANGE; 218 } 219 isModifier()220 public boolean isModifier() { 221 return isModifierInternal(mKeyState.getKeyIndex()); 222 } 223 isOnModifierKey(int x, int y)224 public boolean isOnModifierKey(int x, int y) { 225 return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null)); 226 } 227 isSpaceKey(int keyIndex)228 public boolean isSpaceKey(int keyIndex) { 229 Key key = getKey(keyIndex); 230 return key != null && key.codes[0] == LatinIME.KEYCODE_SPACE; 231 } 232 updateKey(int keyIndex)233 public void updateKey(int keyIndex) { 234 if (mKeyAlreadyProcessed) 235 return; 236 int oldKeyIndex = mPreviousKey; 237 mPreviousKey = keyIndex; 238 if (keyIndex != oldKeyIndex) { 239 if (isValidKeyIndex(oldKeyIndex)) { 240 // if new key index is not a key, old key was just released inside of the key. 241 final boolean inside = (keyIndex == NOT_A_KEY); 242 mKeys[oldKeyIndex].onReleased(inside); 243 mProxy.invalidateKey(mKeys[oldKeyIndex]); 244 } 245 if (isValidKeyIndex(keyIndex)) { 246 mKeys[keyIndex].onPressed(); 247 mProxy.invalidateKey(mKeys[keyIndex]); 248 } 249 } 250 } 251 setAlreadyProcessed()252 public void setAlreadyProcessed() { 253 mKeyAlreadyProcessed = true; 254 } 255 onTouchEvent(int action, int x, int y, long eventTime)256 public void onTouchEvent(int action, int x, int y, long eventTime) { 257 switch (action) { 258 case MotionEvent.ACTION_MOVE: 259 onMoveEvent(x, y, eventTime); 260 break; 261 case MotionEvent.ACTION_DOWN: 262 case MotionEvent.ACTION_POINTER_DOWN: 263 onDownEvent(x, y, eventTime); 264 break; 265 case MotionEvent.ACTION_UP: 266 case MotionEvent.ACTION_POINTER_UP: 267 onUpEvent(x, y, eventTime); 268 break; 269 case MotionEvent.ACTION_CANCEL: 270 onCancelEvent(x, y, eventTime); 271 break; 272 } 273 } 274 onDownEvent(int x, int y, long eventTime)275 public void onDownEvent(int x, int y, long eventTime) { 276 if (DEBUG) 277 debugLog("onDownEvent:", x, y); 278 int keyIndex = mKeyState.onDownKey(x, y, eventTime); 279 mKeyboardLayoutHasBeenChanged = false; 280 mKeyAlreadyProcessed = false; 281 mIsRepeatableKey = false; 282 mIsInSlidingKeyInput = false; 283 checkMultiTap(eventTime, keyIndex); 284 if (mListener != null) { 285 if (isValidKeyIndex(keyIndex)) { 286 mListener.onPress(mKeys[keyIndex].codes[0]); 287 // This onPress call may have changed keyboard layout. Those cases are detected at 288 // {@link #setKeyboard}. In those cases, we should update keyIndex according to the 289 // new keyboard layout. 290 if (mKeyboardLayoutHasBeenChanged) { 291 mKeyboardLayoutHasBeenChanged = false; 292 keyIndex = mKeyState.onDownKey(x, y, eventTime); 293 } 294 } 295 } 296 if (isValidKeyIndex(keyIndex)) { 297 if (mKeys[keyIndex].repeatable) { 298 repeatKey(keyIndex); 299 mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this); 300 mIsRepeatableKey = true; 301 } 302 startLongPressTimer(keyIndex); 303 } 304 showKeyPreviewAndUpdateKey(keyIndex); 305 } 306 onMoveEvent(int x, int y, long eventTime)307 public void onMoveEvent(int x, int y, long eventTime) { 308 if (DEBUG_MOVE) 309 debugLog("onMoveEvent:", x, y); 310 if (mKeyAlreadyProcessed) 311 return; 312 final KeyState keyState = mKeyState; 313 int keyIndex = keyState.onMoveKey(x, y); 314 final Key oldKey = getKey(keyState.getKeyIndex()); 315 if (isValidKeyIndex(keyIndex)) { 316 if (oldKey == null) { 317 // The pointer has been slid in to the new key, but the finger was not on any keys. 318 // In this case, we must call onPress() to notify that the new key is being pressed. 319 if (mListener != null) { 320 mListener.onPress(getKey(keyIndex).codes[0]); 321 // This onPress call may have changed keyboard layout. Those cases are detected 322 // at {@link #setKeyboard}. In those cases, we should update keyIndex according 323 // to the new keyboard layout. 324 if (mKeyboardLayoutHasBeenChanged) { 325 mKeyboardLayoutHasBeenChanged = false; 326 keyIndex = keyState.onMoveKey(x, y); 327 } 328 } 329 keyState.onMoveToNewKey(keyIndex, x, y); 330 startLongPressTimer(keyIndex); 331 } else if (!isMinorMoveBounce(x, y, keyIndex)) { 332 // The pointer has been slid in to the new key from the previous key, we must call 333 // onRelease() first to notify that the previous key has been released, then call 334 // onPress() to notify that the new key is being pressed. 335 mIsInSlidingKeyInput = true; 336 if (mListener != null) 337 mListener.onRelease(oldKey.codes[0]); 338 resetMultiTap(); 339 if (mListener != null) { 340 mListener.onPress(getKey(keyIndex).codes[0]); 341 // This onPress call may have changed keyboard layout. Those cases are detected 342 // at {@link #setKeyboard}. In those cases, we should update keyIndex according 343 // to the new keyboard layout. 344 if (mKeyboardLayoutHasBeenChanged) { 345 mKeyboardLayoutHasBeenChanged = false; 346 keyIndex = keyState.onMoveKey(x, y); 347 } 348 } 349 keyState.onMoveToNewKey(keyIndex, x, y); 350 startLongPressTimer(keyIndex); 351 } 352 } else { 353 if (oldKey != null && !isMinorMoveBounce(x, y, keyIndex)) { 354 // The pointer has been slid out from the previous key, we must call onRelease() to 355 // notify that the previous key has been released. 356 mIsInSlidingKeyInput = true; 357 if (mListener != null) 358 mListener.onRelease(oldKey.codes[0]); 359 resetMultiTap(); 360 keyState.onMoveToNewKey(keyIndex, x ,y); 361 mHandler.cancelLongPressTimer(); 362 } 363 } 364 showKeyPreviewAndUpdateKey(keyState.getKeyIndex()); 365 } 366 onUpEvent(int x, int y, long eventTime)367 public void onUpEvent(int x, int y, long eventTime) { 368 if (DEBUG) 369 debugLog("onUpEvent :", x, y); 370 mHandler.cancelKeyTimers(); 371 mHandler.cancelPopupPreview(); 372 showKeyPreviewAndUpdateKey(NOT_A_KEY); 373 mIsInSlidingKeyInput = false; 374 if (mKeyAlreadyProcessed) 375 return; 376 int keyIndex = mKeyState.onUpKey(x, y); 377 if (isMinorMoveBounce(x, y, keyIndex)) { 378 // Use previous fixed key index and coordinates. 379 keyIndex = mKeyState.getKeyIndex(); 380 x = mKeyState.getKeyX(); 381 y = mKeyState.getKeyY(); 382 } 383 if (!mIsRepeatableKey) { 384 detectAndSendKey(keyIndex, x, y, eventTime); 385 } 386 387 if (isValidKeyIndex(keyIndex)) 388 mProxy.invalidateKey(mKeys[keyIndex]); 389 } 390 onCancelEvent(int x, int y, long eventTime)391 public void onCancelEvent(int x, int y, long eventTime) { 392 if (DEBUG) 393 debugLog("onCancelEvt:", x, y); 394 mHandler.cancelKeyTimers(); 395 mHandler.cancelPopupPreview(); 396 showKeyPreviewAndUpdateKey(NOT_A_KEY); 397 mIsInSlidingKeyInput = false; 398 int keyIndex = mKeyState.getKeyIndex(); 399 if (isValidKeyIndex(keyIndex)) 400 mProxy.invalidateKey(mKeys[keyIndex]); 401 } 402 repeatKey(int keyIndex)403 public void repeatKey(int keyIndex) { 404 Key key = getKey(keyIndex); 405 if (key != null) { 406 // While key is repeating, because there is no need to handle multi-tap key, we can 407 // pass -1 as eventTime argument. 408 detectAndSendKey(keyIndex, key.x, key.y, -1); 409 } 410 } 411 getLastX()412 public int getLastX() { 413 return mKeyState.getLastX(); 414 } 415 getLastY()416 public int getLastY() { 417 return mKeyState.getLastY(); 418 } 419 getDownTime()420 public long getDownTime() { 421 return mKeyState.getDownTime(); 422 } 423 424 // These package scope methods are only for debugging purpose. getStartX()425 /* package */ int getStartX() { 426 return mKeyState.getStartX(); 427 } 428 getStartY()429 /* package */ int getStartY() { 430 return mKeyState.getStartY(); 431 } 432 isMinorMoveBounce(int x, int y, int newKey)433 private boolean isMinorMoveBounce(int x, int y, int newKey) { 434 if (mKeys == null || mKeyHysteresisDistanceSquared < 0) 435 throw new IllegalStateException("keyboard and/or hysteresis not set"); 436 int curKey = mKeyState.getKeyIndex(); 437 if (newKey == curKey) { 438 return true; 439 } else if (isValidKeyIndex(curKey)) { 440 return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) < mKeyHysteresisDistanceSquared; 441 } else { 442 return false; 443 } 444 } 445 getSquareDistanceToKeyEdge(int x, int y, Key key)446 private static int getSquareDistanceToKeyEdge(int x, int y, Key key) { 447 final int left = key.x; 448 final int right = key.x + key.width; 449 final int top = key.y; 450 final int bottom = key.y + key.height; 451 final int edgeX = x < left ? left : (x > right ? right : x); 452 final int edgeY = y < top ? top : (y > bottom ? bottom : y); 453 final int dx = x - edgeX; 454 final int dy = y - edgeY; 455 return dx * dx + dy * dy; 456 } 457 showKeyPreviewAndUpdateKey(int keyIndex)458 private void showKeyPreviewAndUpdateKey(int keyIndex) { 459 updateKey(keyIndex); 460 // The modifier key, such as shift key, should not be shown as preview when multi-touch is 461 // supported. On the other hand, if multi-touch is not supported, the modifier key should 462 // be shown as preview. 463 if (mHasDistinctMultitouch && isModifier()) { 464 mProxy.showPreview(NOT_A_KEY, this); 465 } else { 466 mProxy.showPreview(keyIndex, this); 467 } 468 } 469 startLongPressTimer(int keyIndex)470 private void startLongPressTimer(int keyIndex) { 471 if (mKeyboardSwitcher.isInMomentaryAutoModeSwitchState()) { 472 // We use longer timeout for sliding finger input started from the symbols mode key. 473 mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this); 474 } else { 475 mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this); 476 } 477 } 478 detectAndSendKey(int index, int x, int y, long eventTime)479 private void detectAndSendKey(int index, int x, int y, long eventTime) { 480 final OnKeyboardActionListener listener = mListener; 481 final Key key = getKey(index); 482 483 if (key == null) { 484 if (listener != null) 485 listener.onCancel(); 486 } else { 487 if (key.text != null) { 488 if (listener != null) { 489 listener.onText(key.text); 490 listener.onRelease(0); // dummy key code 491 } 492 } else { 493 int code = key.codes[0]; 494 int[] codes = mKeyDetector.newCodeArray(); 495 mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes); 496 // Multi-tap 497 if (mInMultiTap) { 498 if (mTapCount != -1) { 499 mListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE, x, y); 500 } else { 501 mTapCount = 0; 502 } 503 code = key.codes[mTapCount]; 504 } 505 /* 506 * Swap the first and second values in the codes array if the primary code is not 507 * the first value but the second value in the array. This happens when key 508 * debouncing is in effect. 509 */ 510 if (codes.length >= 2 && codes[0] != code && codes[1] == code) { 511 codes[1] = codes[0]; 512 codes[0] = code; 513 } 514 if (listener != null) { 515 listener.onKey(code, codes, x, y); 516 listener.onRelease(code); 517 } 518 } 519 mLastSentIndex = index; 520 mLastTapTime = eventTime; 521 } 522 } 523 524 /** 525 * Handle multi-tap keys by producing the key label for the current multi-tap state. 526 */ getPreviewText(Key key)527 public CharSequence getPreviewText(Key key) { 528 if (mInMultiTap) { 529 // Multi-tap 530 mPreviewLabel.setLength(0); 531 mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); 532 return mPreviewLabel; 533 } else { 534 return key.label; 535 } 536 } 537 resetMultiTap()538 private void resetMultiTap() { 539 mLastSentIndex = NOT_A_KEY; 540 mTapCount = 0; 541 mLastTapTime = -1; 542 mInMultiTap = false; 543 } 544 checkMultiTap(long eventTime, int keyIndex)545 private void checkMultiTap(long eventTime, int keyIndex) { 546 Key key = getKey(keyIndex); 547 if (key == null) 548 return; 549 550 final boolean isMultiTap = 551 (eventTime < mLastTapTime + mMultiTapKeyTimeout && keyIndex == mLastSentIndex); 552 if (key.codes.length > 1) { 553 mInMultiTap = true; 554 if (isMultiTap) { 555 mTapCount = (mTapCount + 1) % key.codes.length; 556 return; 557 } else { 558 mTapCount = -1; 559 return; 560 } 561 } 562 if (!isMultiTap) { 563 resetMultiTap(); 564 } 565 } 566 debugLog(String title, int x, int y)567 private void debugLog(String title, int x, int y) { 568 int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null); 569 Key key = getKey(keyIndex); 570 final String code; 571 if (key == null) { 572 code = "----"; 573 } else { 574 int primaryCode = key.codes[0]; 575 code = String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode); 576 } 577 Log.d(TAG, String.format("%s%s[%d] %3d,%3d %3d(%s) %s", title, 578 (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, keyIndex, code, 579 (isModifier() ? "modifier" : ""))); 580 } 581 } 582