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