PointerTracker.java revision 9c3860ce461c3791891bf667edc77fe798c8d332
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.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
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.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
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.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
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.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
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 (sShouldHandleGesture && me != null) {
955            // Add historical points to gesture path.
956            final int pointerIndex = me.findPointerIndex(mPointerId);
957            final int historicalSize = me.getHistorySize();
958            for (int h = 0; h < historicalSize; h++) {
959                final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
960                final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
961                final long historicalTime = me.getHistoricalEventTime(h);
962                onGestureMoveEvent(historicalX, historicalY, historicalTime,
963                        false /* isMajorEvent */, null);
964            }
965        }
966
967        if (isShowingMoreKeysPanel()) {
968            final int translatedX = mMoreKeysPanel.translateX(x);
969            final int translatedY = mMoreKeysPanel.translateY(y);
970            mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime);
971            onMoveKey(x, y);
972            mDrawingProxy.showSlidingKeyInputPreview(this);
973            return;
974        }
975        onMoveEventInternal(x, y, eventTime);
976    }
977
978    private void processSlidingKeyInput(final Key newKey, final int x, final int y,
979            final long eventTime) {
980        // This onPress call may have changed keyboard layout. Those cases are detected
981        // at {@link #setKeyboard}. In those cases, we should update key according
982        // to the new keyboard layout.
983        Key key = newKey;
984        if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
985            key = onMoveKey(x, y);
986        }
987        onMoveToNewKey(key, x, y);
988        startLongPressTimer(key);
989        setPressedKeyGraphics(key, eventTime);
990    }
991
992    private void processPhantomSuddenMoveHack(final Key key, final int x, final int y,
993            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
994        if (DEBUG_MODE) {
995            Log.w(TAG, String.format("[%d] onMoveEvent:"
996                    + " phantom sudden move event (distance=%d) is translated to "
997                    + "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId,
998                    getDistance(x, y, lastX, lastY),
999                    lastX, lastY, Constants.printableCode(oldKey.mCode),
1000                    x, y, Constants.printableCode(key.mCode)));
1001        }
1002        // TODO: This should be moved to outside of this nested if-clause?
1003        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1004            ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
1005        }
1006        onUpEventInternal(x, y, eventTime);
1007        onDownEventInternal(x, y, eventTime);
1008    }
1009
1010    private void processProximateBogusDownMoveUpEventHack(final Key key, final int x, final int y,
1011            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1012        if (DEBUG_MODE) {
1013            final float keyDiagonal = (float)Math.hypot(
1014                    mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1015            final float radiusRatio =
1016                    mBogusMoveEventDetector.getDistanceFromDownEvent(x, y)
1017                    / keyDiagonal;
1018            Log.w(TAG, String.format("[%d] onMoveEvent:"
1019                    + " bogus down-move-up event (raidus=%.2f key diagonal) is "
1020                    + " translated to up[%d,%d,%s]/down[%d,%d,%s] events",
1021                    mPointerId, radiusRatio,
1022                    lastX, lastY, Constants.printableCode(oldKey.mCode),
1023                    x, y, Constants.printableCode(key.mCode)));
1024        }
1025        onUpEventInternal(x, y, eventTime);
1026        onDownEventInternal(x, y, eventTime);
1027    }
1028
1029    private void processSildeOutFromOldKey(final Key oldKey) {
1030        setReleasedKeyGraphics(oldKey);
1031        callListenerOnRelease(oldKey, oldKey.mCode, true);
1032        startSlidingKeyInput(oldKey);
1033        mTimerProxy.cancelKeyTimers();
1034    }
1035
1036    private void slideFromOldKeyToNewKey(final Key key, final int x, final int y,
1037            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1038        // The pointer has been slid in to the new key from the previous key, we must call
1039        // onRelease() first to notify that the previous key has been released, then call
1040        // onPress() to notify that the new key is being pressed.
1041        processSildeOutFromOldKey(oldKey);
1042        startRepeatKey(key);
1043        if (mIsAllowedSlidingKeyInput) {
1044            processSlidingKeyInput(key, x, y, eventTime);
1045        }
1046        // HACK: On some devices, quick successive touches may be reported as a sudden move by
1047        // touch panel firmware. This hack detects such cases and translates the move event to
1048        // successive up and down events.
1049        // TODO: Should find a way to balance gesture detection and this hack.
1050        else if (sNeedsPhantomSuddenMoveEventHack
1051                && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) {
1052            processPhantomSuddenMoveHack(key, x, y, eventTime, oldKey, lastX, lastY);
1053        }
1054        // HACK: On some devices, quick successive proximate touches may be reported as a bogus
1055        // down-move-up event by touch panel firmware. This hack detects such cases and breaks
1056        // these events into separate up and down events.
1057        else if (sNeedsProximateBogusDownMoveUpEventHack && sTimeRecorder.isInFastTyping(eventTime)
1058                && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) {
1059            processProximateBogusDownMoveUpEventHack(key, x, y, eventTime, oldKey, lastX, lastY);
1060        }
1061        // HACK: If there are currently multiple touches, register the key even if the finger
1062        // slides off the key. This defends against noise from some touch panels when there are
1063        // close multiple touches.
1064        // Caveat: When in chording input mode with a modifier key, we don't use this hack.
1065        else if (getActivePointerTrackerCount() > 1
1066                && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
1067            if (DEBUG_MODE) {
1068                Log.w(TAG, String.format("[%d] onMoveEvent:"
1069                        + " detected sliding finger while multi touching", mPointerId));
1070            }
1071            onUpEvent(x, y, eventTime);
1072            cancelTracking();
1073            setReleasedKeyGraphics(oldKey);
1074        } else {
1075            if (!mIsDetectingGesture) {
1076                cancelTracking();
1077            }
1078            setReleasedKeyGraphics(oldKey);
1079        }
1080    }
1081
1082    private void slideOutFromOldKey(final Key oldKey, final int x, final int y) {
1083        // The pointer has been slid out from the previous key, we must call onRelease() to
1084        // notify that the previous key has been released.
1085        processSildeOutFromOldKey(oldKey);
1086        if (mIsAllowedSlidingKeyInput) {
1087            onMoveToNewKey(null, x, y);
1088        } else {
1089            if (!mIsDetectingGesture) {
1090                cancelTracking();
1091            }
1092        }
1093    }
1094
1095    private void onMoveEventInternal(final int x, final int y, final long eventTime) {
1096        final int lastX = mLastX;
1097        final int lastY = mLastY;
1098        final Key oldKey = mCurrentKey;
1099        final Key newKey = onMoveKey(x, y);
1100
1101        if (sShouldHandleGesture) {
1102            // Register move event on gesture tracker.
1103            onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey);
1104            if (sInGesture) {
1105                mCurrentKey = null;
1106                setReleasedKeyGraphics(oldKey);
1107                return;
1108            }
1109        }
1110
1111        if (newKey != null) {
1112            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1113                slideFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY);
1114            } else if (oldKey == null) {
1115                // The pointer has been slid in to the new key, but the finger was not on any keys.
1116                // In this case, we must call onPress() to notify that the new key is being pressed.
1117                processSlidingKeyInput(newKey, x, y, eventTime);
1118            }
1119        } else { // newKey == null
1120            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1121                slideOutFromOldKey(oldKey, x, y);
1122            }
1123        }
1124        mDrawingProxy.showSlidingKeyInputPreview(this);
1125    }
1126
1127    public void onUpEvent(final int x, final int y, final long eventTime) {
1128        if (DEBUG_EVENT) {
1129            printTouchEvent("onUpEvent  :", x, y, eventTime);
1130        }
1131
1132        mTimerProxy.cancelUpdateBatchInputTimer(this);
1133        if (!sInGesture) {
1134            if (mCurrentKey != null && mCurrentKey.isModifier()) {
1135                // Before processing an up event of modifier key, all pointers already being
1136                // tracked should be released.
1137                sPointerTrackerQueue.releaseAllPointersExcept(this, eventTime);
1138            } else {
1139                sPointerTrackerQueue.releaseAllPointersOlderThan(this, eventTime);
1140            }
1141        }
1142        onUpEventInternal(x, y, eventTime);
1143        sPointerTrackerQueue.remove(this);
1144    }
1145
1146    // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
1147    // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
1148    // "virtual" up event.
1149    @Override
1150    public void onPhantomUpEvent(final long eventTime) {
1151        if (DEBUG_EVENT) {
1152            printTouchEvent("onPhntEvent:", mLastX, mLastY, eventTime);
1153        }
1154        if (isShowingMoreKeysPanel()) {
1155            return;
1156        }
1157        onUpEventInternal(mLastX, mLastY, eventTime);
1158        cancelTracking();
1159    }
1160
1161    private void onUpEventInternal(final int x, final int y, final long eventTime) {
1162        mTimerProxy.cancelKeyTimers();
1163        resetSlidingKeyInput();
1164        mIsDetectingGesture = false;
1165        final Key currentKey = mCurrentKey;
1166        mCurrentKey = null;
1167        // Release the last pressed key.
1168        setReleasedKeyGraphics(currentKey);
1169
1170        if (isShowingMoreKeysPanel()) {
1171            if (!mIsTrackingCanceled) {
1172                final int translatedX = mMoreKeysPanel.translateX(x);
1173                final int translatedY = mMoreKeysPanel.translateY(y);
1174                mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId, eventTime);
1175            }
1176            mMoreKeysPanel.dismissMoreKeysPanel();
1177            mMoreKeysPanel = null;
1178            return;
1179        }
1180
1181        if (sInGesture) {
1182            if (currentKey != null) {
1183                callListenerOnRelease(currentKey, currentKey.mCode, true);
1184            }
1185            mayEndBatchInput(eventTime);
1186            return;
1187        }
1188
1189        if (mIsTrackingCanceled) {
1190            return;
1191        }
1192        if (currentKey != null && !currentKey.isRepeatable()) {
1193            detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
1194        }
1195    }
1196
1197    public void onShowMoreKeysPanel(final int translatedX, final int translatedY,
1198                final MoreKeysPanel panel) {
1199        setReleasedKeyGraphics(mCurrentKey);
1200        final long eventTime = SystemClock.uptimeMillis();
1201        mMoreKeysPanel = panel;
1202        mMoreKeysPanel.onDownEvent(translatedX, translatedY, mPointerId, eventTime);
1203    }
1204
1205    @Override
1206    public void cancelTracking() {
1207        if (isShowingMoreKeysPanel()) {
1208            return;
1209        }
1210        mIsTrackingCanceled = true;
1211    }
1212
1213    public void onLongPressed() {
1214        resetSlidingKeyInput();
1215        cancelTracking();
1216        setReleasedKeyGraphics(mCurrentKey);
1217        sPointerTrackerQueue.remove(this);
1218    }
1219
1220    public void onCancelEvent(final int x, final int y, final long eventTime) {
1221        if (DEBUG_EVENT) {
1222            printTouchEvent("onCancelEvt:", x, y, eventTime);
1223        }
1224
1225        cancelBatchInput();
1226        sPointerTrackerQueue.cancelAllPointerTracker();
1227        sPointerTrackerQueue.releaseAllPointers(eventTime);
1228        onCancelEventInternal();
1229    }
1230
1231    private void onCancelEventInternal() {
1232        mTimerProxy.cancelKeyTimers();
1233        setReleasedKeyGraphics(mCurrentKey);
1234        resetSlidingKeyInput();
1235        if (isShowingMoreKeysPanel()) {
1236            mMoreKeysPanel.dismissMoreKeysPanel();
1237            mMoreKeysPanel = null;
1238        }
1239    }
1240
1241    private void startRepeatKey(final Key key) {
1242        if (key != null && key.isRepeatable() && !sInGesture) {
1243            onRegisterKey(key);
1244            mTimerProxy.startKeyRepeatTimer(this);
1245        }
1246    }
1247
1248    public void onRegisterKey(final Key key) {
1249        if (key != null) {
1250            detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
1251            mTimerProxy.startTypingStateTimer(key);
1252        }
1253    }
1254
1255    private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
1256            final Key newKey) {
1257        if (mKeyDetector == null) {
1258            throw new NullPointerException("keyboard and/or key detector not set");
1259        }
1260        final Key curKey = mCurrentKey;
1261        if (newKey == curKey) {
1262            return false;
1263        }
1264        if (curKey == null /* && newKey != null */) {
1265            return true;
1266        }
1267        // Here curKey points to the different key from newKey.
1268        final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared(
1269                mIsInSlidingKeyInputFromModifier);
1270        final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y);
1271        if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) {
1272            if (DEBUG_MODE) {
1273                final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared)
1274                        / mKeyboard.mMostCommonKeyWidth;
1275                Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1276                        +" %.2f key width from key edge", mPointerId, distanceToEdgeRatio));
1277            }
1278            return true;
1279        }
1280        if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput
1281                && sTimeRecorder.isInFastTyping(eventTime)
1282                && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) {
1283            if (DEBUG_MODE) {
1284                final float keyDiagonal = (float)Math.hypot(
1285                        mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1286                final float lengthFromDownRatio =
1287                        mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal;
1288                Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1289                        + " %.2f key diagonal from virtual down point",
1290                        mPointerId, lengthFromDownRatio));
1291            }
1292            return true;
1293        }
1294        return false;
1295    }
1296
1297    private void startLongPressTimer(final Key key) {
1298        if (key != null && key.isLongPressEnabled() && !sInGesture) {
1299            mTimerProxy.startLongPressTimer(this);
1300        }
1301    }
1302
1303    private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
1304        if (key == null) {
1305            callListenerOnCancelInput();
1306            return;
1307        }
1308
1309        final int code = key.mCode;
1310        callListenerOnCodeInput(key, code, x, y, eventTime);
1311        callListenerOnRelease(key, code, false);
1312    }
1313
1314    private void printTouchEvent(final String title, final int x, final int y,
1315            final long eventTime) {
1316        final Key key = mKeyDetector.detectHitKey(x, y);
1317        final String code = KeyDetector.printableCode(key);
1318        Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
1319                (mIsTrackingCanceled ? "-" : " "), title, x, y, eventTime, code));
1320    }
1321}
1322