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