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