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