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