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