PointerTracker.java revision ac69ab400d1ea4f90b4ca24486d62212decf1069
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        sInGesture = false;
795        if (DEBUG_LISTENER) {
796            Log.d(TAG, String.format("[%d] onCancelBatchInput", mPointerId));
797        }
798        mListener.onCancelBatchInput();
799    }
800
801    public void processMotionEvent(final int action, final int x, final int y, final long eventTime,
802            final KeyEventHandler handler) {
803        switch (action) {
804        case MotionEvent.ACTION_DOWN:
805        case MotionEvent.ACTION_POINTER_DOWN:
806            onDownEvent(x, y, eventTime, handler);
807            break;
808        case MotionEvent.ACTION_UP:
809        case MotionEvent.ACTION_POINTER_UP:
810            onUpEvent(x, y, eventTime);
811            break;
812        case MotionEvent.ACTION_MOVE:
813            onMoveEvent(x, y, eventTime, null);
814            break;
815        case MotionEvent.ACTION_CANCEL:
816            onCancelEvent(x, y, eventTime);
817            break;
818        }
819    }
820
821    public void onDownEvent(final int x, final int y, final long eventTime,
822            final KeyEventHandler handler) {
823        if (DEBUG_EVENT) {
824            printTouchEvent("onDownEvent:", x, y, eventTime);
825        }
826        mDrawingProxy = handler.getDrawingProxy();
827        mTimerProxy = handler.getTimerProxy();
828        setKeyboardActionListener(handler.getKeyboardActionListener());
829        setKeyDetectorInner(handler.getKeyDetector());
830        // Naive up-to-down noise filter.
831        final long deltaT = eventTime - mUpTime;
832        if (deltaT < sParams.mTouchNoiseThresholdTime) {
833            final int distance = getDistance(x, y, mLastX, mLastY);
834            if (distance < sParams.mTouchNoiseThresholdDistance) {
835                if (DEBUG_MODE)
836                    Log.w(TAG, String.format("[%d] onDownEvent:"
837                            + " ignore potential noise: time=%d distance=%d",
838                            mPointerId, deltaT, distance));
839                if (ProductionFlag.IS_EXPERIMENTAL) {
840                    ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance);
841                }
842                cancelTracking();
843                return;
844            }
845        }
846
847        final Key key = getKeyOn(x, y);
848        mBogusMoveEventDetector.onActualDownEvent(x, y);
849        if (key != null && key.isModifier()) {
850            // Before processing a down event of modifier key, all pointers already being
851            // tracked should be released.
852            sPointerTrackerQueue.releaseAllPointers(eventTime);
853        }
854        sPointerTrackerQueue.add(this);
855        onDownEventInternal(x, y, eventTime);
856        if (!sShouldHandleGesture) {
857            return;
858        }
859        // A gesture should start only from a non-modifier key.
860        mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard()
861                && key != null && !key.isModifier();
862        if (mIsDetectingGesture) {
863            if (getActivePointerTrackerCount() == 1) {
864                sGestureFirstDownTime = eventTime;
865            }
866            mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime,
867                    sTimeRecorder.getLastLetterTypingTime());
868        }
869    }
870
871    private boolean isShowingMoreKeysPanel() {
872        return (mMoreKeysPanel != null);
873    }
874
875    private void onDownEventInternal(final int x, final int y, final long eventTime) {
876        Key key = onDownKey(x, y, eventTime);
877        // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
878        // from modifier key, or 3) this pointer's KeyDetector always allows sliding input.
879        mIsAllowedSlidingKeyInput = sParams.mSlidingKeyInputEnabled
880                || (key != null && key.isModifier())
881                || mKeyDetector.alwaysAllowsSlidingInput();
882        mKeyboardLayoutHasBeenChanged = false;
883        mIsTrackingCanceled = false;
884        resetSlidingKeyInput();
885        if (key != null) {
886            // This onPress call may have changed keyboard layout. Those cases are detected at
887            // {@link #setKeyboard}. In those cases, we should update key according to the new
888            // keyboard layout.
889            if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
890                key = onDownKey(x, y, eventTime);
891            }
892
893            startRepeatKey(key);
894            startLongPressTimer(key);
895            setPressedKeyGraphics(key, eventTime);
896        }
897    }
898
899    private void startSlidingKeyInput(final Key key) {
900        if (!mIsInSlidingKeyInput) {
901            mIsInSlidingKeyInputFromModifier = key.isModifier();
902        }
903        mIsInSlidingKeyInput = true;
904    }
905
906    private void resetSlidingKeyInput() {
907        mIsInSlidingKeyInput = false;
908        mIsInSlidingKeyInputFromModifier = false;
909        mDrawingProxy.dismissSlidingKeyInputPreview();
910    }
911
912    private void onGestureMoveEvent(final int x, final int y, final long eventTime,
913            final boolean isMajorEvent, final Key key) {
914        final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
915        if (mIsDetectingGesture) {
916            final boolean onValidArea = mGestureStrokeWithPreviewPoints.addPointOnKeyboard(
917                    x, y, gestureTime, isMajorEvent);
918            if (!onValidArea) {
919                cancelBatchInput();
920                return;
921            }
922            // If the MoreKeysPanel is showing then do not attempt to enter gesture mode. However,
923            // the gestured touch points are still being recorded in case the panel is dismissed.
924            if (isShowingMoreKeysPanel()) {
925                return;
926            }
927            mayStartBatchInput(key);
928            if (sInGesture) {
929                mayUpdateBatchInput(eventTime, key);
930            }
931        }
932    }
933
934    public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
935        if (DEBUG_MOVE_EVENT) {
936            printTouchEvent("onMoveEvent:", x, y, eventTime);
937        }
938        if (mIsTrackingCanceled) {
939            return;
940        }
941
942        if (isShowingMoreKeysPanel()) {
943            final int translatedX = mMoreKeysPanel.translateX(x);
944            final int translatedY = mMoreKeysPanel.translateY(y);
945            mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime);
946        }
947
948        if (sShouldHandleGesture && me != null) {
949            // Add historical points to gesture path.
950            final int pointerIndex = me.findPointerIndex(mPointerId);
951            final int historicalSize = me.getHistorySize();
952            for (int h = 0; h < historicalSize; h++) {
953                final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
954                final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
955                final long historicalTime = me.getHistoricalEventTime(h);
956                onGestureMoveEvent(historicalX, historicalY, historicalTime,
957                        false /* isMajorEvent */, null);
958            }
959        }
960
961        if (isShowingMoreKeysPanel()) {
962            // Do not handle sliding keys (or show key pop-ups) when the MoreKeysPanel is visible.
963            return;
964        }
965        onMoveEventInternal(x, y, eventTime);
966    }
967
968    private void processSlidingKeyInput(final Key newKey, final int x, final int y,
969            final long eventTime) {
970        // This onPress call may have changed keyboard layout. Those cases are detected
971        // at {@link #setKeyboard}. In those cases, we should update key according
972        // to the new keyboard layout.
973        Key key = newKey;
974        if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
975            key = onMoveKey(x, y);
976        }
977        onMoveToNewKey(key, x, y);
978        startLongPressTimer(key);
979        setPressedKeyGraphics(key, eventTime);
980    }
981
982    private void processPhantomSuddenMoveHack(final Key key, final int x, final int y,
983            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
984        if (DEBUG_MODE) {
985            Log.w(TAG, String.format("[%d] onMoveEvent:"
986                    + " phantom sudden move event (distance=%d) is translated to "
987                    + "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId,
988                    getDistance(x, y, lastX, lastY),
989                    lastX, lastY, Constants.printableCode(oldKey.mCode),
990                    x, y, Constants.printableCode(key.mCode)));
991        }
992        // TODO: This should be moved to outside of this nested if-clause?
993        if (ProductionFlag.IS_EXPERIMENTAL) {
994            ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
995        }
996        onUpEventInternal(x, y, eventTime);
997        onDownEventInternal(x, y, eventTime);
998    }
999
1000    private void processProximateBogusDownMoveUpEventHack(final Key key, final int x, final int y,
1001            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1002        if (DEBUG_MODE) {
1003            final float keyDiagonal = (float)Math.hypot(
1004                    mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1005            final float radiusRatio =
1006                    mBogusMoveEventDetector.getDistanceFromDownEvent(x, y)
1007                    / keyDiagonal;
1008            Log.w(TAG, String.format("[%d] onMoveEvent:"
1009                    + " bogus down-move-up event (raidus=%.2f key diagonal) is "
1010                    + " translated to up[%d,%d,%s]/down[%d,%d,%s] events",
1011                    mPointerId, radiusRatio,
1012                    lastX, lastY, Constants.printableCode(oldKey.mCode),
1013                    x, y, Constants.printableCode(key.mCode)));
1014        }
1015        onUpEventInternal(x, y, eventTime);
1016        onDownEventInternal(x, y, eventTime);
1017    }
1018
1019    private void processSildeOutFromOldKey(final Key oldKey) {
1020        setReleasedKeyGraphics(oldKey);
1021        callListenerOnRelease(oldKey, oldKey.mCode, true);
1022        startSlidingKeyInput(oldKey);
1023        mTimerProxy.cancelKeyTimers();
1024    }
1025
1026    private void slideFromOldKeyToNewKey(final Key key, final int x, final int y,
1027            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1028        // The pointer has been slid in to the new key from the previous key, we must call
1029        // onRelease() first to notify that the previous key has been released, then call
1030        // onPress() to notify that the new key is being pressed.
1031        processSildeOutFromOldKey(oldKey);
1032        startRepeatKey(key);
1033        if (mIsAllowedSlidingKeyInput) {
1034            processSlidingKeyInput(key, x, y, eventTime);
1035        }
1036        // HACK: On some devices, quick successive touches may be reported as a sudden move by
1037        // touch panel firmware. This hack detects such cases and translates the move event to
1038        // successive up and down events.
1039        // TODO: Should find a way to balance gesture detection and this hack.
1040        else if (sNeedsPhantomSuddenMoveEventHack
1041                && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) {
1042            processPhantomSuddenMoveHack(key, x, y, eventTime, oldKey, lastX, lastY);
1043        }
1044        // HACK: On some devices, quick successive proximate touches may be reported as a bogus
1045        // down-move-up event by touch panel firmware. This hack detects such cases and breaks
1046        // these events into separate up and down events.
1047        else if (sNeedsProximateBogusDownMoveUpEventHack && sTimeRecorder.isInFastTyping(eventTime)
1048                && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) {
1049            processProximateBogusDownMoveUpEventHack(key, x, y, eventTime, oldKey, lastX, lastY);
1050        }
1051        // HACK: If there are currently multiple touches, register the key even if the finger
1052        // slides off the key. This defends against noise from some touch panels when there are
1053        // close multiple touches.
1054        // Caveat: When in chording input mode with a modifier key, we don't use this hack.
1055        else if (getActivePointerTrackerCount() > 1
1056                && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
1057            if (DEBUG_MODE) {
1058                Log.w(TAG, String.format("[%d] onMoveEvent:"
1059                        + " detected sliding finger while multi touching", mPointerId));
1060            }
1061            onUpEvent(x, y, eventTime);
1062            cancelTracking();
1063            setReleasedKeyGraphics(oldKey);
1064        } else {
1065            if (!mIsDetectingGesture) {
1066                cancelTracking();
1067            }
1068            setReleasedKeyGraphics(oldKey);
1069        }
1070    }
1071
1072    private void slideOutFromOldKey(final Key oldKey, final int x, final int y) {
1073        // The pointer has been slid out from the previous key, we must call onRelease() to
1074        // notify that the previous key has been released.
1075        processSildeOutFromOldKey(oldKey);
1076        if (mIsAllowedSlidingKeyInput) {
1077            onMoveToNewKey(null, x, y);
1078        } else {
1079            if (!mIsDetectingGesture) {
1080                cancelTracking();
1081            }
1082        }
1083    }
1084
1085    private void onMoveEventInternal(final int x, final int y, final long eventTime) {
1086        final int lastX = mLastX;
1087        final int lastY = mLastY;
1088        final Key oldKey = mCurrentKey;
1089        final Key newKey = onMoveKey(x, y);
1090
1091        if (sShouldHandleGesture) {
1092            // Register move event on gesture tracker.
1093            onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey);
1094            if (sInGesture) {
1095                mCurrentKey = null;
1096                setReleasedKeyGraphics(oldKey);
1097                return;
1098            }
1099        }
1100
1101        if (newKey != null) {
1102            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1103                slideFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY);
1104            } else if (oldKey == null) {
1105                // The pointer has been slid in to the new key, but the finger was not on any keys.
1106                // In this case, we must call onPress() to notify that the new key is being pressed.
1107                processSlidingKeyInput(newKey, x, y, eventTime);
1108            }
1109        } else { // newKey == null
1110            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1111                slideOutFromOldKey(oldKey, x, y);
1112            }
1113        }
1114        mDrawingProxy.showSlidingKeyInputPreview(this);
1115    }
1116
1117    public void onUpEvent(final int x, final int y, final long eventTime) {
1118        if (DEBUG_EVENT) {
1119            printTouchEvent("onUpEvent  :", x, y, eventTime);
1120        }
1121
1122        if (!sInGesture) {
1123            if (mCurrentKey != null && mCurrentKey.isModifier()) {
1124                // Before processing an up event of modifier key, all pointers already being
1125                // tracked should be released.
1126                sPointerTrackerQueue.releaseAllPointersExcept(this, eventTime);
1127            } else {
1128                sPointerTrackerQueue.releaseAllPointersOlderThan(this, eventTime);
1129            }
1130        }
1131        onUpEventInternal(x, y, eventTime);
1132        sPointerTrackerQueue.remove(this);
1133    }
1134
1135    // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
1136    // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
1137    // "virtual" up event.
1138    @Override
1139    public void onPhantomUpEvent(final long eventTime) {
1140        if (DEBUG_EVENT) {
1141            printTouchEvent("onPhntEvent:", mLastX, mLastY, eventTime);
1142        }
1143        if (isShowingMoreKeysPanel()) {
1144            return;
1145        }
1146        onUpEventInternal(mLastX, mLastY, eventTime);
1147        cancelTracking();
1148    }
1149
1150    private void onUpEventInternal(final int x, final int y, final long eventTime) {
1151        mTimerProxy.cancelKeyTimers();
1152        resetSlidingKeyInput();
1153        mIsDetectingGesture = false;
1154        final Key currentKey = mCurrentKey;
1155        mCurrentKey = null;
1156        // Release the last pressed key.
1157        setReleasedKeyGraphics(currentKey);
1158
1159        if (isShowingMoreKeysPanel()) {
1160            if (!mIsTrackingCanceled) {
1161                final int translatedX = mMoreKeysPanel.translateX(x);
1162                final int translatedY = mMoreKeysPanel.translateY(y);
1163                mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId, eventTime);
1164            }
1165            mMoreKeysPanel.dismissMoreKeysPanel();
1166            mMoreKeysPanel = null;
1167            return;
1168        }
1169
1170        if (sInGesture) {
1171            if (currentKey != null) {
1172                callListenerOnRelease(currentKey, currentKey.mCode, true);
1173            }
1174            mayEndBatchInput(eventTime);
1175            return;
1176        }
1177
1178        if (mIsTrackingCanceled) {
1179            return;
1180        }
1181        if (currentKey != null && !currentKey.isRepeatable()) {
1182            detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
1183        }
1184    }
1185
1186    public void onShowMoreKeysPanel(final int translatedX, final int translatedY,
1187                final MoreKeysPanel panel) {
1188        setReleasedKeyGraphics(mCurrentKey);
1189        final long eventTime = SystemClock.uptimeMillis();
1190        mMoreKeysPanel = panel;
1191        mMoreKeysPanel.onDownEvent(translatedX, translatedY, mPointerId, eventTime);
1192    }
1193
1194    @Override
1195    public void cancelTracking() {
1196        mIsTrackingCanceled = true;
1197    }
1198
1199    public void onLongPressed() {
1200        resetSlidingKeyInput();
1201        cancelTracking();
1202        setReleasedKeyGraphics(mCurrentKey);
1203        sPointerTrackerQueue.remove(this);
1204    }
1205
1206    public void onCancelEvent(final int x, final int y, final long eventTime) {
1207        if (DEBUG_EVENT) {
1208            printTouchEvent("onCancelEvt:", x, y, eventTime);
1209        }
1210
1211        if (sInGesture) {
1212            cancelBatchInput();
1213        }
1214        sPointerTrackerQueue.cancelAllPointerTracker();
1215        sPointerTrackerQueue.releaseAllPointers(eventTime);
1216        onCancelEventInternal();
1217    }
1218
1219    private void onCancelEventInternal() {
1220        mTimerProxy.cancelKeyTimers();
1221        setReleasedKeyGraphics(mCurrentKey);
1222        resetSlidingKeyInput();
1223        if (isShowingMoreKeysPanel()) {
1224            mMoreKeysPanel.dismissMoreKeysPanel();
1225            mMoreKeysPanel = null;
1226        }
1227    }
1228
1229    private void startRepeatKey(final Key key) {
1230        if (key != null && key.isRepeatable() && !sInGesture) {
1231            onRegisterKey(key);
1232            mTimerProxy.startKeyRepeatTimer(this);
1233        }
1234    }
1235
1236    public void onRegisterKey(final Key key) {
1237        if (key != null) {
1238            detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
1239            mTimerProxy.startTypingStateTimer(key);
1240        }
1241    }
1242
1243    private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
1244            final Key newKey) {
1245        if (mKeyDetector == null) {
1246            throw new NullPointerException("keyboard and/or key detector not set");
1247        }
1248        final Key curKey = mCurrentKey;
1249        if (newKey == curKey) {
1250            return false;
1251        }
1252        if (curKey == null /* && newKey != null */) {
1253            return true;
1254        }
1255        // Here curKey points to the different key from newKey.
1256        final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared(
1257                mIsInSlidingKeyInputFromModifier);
1258        final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y);
1259        if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) {
1260            if (DEBUG_MODE) {
1261                final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared)
1262                        / mKeyboard.mMostCommonKeyWidth;
1263                Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1264                        +" %.2f key width from key edge", mPointerId, distanceToEdgeRatio));
1265            }
1266            return true;
1267        }
1268        if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput
1269                && sTimeRecorder.isInFastTyping(eventTime)
1270                && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) {
1271            if (DEBUG_MODE) {
1272                final float keyDiagonal = (float)Math.hypot(
1273                        mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1274                final float lengthFromDownRatio =
1275                        mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal;
1276                Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1277                        + " %.2f key diagonal from virtual down point",
1278                        mPointerId, lengthFromDownRatio));
1279            }
1280            return true;
1281        }
1282        return false;
1283    }
1284
1285    private void startLongPressTimer(final Key key) {
1286        if (key != null && key.isLongPressEnabled() && !sInGesture) {
1287            mTimerProxy.startLongPressTimer(this);
1288        }
1289    }
1290
1291    private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
1292        if (key == null) {
1293            callListenerOnCancelInput();
1294            return;
1295        }
1296
1297        final int code = key.mCode;
1298        callListenerOnCodeInput(key, code, x, y, eventTime);
1299        callListenerOnRelease(key, code, false);
1300    }
1301
1302    private void printTouchEvent(final String title, final int x, final int y,
1303            final long eventTime) {
1304        final Key key = mKeyDetector.detectHitKey(x, y);
1305        final String code = KeyDetector.printableCode(key);
1306        Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
1307                (mIsTrackingCanceled ? "-" : " "), title, x, y, eventTime, code));
1308    }
1309}
1310