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