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