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