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