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