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