PointerTracker.java revision 8b449c6dda88174ec19bfc366baf048a72857215
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 PointerTrackerQueue sPointerTrackerQueue;
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(boolean hasDistinctMultitouch,
330            boolean needsPhantomSuddenMoveEventHack) {
331        if (hasDistinctMultitouch) {
332            sPointerTrackerQueue = new PointerTrackerQueue();
333        } else {
334            sPointerTrackerQueue = null;
335        }
336        sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack;
337        sParams = PointerTrackerParams.DEFAULT;
338        sGestureStrokeParams = GestureStrokeParams.DEFAULT;
339        sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
340    }
341
342    public static void setParameters(final TypedArray mainKeyboardViewAttr) {
343        sParams = new PointerTrackerParams(mainKeyboardViewAttr);
344        sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr);
345        sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
346    }
347
348    private static void updateGestureHandlingMode() {
349        sShouldHandleGesture = sMainDictionaryAvailable
350                && sGestureHandlingEnabledByInputField
351                && sGestureHandlingEnabledByUser
352                && !AccessibilityUtils.getInstance().isTouchExplorationEnabled();
353    }
354
355    // Note that this method is called from a non-UI thread.
356    public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
357        sMainDictionaryAvailable = mainDictionaryAvailable;
358        updateGestureHandlingMode();
359    }
360
361    public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
362        sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser;
363        updateGestureHandlingMode();
364    }
365
366    public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) {
367        final ArrayList<PointerTracker> trackers = sTrackers;
368
369        // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
370        for (int i = trackers.size(); i <= id; i++) {
371            final PointerTracker tracker = new PointerTracker(i, handler);
372            trackers.add(tracker);
373        }
374
375        return trackers.get(id);
376    }
377
378    public static boolean isAnyInSlidingKeyInput() {
379        return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false;
380    }
381
382    public static void setKeyboardActionListener(final KeyboardActionListener listener) {
383        final int trackersSize = sTrackers.size();
384        for (int i = 0; i < trackersSize; ++i) {
385            final PointerTracker tracker = sTrackers.get(i);
386            tracker.mListener = listener;
387        }
388    }
389
390    public static void setKeyDetector(final KeyDetector keyDetector) {
391        final int trackersSize = sTrackers.size();
392        for (int i = 0; i < trackersSize; ++i) {
393            final PointerTracker tracker = sTrackers.get(i);
394            tracker.setKeyDetectorInner(keyDetector);
395            // Mark that keyboard layout has been changed.
396            tracker.mKeyboardLayoutHasBeenChanged = true;
397        }
398        final Keyboard keyboard = keyDetector.getKeyboard();
399        sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput();
400        updateGestureHandlingMode();
401    }
402
403    public static void setReleasedKeyGraphicsToAllKeys() {
404        final int trackersSize = sTrackers.size();
405        for (int i = 0; i < trackersSize; ++i) {
406            final PointerTracker tracker = sTrackers.get(i);
407            tracker.setReleasedKeyGraphics(tracker.mCurrentKey);
408        }
409    }
410
411    private PointerTracker(final int id, final KeyEventHandler handler) {
412        if (handler == null) {
413            throw new NullPointerException();
414        }
415        mPointerId = id;
416        mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints(
417                id, sGestureStrokeParams);
418        setKeyDetectorInner(handler.getKeyDetector());
419        mListener = handler.getKeyboardActionListener();
420        mDrawingProxy = handler.getDrawingProxy();
421        mTimerProxy = handler.getTimerProxy();
422    }
423
424    // Returns true if keyboard has been changed by this callback.
425    private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) {
426        if (sInGesture) {
427            return false;
428        }
429        final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
430        if (DEBUG_LISTENER) {
431            Log.d(TAG, String.format("[%d] onPress    : %s%s%s", mPointerId,
432                    KeyDetector.printableCode(key),
433                    ignoreModifierKey ? " ignoreModifier" : "",
434                    key.isEnabled() ? "" : " disabled"));
435        }
436        if (ignoreModifierKey) {
437            return false;
438        }
439        if (key.isEnabled()) {
440            mListener.onPressKey(key.mCode);
441            final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
442            mKeyboardLayoutHasBeenChanged = false;
443            mTimerProxy.startTypingStateTimer(key);
444            return keyboardLayoutHasBeenChanged;
445        }
446        return false;
447    }
448
449    // Note that we need primaryCode argument because the keyboard may in shifted state and the
450    // primaryCode is different from {@link Key#mCode}.
451    private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x,
452            final int y, final long eventTime) {
453        final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
454        final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
455        final int code = altersCode ? key.getAltCode() : primaryCode;
456        if (DEBUG_LISTENER) {
457            final String output = code == Constants.CODE_OUTPUT_TEXT
458                    ? key.getOutputText() : Constants.printableCode(code);
459            Log.d(TAG, String.format("[%d] onCodeInput: %4d %4d %s%s%s", mPointerId, x, y,
460                    output, ignoreModifierKey ? " ignoreModifier" : "",
461                    altersCode ? " altersCode" : "", key.isEnabled() ? "" : " disabled"));
462        }
463        if (ProductionFlag.IS_EXPERIMENTAL) {
464            ResearchLogger.pointerTracker_callListenerOnCodeInput(key, x, y, ignoreModifierKey,
465                    altersCode, code);
466        }
467        if (ignoreModifierKey) {
468            return;
469        }
470        // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
471        if (key.isEnabled() || altersCode) {
472            sTimeRecorder.onCodeInput(code, eventTime);
473            if (code == Constants.CODE_OUTPUT_TEXT) {
474                mListener.onTextInput(key.getOutputText());
475            } else if (code != Constants.CODE_UNSPECIFIED) {
476                mListener.onCodeInput(code, x, y);
477            }
478        }
479    }
480
481    // Note that we need primaryCode argument because the keyboard may be in shifted state and the
482    // primaryCode is different from {@link Key#mCode}.
483    private void callListenerOnRelease(final Key key, final int primaryCode,
484            final boolean withSliding) {
485        if (sInGesture) {
486            return;
487        }
488        final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
489        if (DEBUG_LISTENER) {
490            Log.d(TAG, String.format("[%d] onRelease  : %s%s%s%s", mPointerId,
491                    Constants.printableCode(primaryCode),
492                    withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : "",
493                    key.isEnabled() ?  "": " disabled"));
494        }
495        if (ProductionFlag.IS_EXPERIMENTAL) {
496            ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding,
497                    ignoreModifierKey);
498        }
499        if (ignoreModifierKey) {
500            return;
501        }
502        if (key.isEnabled()) {
503            mListener.onReleaseKey(primaryCode, withSliding);
504        }
505    }
506
507    private void callListenerOnCancelInput() {
508        if (DEBUG_LISTENER) {
509            Log.d(TAG, String.format("[%d] onCancelInput", mPointerId));
510        }
511        if (ProductionFlag.IS_EXPERIMENTAL) {
512            ResearchLogger.pointerTracker_callListenerOnCancelInput();
513        }
514        mListener.onCancelInput();
515    }
516
517    private void setKeyDetectorInner(final KeyDetector keyDetector) {
518        final Keyboard keyboard = keyDetector.getKeyboard();
519        if (keyDetector == mKeyDetector && keyboard == mKeyboard) {
520            return;
521        }
522        mKeyDetector = keyDetector;
523        mKeyboard = keyDetector.getKeyboard();
524        final int keyWidth = mKeyboard.mMostCommonKeyWidth;
525        final int keyHeight = mKeyboard.mMostCommonKeyHeight;
526        mGestureStrokeWithPreviewPoints.setKeyboardGeometry(keyWidth);
527        final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY);
528        if (newKey != mCurrentKey) {
529            if (mDrawingProxy != null) {
530                setReleasedKeyGraphics(mCurrentKey);
531            }
532            // Keep {@link #mCurrentKey} that comes from previous keyboard.
533        }
534        mPhantonSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD);
535        mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight);
536    }
537
538    @Override
539    public boolean isInSlidingKeyInput() {
540        return mIsInSlidingKeyInput;
541    }
542
543    public Key getKey() {
544        return mCurrentKey;
545    }
546
547    @Override
548    public boolean isModifier() {
549        return mCurrentKey != null && mCurrentKey.isModifier();
550    }
551
552    public Key getKeyOn(final int x, final int y) {
553        return mKeyDetector.detectHitKey(x, y);
554    }
555
556    private void setReleasedKeyGraphics(final Key key) {
557        mDrawingProxy.dismissKeyPreview(this);
558        if (key == null) {
559            return;
560        }
561
562        // Even if the key is disabled, update the key release graphics just in case.
563        updateReleaseKeyGraphics(key);
564
565        if (key.isShift()) {
566            for (final Key shiftKey : mKeyboard.mShiftKeys) {
567                if (shiftKey != key) {
568                    updateReleaseKeyGraphics(shiftKey);
569                }
570            }
571        }
572
573        if (key.altCodeWhileTyping()) {
574            final int altCode = key.getAltCode();
575            final Key altKey = mKeyboard.getKey(altCode);
576            if (altKey != null) {
577                updateReleaseKeyGraphics(altKey);
578            }
579            for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
580                if (k != key && k.getAltCode() == altCode) {
581                    updateReleaseKeyGraphics(k);
582                }
583            }
584        }
585    }
586
587    private static boolean needsToSuppressKeyPreviewPopup(final long eventTime) {
588        if (!sShouldHandleGesture) return false;
589        return sTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime);
590    }
591
592    private void setPressedKeyGraphics(final Key key, final long eventTime) {
593        if (key == null) {
594            return;
595        }
596
597        // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
598        final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
599        final boolean needsToUpdateGraphics = key.isEnabled() || altersCode;
600        if (!needsToUpdateGraphics) {
601            return;
602        }
603
604        if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) {
605            mDrawingProxy.showKeyPreview(this);
606        }
607        updatePressKeyGraphics(key);
608
609        if (key.isShift()) {
610            for (final Key shiftKey : mKeyboard.mShiftKeys) {
611                if (shiftKey != key) {
612                    updatePressKeyGraphics(shiftKey);
613                }
614            }
615        }
616
617        if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) {
618            final int altCode = key.getAltCode();
619            final Key altKey = mKeyboard.getKey(altCode);
620            if (altKey != null) {
621                updatePressKeyGraphics(altKey);
622            }
623            for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
624                if (k != key && k.getAltCode() == altCode) {
625                    updatePressKeyGraphics(k);
626                }
627            }
628        }
629    }
630
631    private void updateReleaseKeyGraphics(final Key key) {
632        key.onReleased();
633        mDrawingProxy.invalidateKey(key);
634    }
635
636    private void updatePressKeyGraphics(final Key key) {
637        key.onPressed();
638        mDrawingProxy.invalidateKey(key);
639    }
640
641    public GestureStrokeWithPreviewPoints getGestureStrokeWithPreviewPoints() {
642        return mGestureStrokeWithPreviewPoints;
643    }
644
645    public int getLastX() {
646        return mLastX;
647    }
648
649    public int getLastY() {
650        return mLastY;
651    }
652
653    public long getDownTime() {
654        return mDownTime;
655    }
656
657    private Key onDownKey(final int x, final int y, final long eventTime) {
658        mDownTime = eventTime;
659        mBogusMoveEventDetector.onDownKey();
660        return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
661    }
662
663    static int getDistance(final int x1, final int y1, final int x2, final int y2) {
664        return (int)Math.hypot(x1 - x2, y1 - y2);
665    }
666
667    private Key onMoveKeyInternal(final int x, final int y) {
668        mBogusMoveEventDetector.onMoveKey(getDistance(x, y, mLastX, mLastY));
669        mLastX = x;
670        mLastY = y;
671        return mKeyDetector.detectHitKey(x, y);
672    }
673
674    private Key onMoveKey(final int x, final int y) {
675        return onMoveKeyInternal(x, y);
676    }
677
678    private Key onMoveToNewKey(final Key newKey, final int x, final int y) {
679        mCurrentKey = newKey;
680        mKeyX = x;
681        mKeyY = y;
682        return newKey;
683    }
684
685    private static int getActivePointerTrackerCount() {
686        return (sPointerTrackerQueue == null) ? 1 : sPointerTrackerQueue.size();
687    }
688
689    private void mayStartBatchInput(final Key key) {
690        if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) {
691            return;
692        }
693        if (key == null || !Character.isLetter(key.mCode)) {
694            return;
695        }
696        if (DEBUG_LISTENER) {
697            Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId));
698        }
699        sInGesture = true;
700        synchronized (sAggregratedPointers) {
701            sAggregratedPointers.reset();
702            sLastRecognitionPointSize = 0;
703            sLastRecognitionTime = 0;
704            mListener.onStartBatchInput();
705        }
706        mTimerProxy.cancelLongPressTimer();
707        final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
708        mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
709    }
710
711    private void mayUpdateBatchInput(final long eventTime, final Key key) {
712        if (key != null) {
713            synchronized (sAggregratedPointers) {
714                final GestureStroke stroke = mGestureStrokeWithPreviewPoints;
715                stroke.appendIncrementalBatchPoints(sAggregratedPointers);
716                final int size = sAggregratedPointers.getPointerSize();
717                if (size > sLastRecognitionPointSize
718                        && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) {
719                    sLastRecognitionPointSize = size;
720                    sLastRecognitionTime = eventTime;
721                    if (DEBUG_LISTENER) {
722                        Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d",
723                                mPointerId, size));
724                    }
725                    mListener.onUpdateBatchInput(sAggregratedPointers);
726                }
727            }
728        }
729        final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
730        mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
731    }
732
733    private void mayEndBatchInput(final long eventTime) {
734        synchronized (sAggregratedPointers) {
735            mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers);
736            if (getActivePointerTrackerCount() == 1) {
737                if (DEBUG_LISTENER) {
738                    Log.d(TAG, String.format("[%d] onEndBatchInput   : batchPoints=%d",
739                            mPointerId, sAggregratedPointers.getPointerSize()));
740                }
741                sInGesture = false;
742                sTimeRecorder.onEndBatchInput(eventTime);
743                mListener.onEndBatchInput(sAggregratedPointers);
744            }
745        }
746        final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
747        mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
748    }
749
750    public void processMotionEvent(final int action, final int x, final int y, final long eventTime,
751            final KeyEventHandler handler) {
752        switch (action) {
753        case MotionEvent.ACTION_DOWN:
754        case MotionEvent.ACTION_POINTER_DOWN:
755            onDownEvent(x, y, eventTime, handler);
756            break;
757        case MotionEvent.ACTION_UP:
758        case MotionEvent.ACTION_POINTER_UP:
759            onUpEvent(x, y, eventTime);
760            break;
761        case MotionEvent.ACTION_MOVE:
762            onMoveEvent(x, y, eventTime, null);
763            break;
764        case MotionEvent.ACTION_CANCEL:
765            onCancelEvent(x, y, eventTime);
766            break;
767        }
768    }
769
770    public void onDownEvent(final int x, final int y, final long eventTime,
771            final KeyEventHandler handler) {
772        if (DEBUG_EVENT) {
773            printTouchEvent("onDownEvent:", x, y, eventTime);
774        }
775
776        mDrawingProxy = handler.getDrawingProxy();
777        mTimerProxy = handler.getTimerProxy();
778        setKeyboardActionListener(handler.getKeyboardActionListener());
779        setKeyDetectorInner(handler.getKeyDetector());
780        // Naive up-to-down noise filter.
781        final long deltaT = eventTime - mUpTime;
782        if (deltaT < sParams.mTouchNoiseThresholdTime) {
783            final int distance = getDistance(x, y, mLastX, mLastY);
784            if (distance < sParams.mTouchNoiseThresholdDistance) {
785                if (DEBUG_MODE)
786                    Log.w(TAG, String.format("[%d] onDownEvent:"
787                            + " ignore potential noise: time=%d distance=%d",
788                            mPointerId, deltaT, distance));
789                if (ProductionFlag.IS_EXPERIMENTAL) {
790                    ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance);
791                }
792                mKeyAlreadyProcessed = true;
793                return;
794            }
795        }
796
797        final Key key = getKeyOn(x, y);
798        mBogusMoveEventDetector.onActualDownEvent(x, y);
799        final PointerTrackerQueue queue = sPointerTrackerQueue;
800        if (queue != null) {
801            if (key != null && key.isModifier()) {
802                // Before processing a down event of modifier key, all pointers already being
803                // tracked should be released.
804                queue.releaseAllPointers(eventTime);
805            }
806            queue.add(this);
807        }
808        onDownEventInternal(x, y, eventTime);
809        if (!sShouldHandleGesture) {
810            return;
811        }
812        // A gesture should start only from a non-modifier key.
813        mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard()
814                && !mIsShowingMoreKeysPanel && key != null && !key.isModifier();
815        if (mIsDetectingGesture) {
816            if (getActivePointerTrackerCount() == 1) {
817                sGestureFirstDownTime = eventTime;
818            }
819            mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime,
820                    sTimeRecorder.getLastLetterTypingTime());
821        }
822    }
823
824    private void onDownEventInternal(final int x, final int y, final long eventTime) {
825        Key key = onDownKey(x, y, eventTime);
826        // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
827        // from modifier key, or 3) this pointer's KeyDetector always allows sliding input.
828        mIsAllowedSlidingKeyInput = sParams.mSlidingKeyInputEnabled
829                || (key != null && key.isModifier())
830                || mKeyDetector.alwaysAllowsSlidingInput();
831        mKeyboardLayoutHasBeenChanged = false;
832        mKeyAlreadyProcessed = false;
833        resetSlidingKeyInput();
834        if (key != null) {
835            // This onPress call may have changed keyboard layout. Those cases are detected at
836            // {@link #setKeyboard}. In those cases, we should update key according to the new
837            // keyboard layout.
838            if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
839                key = onDownKey(x, y, eventTime);
840            }
841
842            startRepeatKey(key);
843            startLongPressTimer(key);
844            setPressedKeyGraphics(key, eventTime);
845        }
846    }
847
848    private void startSlidingKeyInput(final Key key) {
849        if (!mIsInSlidingKeyInput) {
850            mIsInSlidingKeyInputFromModifier = key.isModifier();
851        }
852        mIsInSlidingKeyInput = true;
853    }
854
855    private void resetSlidingKeyInput() {
856        mIsInSlidingKeyInput = false;
857        mIsInSlidingKeyInputFromModifier = false;
858    }
859
860    private void onGestureMoveEvent(final int x, final int y, final long eventTime,
861            final boolean isMajorEvent, final Key key) {
862        final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
863        if (mIsDetectingGesture) {
864            mGestureStrokeWithPreviewPoints.addPoint(x, y, gestureTime, isMajorEvent);
865            mayStartBatchInput(key);
866            if (sInGesture) {
867                mayUpdateBatchInput(eventTime, key);
868            }
869        }
870    }
871
872    public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
873        if (DEBUG_MOVE_EVENT) {
874            printTouchEvent("onMoveEvent:", x, y, eventTime);
875        }
876        if (mKeyAlreadyProcessed) {
877            return;
878        }
879
880        if (sShouldHandleGesture && me != null) {
881            // Add historical points to gesture path.
882            final int pointerIndex = me.findPointerIndex(mPointerId);
883            final int historicalSize = me.getHistorySize();
884            for (int h = 0; h < historicalSize; h++) {
885                final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
886                final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
887                final long historicalTime = me.getHistoricalEventTime(h);
888                onGestureMoveEvent(historicalX, historicalY, historicalTime,
889                        false /* isMajorEvent */, null);
890            }
891        }
892
893        onMoveEventInternal(x, y, eventTime);
894    }
895
896    private void slideInToNewKey(final Key newKey, final int x, final int y, final long eventTime) {
897        // The pointer has been slid in to the new key, but the finger was not on any keys.
898        // In this case, we must call onPress() to notify that the new key is being pressed.
899        // This onPress call may have changed keyboard layout. Those cases are detected at
900        // {@link #setKeyboard}. In those cases, we should update key according to the
901        // new keyboard layout.
902        Key key = newKey;
903        if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
904            key = onMoveKey(x, y);
905        }
906        onMoveToNewKey(key, x, y);
907        startLongPressTimer(key);
908        setPressedKeyGraphics(key, eventTime);
909    }
910
911    private void slideFromOldKeyToNewKey(final Key newKey, final int x, final int y,
912            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
913        // The pointer has been slid in to the new key from the previous key, we must call
914        // onRelease() first to notify that the previous key has been released, then call
915        // onPress() to notify that the new key is being pressed.
916        Key key = newKey;
917        setReleasedKeyGraphics(oldKey);
918        callListenerOnRelease(oldKey, oldKey.mCode, true);
919        startSlidingKeyInput(oldKey);
920        mTimerProxy.cancelKeyTimers();
921        startRepeatKey(key);
922        if (mIsAllowedSlidingKeyInput) {
923            // This onPress call may have changed keyboard layout. Those cases are detected
924            // at {@link #setKeyboard}. In those cases, we should update key according
925            // to the new keyboard layout.
926            if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
927                key = onMoveKey(x, y);
928            }
929            onMoveToNewKey(key, x, y);
930            startLongPressTimer(key);
931            setPressedKeyGraphics(key, eventTime);
932        } else {
933            // HACK: On some devices, quick successive touches may be reported as a sudden
934            // move by touch panel firmware. This hack detects such cases and translates the
935            // move event to successive up and down events.
936            // TODO: Should find a way to balance gesture detection and this hack.
937            if (sNeedsPhantomSuddenMoveEventHack
938                    && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) {
939                if (DEBUG_MODE) {
940                    Log.w(TAG, String.format("[%d] onMoveEvent:"
941                            + " phantom sudden move event (distance=%d) is translated to "
942                            + "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId,
943                            getDistance(x, y, lastX, lastY),
944                            lastX, lastY, Constants.printableCode(oldKey.mCode),
945                            x, y, Constants.printableCode(key.mCode)));
946                }
947                // TODO: This should be moved to outside of this nested if-clause?
948                if (ProductionFlag.IS_EXPERIMENTAL) {
949                    ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
950                }
951                onUpEventInternal(eventTime);
952                onDownEventInternal(x, y, eventTime);
953            }
954            // HACK: On some devices, quick successive proximate touches may be reported as
955            // a bogus down-move-up event by touch panel firmware. This hack detects such
956            // cases and breaks these events into separate up and down events.
957            else if (sNeedsProximateBogusDownMoveUpEventHack
958                    && sTimeRecorder.isInFastTyping(eventTime)
959                    && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) {
960                if (DEBUG_MODE) {
961                    final float keyDiagonal = (float)Math.hypot(
962                            mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
963                    final float radiusRatio =
964                            mBogusMoveEventDetector.getDistanceFromDownEvent(x, y)
965                            / keyDiagonal;
966                    Log.w(TAG, String.format("[%d] onMoveEvent:"
967                            + " bogus down-move-up event (raidus=%.2f key diagonal) is "
968                            + " translated to up[%d,%d,%s]/down[%d,%d,%s] events",
969                            mPointerId, radiusRatio,
970                            lastX, lastY, Constants.printableCode(oldKey.mCode),
971                            x, y, Constants.printableCode(key.mCode)));
972                }
973                onUpEventInternal(eventTime);
974                onDownEventInternal(x, y, eventTime);
975            } else {
976                // HACK: If there are currently multiple touches, register the key even if
977                // the finger slides off the key. This defends against noise from some
978                // touch panels when there are close multiple touches.
979                // Caveat: When in chording input mode with a modifier key, we don't use
980                // this hack.
981                if (getActivePointerTrackerCount() > 1 && sPointerTrackerQueue != null
982                        && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
983                    if (DEBUG_MODE) {
984                        Log.w(TAG, String.format("[%d] onMoveEvent:"
985                                + " detected sliding finger while multi touching",
986                                mPointerId));
987                    }
988                    onUpEvent(x, y, eventTime);
989                    mKeyAlreadyProcessed = true;
990                }
991                if (!mIsDetectingGesture) {
992                    mKeyAlreadyProcessed = true;
993                }
994                setReleasedKeyGraphics(oldKey);
995            }
996        }
997    }
998
999    private void slideOutFromOldKey(final Key oldKey, final int x, final int y) {
1000        // The pointer has been slid out from the previous key, we must call onRelease() to
1001        // notify that the previous key has been released.
1002        setReleasedKeyGraphics(oldKey);
1003        callListenerOnRelease(oldKey, oldKey.mCode, true);
1004        startSlidingKeyInput(oldKey);
1005        mTimerProxy.cancelLongPressTimer();
1006        if (mIsAllowedSlidingKeyInput) {
1007            onMoveToNewKey(null, x, y);
1008        } else {
1009            if (!mIsDetectingGesture) {
1010                mKeyAlreadyProcessed = true;
1011            }
1012        }
1013    }
1014
1015    private void onMoveEventInternal(final int x, final int y, final long eventTime) {
1016        final int lastX = mLastX;
1017        final int lastY = mLastY;
1018        final Key oldKey = mCurrentKey;
1019        final Key newKey = onMoveKey(x, y);
1020
1021        if (sShouldHandleGesture) {
1022            // Register move event on gesture tracker.
1023            onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey);
1024            if (sInGesture) {
1025                mCurrentKey = null;
1026                setReleasedKeyGraphics(oldKey);
1027                return;
1028            }
1029        }
1030
1031        if (newKey != null) {
1032            if (oldKey == null) {
1033                slideInToNewKey(newKey, x, y, eventTime);
1034            } else if (isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1035                slideFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY);
1036            }
1037        } else {
1038            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1039                slideOutFromOldKey(oldKey, x, y);
1040            }
1041        }
1042    }
1043
1044    public void onUpEvent(final int x, final int y, final long eventTime) {
1045        if (DEBUG_EVENT) {
1046            printTouchEvent("onUpEvent  :", x, y, eventTime);
1047        }
1048
1049        final PointerTrackerQueue queue = sPointerTrackerQueue;
1050        if (queue != null) {
1051            if (!sInGesture) {
1052                if (mCurrentKey != null && mCurrentKey.isModifier()) {
1053                    // Before processing an up event of modifier key, all pointers already being
1054                    // tracked should be released.
1055                    queue.releaseAllPointersExcept(this, eventTime);
1056                } else {
1057                    queue.releaseAllPointersOlderThan(this, eventTime);
1058                }
1059            }
1060        }
1061        onUpEventInternal(eventTime);
1062        if (queue != null) {
1063            queue.remove(this);
1064        }
1065    }
1066
1067    // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
1068    // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
1069    // "virtual" up event.
1070    @Override
1071    public void onPhantomUpEvent(final long eventTime) {
1072        if (DEBUG_EVENT) {
1073            printTouchEvent("onPhntEvent:", getLastX(), getLastY(), eventTime);
1074        }
1075        onUpEventInternal(eventTime);
1076        mKeyAlreadyProcessed = true;
1077    }
1078
1079    private void onUpEventInternal(final long eventTime) {
1080        mTimerProxy.cancelKeyTimers();
1081        resetSlidingKeyInput();
1082        mIsDetectingGesture = false;
1083        final Key currentKey = mCurrentKey;
1084        mCurrentKey = null;
1085        // Release the last pressed key.
1086        setReleasedKeyGraphics(currentKey);
1087        if (mIsShowingMoreKeysPanel) {
1088            mDrawingProxy.dismissMoreKeysPanel();
1089            mIsShowingMoreKeysPanel = false;
1090        }
1091
1092        if (sInGesture) {
1093            if (currentKey != null) {
1094                callListenerOnRelease(currentKey, currentKey.mCode, true);
1095            }
1096            mayEndBatchInput(eventTime);
1097            return;
1098        }
1099
1100        if (mKeyAlreadyProcessed) {
1101            return;
1102        }
1103        if (currentKey != null && !currentKey.isRepeatable()) {
1104            detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
1105        }
1106    }
1107
1108    public void onShowMoreKeysPanel(final int x, final int y, final KeyEventHandler handler) {
1109        onLongPressed();
1110        mIsShowingMoreKeysPanel = true;
1111        onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
1112    }
1113
1114    public void onLongPressed() {
1115        mKeyAlreadyProcessed = true;
1116        setReleasedKeyGraphics(mCurrentKey);
1117        final PointerTrackerQueue queue = sPointerTrackerQueue;
1118        if (queue != null) {
1119            queue.remove(this);
1120        }
1121    }
1122
1123    public void onCancelEvent(final int x, final int y, final long eventTime) {
1124        if (DEBUG_EVENT) {
1125            printTouchEvent("onCancelEvt:", x, y, eventTime);
1126        }
1127
1128        final PointerTrackerQueue queue = sPointerTrackerQueue;
1129        if (queue != null) {
1130            queue.releaseAllPointersExcept(this, eventTime);
1131            queue.remove(this);
1132        }
1133        onCancelEventInternal();
1134    }
1135
1136    private void onCancelEventInternal() {
1137        mTimerProxy.cancelKeyTimers();
1138        setReleasedKeyGraphics(mCurrentKey);
1139        resetSlidingKeyInput();
1140        if (mIsShowingMoreKeysPanel) {
1141            mDrawingProxy.dismissMoreKeysPanel();
1142            mIsShowingMoreKeysPanel = false;
1143        }
1144    }
1145
1146    private void startRepeatKey(final Key key) {
1147        if (key != null && key.isRepeatable() && !sInGesture) {
1148            onRegisterKey(key);
1149            mTimerProxy.startKeyRepeatTimer(this);
1150        }
1151    }
1152
1153    public void onRegisterKey(final Key key) {
1154        if (key != null) {
1155            detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
1156            mTimerProxy.startTypingStateTimer(key);
1157        }
1158    }
1159
1160    private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
1161            final Key newKey) {
1162        if (mKeyDetector == null) {
1163            throw new NullPointerException("keyboard and/or key detector not set");
1164        }
1165        final Key curKey = mCurrentKey;
1166        if (newKey == curKey) {
1167            return false;
1168        } else if (curKey != null) {
1169            final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared(
1170                    mIsInSlidingKeyInputFromModifier);
1171            final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y);
1172            if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) {
1173                if (DEBUG_MODE) {
1174                    final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared)
1175                            / mKeyboard.mMostCommonKeyWidth;
1176                    Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1177                            +" %.2f key width from key edge",
1178                            mPointerId, distanceToEdgeRatio));
1179                }
1180                return true;
1181            }
1182            if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput
1183                    && sTimeRecorder.isInFastTyping(eventTime)
1184                    && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) {
1185                if (DEBUG_MODE) {
1186                    final float keyDiagonal = (float)Math.hypot(
1187                            mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1188                    final float lengthFromDownRatio =
1189                            mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal;
1190                    Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1191                            + " %.2f key diagonal from virtual down point",
1192                            mPointerId, lengthFromDownRatio));
1193                }
1194                return true;
1195            }
1196            return false;
1197        } else { // curKey == null && newKey != null
1198            return true;
1199        }
1200    }
1201
1202    private void startLongPressTimer(final Key key) {
1203        if (key != null && key.isLongPressEnabled() && !sInGesture) {
1204            mTimerProxy.startLongPressTimer(this);
1205        }
1206    }
1207
1208    private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
1209        if (key == null) {
1210            callListenerOnCancelInput();
1211            return;
1212        }
1213
1214        final int code = key.mCode;
1215        callListenerOnCodeInput(key, code, x, y, eventTime);
1216        callListenerOnRelease(key, code, false);
1217    }
1218
1219    private void printTouchEvent(final String title, final int x, final int y,
1220            final long eventTime) {
1221        final Key key = mKeyDetector.detectHitKey(x, y);
1222        final String code = KeyDetector.printableCode(key);
1223        Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
1224                (mKeyAlreadyProcessed ? "-" : " "), title, x, y, eventTime, code));
1225    }
1226}
1227