PointerTracker.java revision d631778e1c1ffcdf28129894239e7ee7d6f399fc
1/*
2 * Copyright (C) 2010 The Android Open Source Project
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 android.content.res.TypedArray;
20import android.os.SystemClock;
21import android.util.Log;
22import android.view.MotionEvent;
23
24import com.android.inputmethod.accessibility.AccessibilityUtils;
25import com.android.inputmethod.keyboard.internal.GestureStroke;
26import com.android.inputmethod.keyboard.internal.GestureStroke.GestureStrokeParams;
27import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints;
28import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
29import com.android.inputmethod.latin.CollectionUtils;
30import com.android.inputmethod.latin.InputPointers;
31import com.android.inputmethod.latin.LatinImeLogger;
32import com.android.inputmethod.latin.R;
33import com.android.inputmethod.latin.define.ProductionFlag;
34import com.android.inputmethod.research.ResearchLogger;
35
36import java.util.ArrayList;
37
38public final class PointerTracker implements PointerTrackerQueue.Element {
39    private static final String TAG = PointerTracker.class.getSimpleName();
40    private static final boolean DEBUG_EVENT = false;
41    private static final boolean DEBUG_MOVE_EVENT = false;
42    private static final boolean DEBUG_LISTENER = false;
43    private static boolean DEBUG_MODE = LatinImeLogger.sDBG || DEBUG_EVENT;
44
45    /** True if {@link PointerTracker}s should handle gesture events. */
46    private static boolean sShouldHandleGesture = false;
47    private static boolean sMainDictionaryAvailable = false;
48    private static boolean sGestureHandlingEnabledByInputField = false;
49    private static boolean sGestureHandlingEnabledByUser = false;
50
51    public interface KeyEventHandler {
52        /**
53         * Get KeyDetector object that is used for this PointerTracker.
54         * @return the KeyDetector object that is used for this PointerTracker
55         */
56        public KeyDetector getKeyDetector();
57
58        /**
59         * Get KeyboardActionListener object that is used to register key code and so on.
60         * @return the KeyboardActionListner for this PointerTracker
61         */
62        public KeyboardActionListener getKeyboardActionListener();
63
64        /**
65         * Get DrawingProxy object that is used for this PointerTracker.
66         * @return the DrawingProxy object that is used for this PointerTracker
67         */
68        public DrawingProxy getDrawingProxy();
69
70        /**
71         * Get TimerProxy object that handles key repeat and long press timer event for this
72         * PointerTracker.
73         * @return the TimerProxy object that handles key repeat and long press timer event.
74         */
75        public TimerProxy getTimerProxy();
76    }
77
78    public interface DrawingProxy extends MoreKeysPanel.Controller {
79        public void invalidateKey(Key key);
80        public void showKeyPreview(PointerTracker tracker);
81        public void dismissKeyPreview(PointerTracker tracker);
82        public void showGesturePreviewTrail(PointerTracker tracker, boolean isOldestTracker);
83    }
84
85    public interface TimerProxy {
86        public void startTypingStateTimer(Key typedKey);
87        public boolean isTypingState();
88        public void startKeyRepeatTimer(PointerTracker tracker);
89        public void startLongPressTimer(PointerTracker tracker);
90        public void startLongPressTimer(int code);
91        public void cancelLongPressTimer();
92        public void startDoubleTapTimer();
93        public void cancelDoubleTapTimer();
94        public boolean isInDoubleTapTimeout();
95        public void cancelKeyTimers();
96
97        public static class Adapter implements TimerProxy {
98            @Override
99            public void startTypingStateTimer(Key typedKey) {}
100            @Override
101            public boolean isTypingState() { return false; }
102            @Override
103            public void startKeyRepeatTimer(PointerTracker tracker) {}
104            @Override
105            public void startLongPressTimer(PointerTracker tracker) {}
106            @Override
107            public void startLongPressTimer(int code) {}
108            @Override
109            public void cancelLongPressTimer() {}
110            @Override
111            public void startDoubleTapTimer() {}
112            @Override
113            public void cancelDoubleTapTimer() {}
114            @Override
115            public boolean isInDoubleTapTimeout() { return false; }
116            @Override
117            public void cancelKeyTimers() {}
118        }
119    }
120
121    static final class PointerTrackerParams {
122        public final boolean mSlidingKeyInputEnabled;
123        public final int mTouchNoiseThresholdTime;
124        public final int mTouchNoiseThresholdDistance;
125        public final int mSuppressKeyPreviewAfterBatchInputDuration;
126
127        public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
128
129        private PointerTrackerParams() {
130            mSlidingKeyInputEnabled = false;
131            mTouchNoiseThresholdTime = 0;
132            mTouchNoiseThresholdDistance = 0;
133            mSuppressKeyPreviewAfterBatchInputDuration = 0;
134        }
135
136        public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) {
137            mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean(
138                    R.styleable.MainKeyboardView_slidingKeyInputEnable, false);
139            mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt(
140                    R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0);
141            mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimensionPixelSize(
142                    R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0);
143            mSuppressKeyPreviewAfterBatchInputDuration = mainKeyboardViewAttr.getInt(
144                    R.styleable.MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration, 0);
145        }
146    }
147
148    // Parameters for pointer handling.
149    private static PointerTrackerParams sParams;
150    private static GestureStrokeParams sGestureStrokeParams;
151    private static boolean sNeedsPhantomSuddenMoveEventHack;
152    // Move this threshold to resource.
153    // TODO: Device specific parameter would be better for device specific hack?
154    private static final float PHANTOM_SUDDEN_MOVE_THRESHOLD = 0.25f; // in keyWidth
155    // This hack might be device specific.
156    private static final boolean sNeedsProximateBogusDownMoveUpEventHack = true;
157
158    private static final ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList();
159    private static PointerTrackerQueue sPointerTrackerQueue;
160
161    public final int mPointerId;
162
163    private DrawingProxy mDrawingProxy;
164    private TimerProxy mTimerProxy;
165    private KeyDetector mKeyDetector;
166    private KeyboardActionListener mListener = EMPTY_LISTENER;
167
168    private Keyboard mKeyboard;
169    private int mPhantonSuddenMoveThreshold;
170    private final BogusMoveEventDetector mBogusMoveEventDetector = new BogusMoveEventDetector();
171
172    private boolean mIsDetectingGesture = false; // per PointerTracker.
173    private static boolean sInGesture = false;
174    private static long sGestureFirstDownTime;
175    private static TimeRecorder sTimeRecorder;
176    private static final InputPointers sAggregratedPointers = new InputPointers(
177            GestureStroke.DEFAULT_CAPACITY);
178    private static int sLastRecognitionPointSize = 0; // synchronized using sAggregratedPointers
179    private static long sLastRecognitionTime = 0; // synchronized using sAggregratedPointers
180
181    static final class BogusMoveEventDetector {
182        // Move these thresholds to resource.
183        // These thresholds' unit is a diagonal length of a key.
184        private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f;
185        private static final float BOGUS_MOVE_RADIUS_THRESHOLD = 1.14f;
186
187        private int mAccumulatedDistanceThreshold;
188        private int mRadiusThreshold;
189
190        // Accumulated distance from actual and artificial down keys.
191        /* package */ int mAccumulatedDistanceFromDownKey;
192        private int mActualDownX;
193        private int mActualDownY;
194
195        public void setKeyboardGeometry(final int keyWidth, final int keyHeight) {
196            final float keyDiagonal = (float)Math.hypot(keyWidth, keyHeight);
197            mAccumulatedDistanceThreshold = (int)(
198                    keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD);
199            mRadiusThreshold = (int)(keyDiagonal * BOGUS_MOVE_RADIUS_THRESHOLD);
200        }
201
202        public void onActualDownEvent(final int x, final int y) {
203            mActualDownX = x;
204            mActualDownY = y;
205        }
206
207        public void onDownKey() {
208            mAccumulatedDistanceFromDownKey = 0;
209        }
210
211        public void onMoveKey(final int distance) {
212            mAccumulatedDistanceFromDownKey += distance;
213        }
214
215        public boolean hasTraveledLongDistance(final int x, final int y) {
216            final int dx = Math.abs(x - mActualDownX);
217            final int dy = Math.abs(y - mActualDownY);
218            // A bogus move event should be a horizontal movement. A vertical movement might be
219            // a sloppy typing and should be ignored.
220            return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold;
221        }
222
223        /* package */ int getDistanceFromDownEvent(final int x, final int y) {
224            return getDistance(x, y, mActualDownX, mActualDownY);
225        }
226
227        public boolean isCloseToActualDownEvent(final int x, final int y) {
228            return getDistanceFromDownEvent(x, y) < mRadiusThreshold;
229        }
230    }
231
232    static final class TimeRecorder {
233        private final int mSuppressKeyPreviewAfterBatchInputDuration;
234        private final int mStaticTimeThresholdAfterFastTyping; // msec
235        private long mLastTypingTime;
236        private long mLastLetterTypingTime;
237        private long mLastBatchInputTime;
238
239        public TimeRecorder(final PointerTrackerParams pointerTrackerParams,
240                final GestureStrokeParams gestureStrokeParams) {
241            mSuppressKeyPreviewAfterBatchInputDuration =
242                    pointerTrackerParams.mSuppressKeyPreviewAfterBatchInputDuration;
243            mStaticTimeThresholdAfterFastTyping =
244                    gestureStrokeParams.mStaticTimeThresholdAfterFastTyping;
245        }
246
247        public boolean isInFastTyping(final long eventTime) {
248            final long elapsedTimeSinceLastLetterTyping = eventTime - mLastLetterTypingTime;
249            return elapsedTimeSinceLastLetterTyping < mStaticTimeThresholdAfterFastTyping;
250        }
251
252        private boolean wasLastInputTyping() {
253            return mLastTypingTime >= mLastBatchInputTime;
254        }
255
256        public void onCodeInput(final int code, final long eventTime) {
257            // Record the letter typing time when
258            // 1. Letter keys are typed successively without any batch input in between.
259            // 2. A letter key is typed within the threshold time since the last any key typing.
260            // 3. A non-letter key is typed within the threshold time since the last letter key
261            // typing.
262            if (Character.isLetter(code)) {
263                if (wasLastInputTyping()
264                        || eventTime - mLastTypingTime < mStaticTimeThresholdAfterFastTyping) {
265                    mLastLetterTypingTime = eventTime;
266                }
267            } else {
268                if (eventTime - mLastLetterTypingTime < mStaticTimeThresholdAfterFastTyping) {
269                    // This non-letter typing should be treated as a part of fast typing.
270                    mLastLetterTypingTime = eventTime;
271                }
272            }
273            mLastTypingTime = eventTime;
274        }
275
276        public void onEndBatchInput(final long eventTime) {
277            mLastBatchInputTime = eventTime;
278        }
279
280        public long getLastLetterTypingTime() {
281            return mLastLetterTypingTime;
282        }
283
284        public boolean needsToSuppressKeyPreviewPopup(final long eventTime) {
285            return !wasLastInputTyping()
286                    && eventTime - mLastBatchInputTime < mSuppressKeyPreviewAfterBatchInputDuration;
287        }
288    }
289
290    // The position and time at which first down event occurred.
291    private long mDownTime;
292    private long mUpTime;
293
294    // The current key where this pointer is.
295    private Key mCurrentKey = null;
296    // The position where the current key was recognized for the first time.
297    private int mKeyX;
298    private int mKeyY;
299
300    // Last pointer position.
301    private int mLastX;
302    private int mLastY;
303
304    // true if keyboard layout has been changed.
305    private boolean mKeyboardLayoutHasBeenChanged;
306
307    // true if event is already translated to a key action.
308    private boolean mKeyAlreadyProcessed;
309
310    // true if this pointer has been long-pressed and is showing a more keys panel.
311    private boolean mIsShowingMoreKeysPanel;
312
313    // true if this pointer is in a sliding key input.
314    boolean mIsInSlidingKeyInput;
315    // true if this pointer is in a sliding key input from a modifier key,
316    // so that further modifier keys should be ignored.
317    boolean mIsInSlidingKeyInputFromModifier;
318
319    // true if a sliding key input is allowed.
320    private boolean mIsAllowedSlidingKeyInput;
321
322    // Empty {@link KeyboardActionListener}
323    private static final KeyboardActionListener EMPTY_LISTENER =
324            new KeyboardActionListener.Adapter();
325
326    private final GestureStrokeWithPreviewPoints mGestureStrokeWithPreviewPoints;
327
328    public static void init(boolean hasDistinctMultitouch,
329            boolean needsPhantomSuddenMoveEventHack) {
330        if (hasDistinctMultitouch) {
331            sPointerTrackerQueue = new PointerTrackerQueue();
332        } else {
333            sPointerTrackerQueue = null;
334        }
335        sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack;
336        sParams = PointerTrackerParams.DEFAULT;
337        sGestureStrokeParams = GestureStrokeParams.DEFAULT;
338        sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
339    }
340
341    public static void setParameters(final TypedArray mainKeyboardViewAttr) {
342        sParams = new PointerTrackerParams(mainKeyboardViewAttr);
343        sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr);
344        sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
345    }
346
347    private static void updateGestureHandlingMode() {
348        sShouldHandleGesture = sMainDictionaryAvailable
349                && sGestureHandlingEnabledByInputField
350                && sGestureHandlingEnabledByUser
351                && !AccessibilityUtils.getInstance().isTouchExplorationEnabled();
352    }
353
354    // Note that this method is called from a non-UI thread.
355    public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
356        sMainDictionaryAvailable = mainDictionaryAvailable;
357        updateGestureHandlingMode();
358    }
359
360    public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
361        sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser;
362        updateGestureHandlingMode();
363    }
364
365    public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) {
366        final ArrayList<PointerTracker> trackers = sTrackers;
367
368        // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
369        for (int i = trackers.size(); i <= id; i++) {
370            final PointerTracker tracker = new PointerTracker(i, handler);
371            trackers.add(tracker);
372        }
373
374        return trackers.get(id);
375    }
376
377    public static boolean isAnyInSlidingKeyInput() {
378        return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false;
379    }
380
381    public static void setKeyboardActionListener(final KeyboardActionListener listener) {
382        final int trackersSize = sTrackers.size();
383        for (int i = 0; i < trackersSize; ++i) {
384            final PointerTracker tracker = sTrackers.get(i);
385            tracker.mListener = listener;
386        }
387    }
388
389    public static void setKeyDetector(final KeyDetector keyDetector) {
390        final int trackersSize = sTrackers.size();
391        for (int i = 0; i < trackersSize; ++i) {
392            final PointerTracker tracker = sTrackers.get(i);
393            tracker.setKeyDetectorInner(keyDetector);
394            // Mark that keyboard layout has been changed.
395            tracker.mKeyboardLayoutHasBeenChanged = true;
396        }
397        final Keyboard keyboard = keyDetector.getKeyboard();
398        sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput();
399        updateGestureHandlingMode();
400    }
401
402    public static void setReleasedKeyGraphicsToAllKeys() {
403        final int trackersSize = sTrackers.size();
404        for (int i = 0; i < trackersSize; ++i) {
405            final PointerTracker tracker = sTrackers.get(i);
406            tracker.setReleasedKeyGraphics(tracker.mCurrentKey);
407        }
408    }
409
410    private PointerTracker(final int id, final KeyEventHandler handler) {
411        if (handler == null) {
412            throw new NullPointerException();
413        }
414        mPointerId = id;
415        mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints(
416                id, sGestureStrokeParams);
417        setKeyDetectorInner(handler.getKeyDetector());
418        mListener = handler.getKeyboardActionListener();
419        mDrawingProxy = handler.getDrawingProxy();
420        mTimerProxy = handler.getTimerProxy();
421    }
422
423    // Returns true if keyboard has been changed by this callback.
424    private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) {
425        if (sInGesture) {
426            return false;
427        }
428        final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
429        if (DEBUG_LISTENER) {
430            Log.d(TAG, String.format("[%d] onPress    : %s%s%s", mPointerId,
431                    KeyDetector.printableCode(key),
432                    ignoreModifierKey ? " ignoreModifier" : "",
433                    key.isEnabled() ? "" : " disabled"));
434        }
435        if (ignoreModifierKey) {
436            return false;
437        }
438        if (key.isEnabled()) {
439            mListener.onPressKey(key.mCode);
440            final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
441            mKeyboardLayoutHasBeenChanged = false;
442            mTimerProxy.startTypingStateTimer(key);
443            return keyboardLayoutHasBeenChanged;
444        }
445        return false;
446    }
447
448    // Note that we need primaryCode argument because the keyboard may in shifted state and the
449    // primaryCode is different from {@link Key#mCode}.
450    private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x,
451            final int y, final long eventTime) {
452        final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
453        final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
454        final int code = altersCode ? key.getAltCode() : primaryCode;
455        if (DEBUG_LISTENER) {
456            final String output = code == Keyboard.CODE_OUTPUT_TEXT
457                    ? key.getOutputText() : Keyboard.printableCode(code);
458            Log.d(TAG, String.format("[%d] onCodeInput: %4d %4d %s%s%s", mPointerId, x, y,
459                    output, ignoreModifierKey ? " ignoreModifier" : "",
460                    altersCode ? " altersCode" : "", key.isEnabled() ? "" : " disabled"));
461        }
462        if (ProductionFlag.IS_EXPERIMENTAL) {
463            ResearchLogger.pointerTracker_callListenerOnCodeInput(key, x, y, ignoreModifierKey,
464                    altersCode, code);
465        }
466        if (ignoreModifierKey) {
467            return;
468        }
469        // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
470        if (key.isEnabled() || altersCode) {
471            sTimeRecorder.onCodeInput(code, eventTime);
472            if (code == Keyboard.CODE_OUTPUT_TEXT) {
473                mListener.onTextInput(key.getOutputText());
474            } else if (code != Keyboard.CODE_UNSPECIFIED) {
475                mListener.onCodeInput(code, x, y);
476            }
477        }
478    }
479
480    // Note that we need primaryCode argument because the keyboard may be in shifted state and the
481    // primaryCode is different from {@link Key#mCode}.
482    private void callListenerOnRelease(final Key key, final int primaryCode,
483            final boolean withSliding) {
484        if (sInGesture) {
485            return;
486        }
487        final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
488        if (DEBUG_LISTENER) {
489            Log.d(TAG, String.format("[%d] onRelease  : %s%s%s%s", mPointerId,
490                    Keyboard.printableCode(primaryCode),
491                    withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : "",
492                    key.isEnabled() ?  "": " disabled"));
493        }
494        if (ProductionFlag.IS_EXPERIMENTAL) {
495            ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding,
496                    ignoreModifierKey);
497        }
498        if (ignoreModifierKey) {
499            return;
500        }
501        if (key.isEnabled()) {
502            mListener.onReleaseKey(primaryCode, withSliding);
503        }
504    }
505
506    private void callListenerOnCancelInput() {
507        if (DEBUG_LISTENER) {
508            Log.d(TAG, String.format("[%d] onCancelInput", mPointerId));
509        }
510        if (ProductionFlag.IS_EXPERIMENTAL) {
511            ResearchLogger.pointerTracker_callListenerOnCancelInput();
512        }
513        mListener.onCancelInput();
514    }
515
516    private void setKeyDetectorInner(final KeyDetector keyDetector) {
517        final Keyboard keyboard = keyDetector.getKeyboard();
518        if (keyDetector == mKeyDetector && keyboard == mKeyboard) {
519            return;
520        }
521        mKeyDetector = keyDetector;
522        mKeyboard = keyDetector.getKeyboard();
523        final int keyWidth = mKeyboard.mMostCommonKeyWidth;
524        final int keyHeight = mKeyboard.mMostCommonKeyHeight;
525        mGestureStrokeWithPreviewPoints.setKeyboardGeometry(keyWidth);
526        final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY);
527        if (newKey != mCurrentKey) {
528            if (mDrawingProxy != null) {
529                setReleasedKeyGraphics(mCurrentKey);
530            }
531            // Keep {@link #mCurrentKey} that comes from previous keyboard.
532        }
533        mPhantonSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD);
534        mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight);
535    }
536
537    @Override
538    public boolean isInSlidingKeyInput() {
539        return mIsInSlidingKeyInput;
540    }
541
542    public Key getKey() {
543        return mCurrentKey;
544    }
545
546    @Override
547    public boolean isModifier() {
548        return mCurrentKey != null && mCurrentKey.isModifier();
549    }
550
551    public Key getKeyOn(final int x, final int y) {
552        return mKeyDetector.detectHitKey(x, y);
553    }
554
555    private void setReleasedKeyGraphics(final Key key) {
556        mDrawingProxy.dismissKeyPreview(this);
557        if (key == null) {
558            return;
559        }
560
561        // Even if the key is disabled, update the key release graphics just in case.
562        updateReleaseKeyGraphics(key);
563
564        if (key.isShift()) {
565            for (final Key shiftKey : mKeyboard.mShiftKeys) {
566                if (shiftKey != key) {
567                    updateReleaseKeyGraphics(shiftKey);
568                }
569            }
570        }
571
572        if (key.altCodeWhileTyping()) {
573            final int altCode = key.getAltCode();
574            final Key altKey = mKeyboard.getKey(altCode);
575            if (altKey != null) {
576                updateReleaseKeyGraphics(altKey);
577            }
578            for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
579                if (k != key && k.getAltCode() == altCode) {
580                    updateReleaseKeyGraphics(k);
581                }
582            }
583        }
584    }
585
586    private static boolean needsToSuppressKeyPreviewPopup(final long eventTime) {
587        if (!sShouldHandleGesture) return false;
588        return sTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime);
589    }
590
591    private void setPressedKeyGraphics(final Key key, final long eventTime) {
592        if (key == null) {
593            return;
594        }
595
596        // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
597        final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
598        final boolean needsToUpdateGraphics = key.isEnabled() || altersCode;
599        if (!needsToUpdateGraphics) {
600            return;
601        }
602
603        if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) {
604            mDrawingProxy.showKeyPreview(this);
605        }
606        updatePressKeyGraphics(key);
607
608        if (key.isShift()) {
609            for (final Key shiftKey : mKeyboard.mShiftKeys) {
610                if (shiftKey != key) {
611                    updatePressKeyGraphics(shiftKey);
612                }
613            }
614        }
615
616        if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) {
617            final int altCode = key.getAltCode();
618            final Key altKey = mKeyboard.getKey(altCode);
619            if (altKey != null) {
620                updatePressKeyGraphics(altKey);
621            }
622            for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
623                if (k != key && k.getAltCode() == altCode) {
624                    updatePressKeyGraphics(k);
625                }
626            }
627        }
628    }
629
630    private void updateReleaseKeyGraphics(final Key key) {
631        key.onReleased();
632        mDrawingProxy.invalidateKey(key);
633    }
634
635    private void updatePressKeyGraphics(final Key key) {
636        key.onPressed();
637        mDrawingProxy.invalidateKey(key);
638    }
639
640    public GestureStrokeWithPreviewPoints getGestureStrokeWithPreviewPoints() {
641        return mGestureStrokeWithPreviewPoints;
642    }
643
644    public int getLastX() {
645        return mLastX;
646    }
647
648    public int getLastY() {
649        return mLastY;
650    }
651
652    public long getDownTime() {
653        return mDownTime;
654    }
655
656    private Key onDownKey(final int x, final int y, final long eventTime) {
657        mDownTime = eventTime;
658        mBogusMoveEventDetector.onDownKey();
659        return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
660    }
661
662    static int getDistance(final int x1, final int y1, final int x2, final int y2) {
663        return (int)Math.hypot(x1 - x2, y1 - y2);
664    }
665
666    private Key onMoveKeyInternal(final int x, final int y) {
667        mBogusMoveEventDetector.onMoveKey(getDistance(x, y, mLastX, mLastY));
668        mLastX = x;
669        mLastY = y;
670        return mKeyDetector.detectHitKey(x, y);
671    }
672
673    private Key onMoveKey(final int x, final int y) {
674        return onMoveKeyInternal(x, y);
675    }
676
677    private Key onMoveToNewKey(final Key newKey, final int x, final int y) {
678        mCurrentKey = newKey;
679        mKeyX = x;
680        mKeyY = y;
681        return newKey;
682    }
683
684    private static int getActivePointerTrackerCount() {
685        return (sPointerTrackerQueue == null) ? 1 : sPointerTrackerQueue.size();
686    }
687
688    private void mayStartBatchInput(final Key key) {
689        if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) {
690            return;
691        }
692        if (key == null || !Character.isLetter(key.mCode)) {
693            return;
694        }
695        if (DEBUG_LISTENER) {
696            Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId));
697        }
698        sInGesture = true;
699        synchronized (sAggregratedPointers) {
700            sAggregratedPointers.reset();
701            sLastRecognitionPointSize = 0;
702            sLastRecognitionTime = 0;
703            mListener.onStartBatchInput();
704        }
705        final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
706        mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
707    }
708
709    private void mayUpdateBatchInput(final long eventTime, final Key key) {
710        if (key != null) {
711            synchronized (sAggregratedPointers) {
712                final GestureStroke stroke = mGestureStrokeWithPreviewPoints;
713                stroke.appendIncrementalBatchPoints(sAggregratedPointers);
714                final int size = sAggregratedPointers.getPointerSize();
715                if (size > sLastRecognitionPointSize
716                        && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) {
717                    sLastRecognitionPointSize = size;
718                    sLastRecognitionTime = eventTime;
719                    if (DEBUG_LISTENER) {
720                        Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d",
721                                mPointerId, size));
722                    }
723                    mListener.onUpdateBatchInput(sAggregratedPointers);
724                }
725            }
726        }
727        final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
728        mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
729    }
730
731    private void mayEndBatchInput(final long eventTime) {
732        synchronized (sAggregratedPointers) {
733            mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers);
734            if (getActivePointerTrackerCount() == 1) {
735                if (DEBUG_LISTENER) {
736                    Log.d(TAG, String.format("[%d] onEndBatchInput   : batchPoints=%d",
737                            mPointerId, sAggregratedPointers.getPointerSize()));
738                }
739                sInGesture = false;
740                sTimeRecorder.onEndBatchInput(eventTime);
741                mListener.onEndBatchInput(sAggregratedPointers);
742            }
743        }
744        final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
745        mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
746    }
747
748    public void processMotionEvent(final int action, final int x, final int y, final long eventTime,
749            final KeyEventHandler handler) {
750        switch (action) {
751        case MotionEvent.ACTION_DOWN:
752        case MotionEvent.ACTION_POINTER_DOWN:
753            onDownEvent(x, y, eventTime, handler);
754            break;
755        case MotionEvent.ACTION_UP:
756        case MotionEvent.ACTION_POINTER_UP:
757            onUpEvent(x, y, eventTime);
758            break;
759        case MotionEvent.ACTION_MOVE:
760            onMoveEvent(x, y, eventTime, null);
761            break;
762        case MotionEvent.ACTION_CANCEL:
763            onCancelEvent(x, y, eventTime);
764            break;
765        }
766    }
767
768    public void onDownEvent(final int x, final int y, final long eventTime,
769            final KeyEventHandler handler) {
770        if (DEBUG_EVENT) {
771            printTouchEvent("onDownEvent:", x, y, eventTime);
772        }
773
774        mDrawingProxy = handler.getDrawingProxy();
775        mTimerProxy = handler.getTimerProxy();
776        setKeyboardActionListener(handler.getKeyboardActionListener());
777        setKeyDetectorInner(handler.getKeyDetector());
778        // Naive up-to-down noise filter.
779        final long deltaT = eventTime - mUpTime;
780        if (deltaT < sParams.mTouchNoiseThresholdTime) {
781            final int distance = getDistance(x, y, mLastX, mLastY);
782            if (distance < sParams.mTouchNoiseThresholdDistance) {
783                if (DEBUG_MODE)
784                    Log.w(TAG, String.format("[%d] onDownEvent:"
785                            + " ignore potential noise: time=%d distance=%d",
786                            mPointerId, deltaT, distance));
787                if (ProductionFlag.IS_EXPERIMENTAL) {
788                    ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance);
789                }
790                mKeyAlreadyProcessed = true;
791                return;
792            }
793        }
794
795        final Key key = getKeyOn(x, y);
796        mBogusMoveEventDetector.onActualDownEvent(x, y);
797        final PointerTrackerQueue queue = sPointerTrackerQueue;
798        if (queue != null) {
799            if (key != null && key.isModifier()) {
800                // Before processing a down event of modifier key, all pointers already being
801                // tracked should be released.
802                queue.releaseAllPointers(eventTime);
803            }
804            queue.add(this);
805        }
806        onDownEventInternal(x, y, eventTime);
807        if (!sShouldHandleGesture) {
808            return;
809        }
810        // A gesture should start only from the letter key.
811        mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard()
812                && !mIsShowingMoreKeysPanel && key != null && Keyboard.isLetterCode(key.mCode);
813        if (mIsDetectingGesture) {
814            if (getActivePointerTrackerCount() == 1) {
815                sGestureFirstDownTime = eventTime;
816            }
817            mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime,
818                    sTimeRecorder.getLastLetterTypingTime());
819        }
820    }
821
822    private void onDownEventInternal(final int x, final int y, final long eventTime) {
823        Key key = onDownKey(x, y, eventTime);
824        // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
825        // from modifier key, or 3) this pointer's KeyDetector always allows sliding input.
826        mIsAllowedSlidingKeyInput = sParams.mSlidingKeyInputEnabled
827                || (key != null && key.isModifier())
828                || mKeyDetector.alwaysAllowsSlidingInput();
829        mKeyboardLayoutHasBeenChanged = false;
830        mKeyAlreadyProcessed = false;
831        resetSlidingKeyInput();
832        if (key != null) {
833            // This onPress call may have changed keyboard layout. Those cases are detected at
834            // {@link #setKeyboard}. In those cases, we should update key according to the new
835            // keyboard layout.
836            if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
837                key = onDownKey(x, y, eventTime);
838            }
839
840            startRepeatKey(key);
841            startLongPressTimer(key);
842            setPressedKeyGraphics(key, eventTime);
843        }
844    }
845
846    private void startSlidingKeyInput(final Key key) {
847        if (!mIsInSlidingKeyInput) {
848            mIsInSlidingKeyInputFromModifier = key.isModifier();
849        }
850        mIsInSlidingKeyInput = true;
851    }
852
853    private void resetSlidingKeyInput() {
854        mIsInSlidingKeyInput = false;
855        mIsInSlidingKeyInputFromModifier = false;
856    }
857
858    private void onGestureMoveEvent(final int x, final int y, final long eventTime,
859            final boolean isMajorEvent, final Key key) {
860        final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
861        if (mIsDetectingGesture) {
862            mGestureStrokeWithPreviewPoints.addPoint(x, y, gestureTime, isMajorEvent);
863            mayStartBatchInput(key);
864            if (sInGesture) {
865                mayUpdateBatchInput(eventTime, key);
866            }
867        }
868    }
869
870    public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
871        if (DEBUG_MOVE_EVENT) {
872            printTouchEvent("onMoveEvent:", x, y, eventTime);
873        }
874        if (mKeyAlreadyProcessed) {
875            return;
876        }
877
878        if (sShouldHandleGesture && me != null) {
879            // Add historical points to gesture path.
880            final int pointerIndex = me.findPointerIndex(mPointerId);
881            final int historicalSize = me.getHistorySize();
882            for (int h = 0; h < historicalSize; h++) {
883                final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
884                final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
885                final long historicalTime = me.getHistoricalEventTime(h);
886                onGestureMoveEvent(historicalX, historicalY, historicalTime,
887                        false /* isMajorEvent */, null);
888            }
889        }
890
891        onMoveEventInternal(x, y, eventTime);
892    }
893
894    private void onMoveEventInternal(final int x, final int y, final long eventTime) {
895        final int lastX = mLastX;
896        final int lastY = mLastY;
897        final Key oldKey = mCurrentKey;
898        Key key = onMoveKey(x, y);
899
900        if (sShouldHandleGesture) {
901            // Register move event on gesture tracker.
902            onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, key);
903            if (sInGesture) {
904                mTimerProxy.cancelLongPressTimer();
905                mCurrentKey = null;
906                setReleasedKeyGraphics(oldKey);
907                return;
908            }
909        }
910
911        if (key != null) {
912            if (oldKey == null) {
913                // The pointer has been slid in to the new key, but the finger was not on any keys.
914                // In this case, we must call onPress() to notify that the new key is being pressed.
915                // This onPress call may have changed keyboard layout. Those cases are detected at
916                // {@link #setKeyboard}. In those cases, we should update key according to the
917                // new keyboard layout.
918                if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
919                    key = onMoveKey(x, y);
920                }
921                onMoveToNewKey(key, x, y);
922                startLongPressTimer(key);
923                setPressedKeyGraphics(key, eventTime);
924            } else if (isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, key)) {
925                // The pointer has been slid in to the new key from the previous key, we must call
926                // onRelease() first to notify that the previous key has been released, then call
927                // onPress() to notify that the new key is being pressed.
928                setReleasedKeyGraphics(oldKey);
929                callListenerOnRelease(oldKey, oldKey.mCode, true);
930                startSlidingKeyInput(oldKey);
931                mTimerProxy.cancelKeyTimers();
932                startRepeatKey(key);
933                if (mIsAllowedSlidingKeyInput) {
934                    // This onPress call may have changed keyboard layout. Those cases are detected
935                    // at {@link #setKeyboard}. In those cases, we should update key according
936                    // to the new keyboard layout.
937                    if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
938                        key = onMoveKey(x, y);
939                    }
940                    onMoveToNewKey(key, x, y);
941                    startLongPressTimer(key);
942                    setPressedKeyGraphics(key, eventTime);
943                } else {
944                    // HACK: On some devices, quick successive touches may be reported as a sudden
945                    // move by touch panel firmware. This hack detects such cases and translates the
946                    // move event to successive up and down events.
947                    // TODO: Should find a way to balance gesture detection and this hack.
948                    if (sNeedsPhantomSuddenMoveEventHack
949                            && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) {
950                        if (DEBUG_MODE) {
951                            Log.w(TAG, String.format("[%d] onMoveEvent:"
952                                    + " phantom sudden move event (distance=%d) is translated to "
953                                    + "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId,
954                                    getDistance(x, y, lastX, lastY),
955                                    lastX, lastY, Keyboard.printableCode(oldKey.mCode),
956                                    x, y, Keyboard.printableCode(key.mCode)));
957                        }
958                        // TODO: This should be moved to outside of this nested if-clause?
959                        if (ProductionFlag.IS_EXPERIMENTAL) {
960                            ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
961                        }
962                        onUpEventInternal(eventTime);
963                        onDownEventInternal(x, y, eventTime);
964                    }
965                    // HACK: On some devices, quick successive proximate touches may be reported as
966                    // a bogus down-move-up event by touch panel firmware. This hack detects such
967                    // cases and breaks these events into separate up and down events.
968                    else if (sNeedsProximateBogusDownMoveUpEventHack
969                            && sTimeRecorder.isInFastTyping(eventTime)
970                            && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) {
971                        if (DEBUG_MODE) {
972                            final float keyDiagonal = (float)Math.hypot(
973                                    mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
974                            final float radiusRatio =
975                                    mBogusMoveEventDetector.getDistanceFromDownEvent(x, y)
976                                    / keyDiagonal;
977                            Log.w(TAG, String.format("[%d] onMoveEvent:"
978                                    + " bogus down-move-up event (raidus=%.2f key diagonal) is "
979                                    + " translated to up[%d,%d,%s]/down[%d,%d,%s] events",
980                                    mPointerId, radiusRatio,
981                                    lastX, lastY, Keyboard.printableCode(oldKey.mCode),
982                                    x, y, Keyboard.printableCode(key.mCode)));
983                        }
984                        onUpEventInternal(eventTime);
985                        onDownEventInternal(x, y, eventTime);
986                    } else {
987                        // HACK: If there are currently multiple touches, register the key even if
988                        // the finger slides off the key. This defends against noise from some
989                        // touch panels when there are close multiple touches.
990                        // Caveat: When in chording input mode with a modifier key, we don't use
991                        // this hack.
992                        if (getActivePointerTrackerCount() > 1 && sPointerTrackerQueue != null
993                                && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
994                            if (DEBUG_MODE) {
995                                Log.w(TAG, String.format("[%d] onMoveEvent:"
996                                        + " detected sliding finger while multi touching",
997                                        mPointerId));
998                            }
999                            onUpEvent(x, y, eventTime);
1000                            mKeyAlreadyProcessed = true;
1001                        }
1002                        if (!mIsDetectingGesture) {
1003                            mKeyAlreadyProcessed = true;
1004                        }
1005                        setReleasedKeyGraphics(oldKey);
1006                    }
1007                }
1008            }
1009        } else {
1010            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, key)) {
1011                // The pointer has been slid out from the previous key, we must call onRelease() to
1012                // notify that the previous key has been released.
1013                setReleasedKeyGraphics(oldKey);
1014                callListenerOnRelease(oldKey, oldKey.mCode, true);
1015                startSlidingKeyInput(oldKey);
1016                mTimerProxy.cancelLongPressTimer();
1017                if (mIsAllowedSlidingKeyInput) {
1018                    onMoveToNewKey(key, x, y);
1019                } else {
1020                    if (!mIsDetectingGesture) {
1021                        mKeyAlreadyProcessed = true;
1022                    }
1023                }
1024            }
1025        }
1026    }
1027
1028    public void onUpEvent(final int x, final int y, final long eventTime) {
1029        if (DEBUG_EVENT) {
1030            printTouchEvent("onUpEvent  :", x, y, eventTime);
1031        }
1032
1033        final PointerTrackerQueue queue = sPointerTrackerQueue;
1034        if (queue != null) {
1035            if (!sInGesture) {
1036                if (mCurrentKey != null && mCurrentKey.isModifier()) {
1037                    // Before processing an up event of modifier key, all pointers already being
1038                    // tracked should be released.
1039                    queue.releaseAllPointersExcept(this, eventTime);
1040                } else {
1041                    queue.releaseAllPointersOlderThan(this, eventTime);
1042                }
1043            }
1044        }
1045        onUpEventInternal(eventTime);
1046        if (queue != null) {
1047            queue.remove(this);
1048        }
1049    }
1050
1051    // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
1052    // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
1053    // "virtual" up event.
1054    @Override
1055    public void onPhantomUpEvent(final long eventTime) {
1056        if (DEBUG_EVENT) {
1057            printTouchEvent("onPhntEvent:", getLastX(), getLastY(), eventTime);
1058        }
1059        onUpEventInternal(eventTime);
1060        mKeyAlreadyProcessed = true;
1061    }
1062
1063    private void onUpEventInternal(final long eventTime) {
1064        mTimerProxy.cancelKeyTimers();
1065        resetSlidingKeyInput();
1066        mIsDetectingGesture = false;
1067        final Key currentKey = mCurrentKey;
1068        mCurrentKey = null;
1069        // Release the last pressed key.
1070        setReleasedKeyGraphics(currentKey);
1071        if (mIsShowingMoreKeysPanel) {
1072            mDrawingProxy.dismissMoreKeysPanel();
1073            mIsShowingMoreKeysPanel = false;
1074        }
1075
1076        if (sInGesture) {
1077            if (currentKey != null) {
1078                callListenerOnRelease(currentKey, currentKey.mCode, true);
1079            }
1080            mayEndBatchInput(eventTime);
1081            return;
1082        }
1083
1084        if (mKeyAlreadyProcessed) {
1085            return;
1086        }
1087        if (currentKey != null && !currentKey.isRepeatable()) {
1088            detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
1089        }
1090    }
1091
1092    public void onShowMoreKeysPanel(final int x, final int y, final KeyEventHandler handler) {
1093        onLongPressed();
1094        mIsShowingMoreKeysPanel = true;
1095        onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
1096    }
1097
1098    public void onLongPressed() {
1099        mKeyAlreadyProcessed = true;
1100        setReleasedKeyGraphics(mCurrentKey);
1101        final PointerTrackerQueue queue = sPointerTrackerQueue;
1102        if (queue != null) {
1103            queue.remove(this);
1104        }
1105    }
1106
1107    public void onCancelEvent(final int x, final int y, final long eventTime) {
1108        if (DEBUG_EVENT) {
1109            printTouchEvent("onCancelEvt:", x, y, eventTime);
1110        }
1111
1112        final PointerTrackerQueue queue = sPointerTrackerQueue;
1113        if (queue != null) {
1114            queue.releaseAllPointersExcept(this, eventTime);
1115            queue.remove(this);
1116        }
1117        onCancelEventInternal();
1118    }
1119
1120    private void onCancelEventInternal() {
1121        mTimerProxy.cancelKeyTimers();
1122        setReleasedKeyGraphics(mCurrentKey);
1123        resetSlidingKeyInput();
1124        if (mIsShowingMoreKeysPanel) {
1125            mDrawingProxy.dismissMoreKeysPanel();
1126            mIsShowingMoreKeysPanel = false;
1127        }
1128    }
1129
1130    private void startRepeatKey(final Key key) {
1131        if (key != null && key.isRepeatable() && !sInGesture) {
1132            onRegisterKey(key);
1133            mTimerProxy.startKeyRepeatTimer(this);
1134        }
1135    }
1136
1137    public void onRegisterKey(final Key key) {
1138        if (key != null) {
1139            detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
1140            mTimerProxy.startTypingStateTimer(key);
1141        }
1142    }
1143
1144    private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
1145            final Key newKey) {
1146        if (mKeyDetector == null) {
1147            throw new NullPointerException("keyboard and/or key detector not set");
1148        }
1149        final Key curKey = mCurrentKey;
1150        if (newKey == curKey) {
1151            return false;
1152        } else if (curKey != null) {
1153            final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared(
1154                    mIsInSlidingKeyInputFromModifier);
1155            final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y);
1156            if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) {
1157                if (DEBUG_MODE) {
1158                    final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared)
1159                            / mKeyboard.mMostCommonKeyWidth;
1160                    Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1161                            +" %.2f key width from key edge",
1162                            mPointerId, distanceToEdgeRatio));
1163                }
1164                return true;
1165            }
1166            if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput
1167                    && sTimeRecorder.isInFastTyping(eventTime)
1168                    && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) {
1169                if (DEBUG_MODE) {
1170                    final float keyDiagonal = (float)Math.hypot(
1171                            mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1172                    final float lengthFromDownRatio =
1173                            mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal;
1174                    Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1175                            + " %.2f key diagonal from virtual down point",
1176                            mPointerId, lengthFromDownRatio));
1177                }
1178                return true;
1179            }
1180            return false;
1181        } else { // curKey == null && newKey != null
1182            return true;
1183        }
1184    }
1185
1186    private void startLongPressTimer(final Key key) {
1187        if (key != null && key.isLongPressEnabled() && !sInGesture) {
1188            mTimerProxy.startLongPressTimer(this);
1189        }
1190    }
1191
1192    private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
1193        if (key == null) {
1194            callListenerOnCancelInput();
1195            return;
1196        }
1197
1198        final int code = key.mCode;
1199        callListenerOnCodeInput(key, code, x, y, eventTime);
1200        callListenerOnRelease(key, code, false);
1201    }
1202
1203    private void printTouchEvent(final String title, final int x, final int y,
1204            final long eventTime) {
1205        final Key key = mKeyDetector.detectHitKey(x, y);
1206        final String code = KeyDetector.printableCode(key);
1207        Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
1208                (mKeyAlreadyProcessed ? "-" : " "), title, x, y, eventTime, code));
1209    }
1210}
1211