PointerTracker.java revision a456e3f659e03c5a36b87e318a10d469520cf72b
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 PointerTracke
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 repeatCount, 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 repeatCount, 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    // if not a NOT_A_CODE, the key of this code is repeating
350    private int mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
351
352    // true if a sliding key input is allowed.
353    private boolean mIsAllowedSlidingKeyInput;
354
355    private final GestureStrokeWithPreviewPoints mGestureStrokeWithPreviewPoints;
356
357    private static final int SMALL_TABLET_SMALLEST_WIDTH = 600; // dp
358    private static final int LARGE_TABLET_SMALLEST_WIDTH = 768; // dp
359
360    private static boolean needsProximateBogusDownMoveUpEventHack(final Resources res) {
361        // The proximate bogus down move up event hack is needed for a device such like,
362        // 1) is large tablet, or 2) is small tablet and the screen density is less than hdpi.
363        // Though it seems odd to use screen density as criteria of the quality of the touch
364        // screen, the small table that has a less density screen than hdpi most likely has been
365        // made with the touch screen that needs the hack.
366        final int sw = res.getConfiguration().smallestScreenWidthDp;
367        final boolean isLargeTablet = (sw >= LARGE_TABLET_SMALLEST_WIDTH);
368        final boolean isSmallTablet =
369                (sw >= SMALL_TABLET_SMALLEST_WIDTH && sw < LARGE_TABLET_SMALLEST_WIDTH);
370        final int densityDpi = res.getDisplayMetrics().densityDpi;
371        final boolean hasLowDensityScreen = (densityDpi < DisplayMetrics.DENSITY_HIGH);
372        final boolean needsTheHack = isLargeTablet || (isSmallTablet && hasLowDensityScreen);
373        if (DEBUG_MODE) {
374            Log.d(TAG, "needsProximateBogusDownMoveUpEventHack=" + needsTheHack
375                    + " smallestScreenWidthDp=" + sw + " densityDpi=" + densityDpi);
376        }
377        return needsTheHack;
378    }
379
380    public static void init(final Resources res) {
381        sNeedsPhantomSuddenMoveEventHack = Boolean.parseBoolean(
382                ResourceUtils.getDeviceOverrideValue(
383                        res, R.array.phantom_sudden_move_event_device_list));
384        sNeedsProximateBogusDownMoveUpEventHack = needsProximateBogusDownMoveUpEventHack(res);
385        sParams = PointerTrackerParams.DEFAULT;
386        sGestureStrokeParams = GestureStrokeParams.DEFAULT;
387        sGesturePreviewParams = GestureStrokePreviewParams.DEFAULT;
388        sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
389    }
390
391    public static void setParameters(final TypedArray mainKeyboardViewAttr) {
392        sParams = new PointerTrackerParams(mainKeyboardViewAttr);
393        sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr);
394        sGesturePreviewParams = new GestureStrokePreviewParams(mainKeyboardViewAttr);
395        sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
396    }
397
398    private static void updateGestureHandlingMode() {
399        sShouldHandleGesture = sMainDictionaryAvailable
400                && sGestureHandlingEnabledByInputField
401                && sGestureHandlingEnabledByUser
402                && !AccessibilityUtils.getInstance().isTouchExplorationEnabled();
403    }
404
405    // Note that this method is called from a non-UI thread.
406    public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
407        sMainDictionaryAvailable = mainDictionaryAvailable;
408        updateGestureHandlingMode();
409    }
410
411    public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
412        sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser;
413        updateGestureHandlingMode();
414    }
415
416    public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) {
417        final ArrayList<PointerTracker> trackers = sTrackers;
418
419        // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
420        for (int i = trackers.size(); i <= id; i++) {
421            final PointerTracker tracker = new PointerTracker(i, handler);
422            trackers.add(tracker);
423        }
424
425        return trackers.get(id);
426    }
427
428    public static boolean isAnyInSlidingKeyInput() {
429        return sPointerTrackerQueue.isAnyInSlidingKeyInput();
430    }
431
432    public static void cancelAllPointerTrackers() {
433        sPointerTrackerQueue.cancelAllPointerTrackers();
434    }
435
436    public static void setKeyboardActionListener(final KeyboardActionListener listener) {
437        final int trackersSize = sTrackers.size();
438        for (int i = 0; i < trackersSize; ++i) {
439            final PointerTracker tracker = sTrackers.get(i);
440            tracker.mListener = listener;
441        }
442    }
443
444    public static void setKeyDetector(final KeyDetector keyDetector) {
445        final int trackersSize = sTrackers.size();
446        for (int i = 0; i < trackersSize; ++i) {
447            final PointerTracker tracker = sTrackers.get(i);
448            tracker.setKeyDetectorInner(keyDetector);
449            // Mark that keyboard layout has been changed.
450            tracker.mKeyboardLayoutHasBeenChanged = true;
451        }
452        final Keyboard keyboard = keyDetector.getKeyboard();
453        sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput();
454        updateGestureHandlingMode();
455    }
456
457    public static void setReleasedKeyGraphicsToAllKeys() {
458        final int trackersSize = sTrackers.size();
459        for (int i = 0; i < trackersSize; ++i) {
460            final PointerTracker tracker = sTrackers.get(i);
461            tracker.setReleasedKeyGraphics(tracker.mCurrentKey);
462        }
463    }
464
465    public static void dismissAllMoreKeysPanels() {
466        final int trackersSize = sTrackers.size();
467        for (int i = 0; i < trackersSize; ++i) {
468            final PointerTracker tracker = sTrackers.get(i);
469            if (tracker.isShowingMoreKeysPanel()) {
470                tracker.mMoreKeysPanel.dismissMoreKeysPanel();
471                tracker.mMoreKeysPanel = null;
472            }
473        }
474    }
475
476    private PointerTracker(final int id, final KeyEventHandler handler) {
477        if (handler == null) {
478            throw new NullPointerException();
479        }
480        mPointerId = id;
481        mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints(
482                id, sGestureStrokeParams, sGesturePreviewParams);
483        setKeyEventHandler(handler);
484    }
485
486    private void setKeyEventHandler(final KeyEventHandler handler) {
487        setKeyDetectorInner(handler.getKeyDetector());
488        mListener = handler.getKeyboardActionListener();
489        mDrawingProxy = handler.getDrawingProxy();
490        mTimerProxy = handler.getTimerProxy();
491    }
492
493    // Returns true if keyboard has been changed by this callback.
494    private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key,
495            final int repeatCount) {
496        // While gesture input is going on, this method should be a no-operation. But when gesture
497        // input has been canceled, <code>sInGesture</code> and <code>mIsDetectingGesture</code>
498        // are set to false. To keep this method is a no-operation,
499        // <code>mIsTrackingForActionDisabled</code> should also be taken account of.
500        if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) {
501            return false;
502        }
503        final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier();
504        if (DEBUG_LISTENER) {
505            Log.d(TAG, String.format("[%d] onPress    : %s%s%s%s", mPointerId,
506                    KeyDetector.printableCode(key),
507                    ignoreModifierKey ? " ignoreModifier" : "",
508                    key.isEnabled() ? "" : " disabled",
509                    repeatCount > 0 ? " repeatCount=" + repeatCount : ""));
510        }
511        if (ignoreModifierKey) {
512            return false;
513        }
514        if (key.isEnabled()) {
515            mListener.onPressKey(key.getCode(), repeatCount, getActivePointerTrackerCount() == 1);
516            final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
517            mKeyboardLayoutHasBeenChanged = false;
518            mTimerProxy.startTypingStateTimer(key);
519            return keyboardLayoutHasBeenChanged;
520        }
521        return false;
522    }
523
524    // Note that we need primaryCode argument because the keyboard may in shifted state and the
525    // primaryCode is different from {@link Key#mCode}.
526    private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x,
527            final int y, final long eventTime) {
528        final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier();
529        final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
530        final int code = altersCode ? key.getAltCode() : primaryCode;
531        if (DEBUG_LISTENER) {
532            final String output = code == Constants.CODE_OUTPUT_TEXT
533                    ? key.getOutputText() : Constants.printableCode(code);
534            Log.d(TAG, String.format("[%d] onCodeInput: %4d %4d %s%s%s", mPointerId, x, y,
535                    output, ignoreModifierKey ? " ignoreModifier" : "",
536                    altersCode ? " altersCode" : "", key.isEnabled() ? "" : " disabled"));
537        }
538        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
539            ResearchLogger.pointerTracker_callListenerOnCodeInput(key, x, y, ignoreModifierKey,
540                    altersCode, code);
541        }
542        if (ignoreModifierKey) {
543            return;
544        }
545        // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
546        if (key.isEnabled() || altersCode) {
547            sTimeRecorder.onCodeInput(code, eventTime);
548            if (code == Constants.CODE_OUTPUT_TEXT) {
549                mListener.onTextInput(key.getOutputText());
550            } else if (code != Constants.CODE_UNSPECIFIED) {
551                mListener.onCodeInput(code, x, y);
552            }
553        }
554    }
555
556    // Note that we need primaryCode argument because the keyboard may be in shifted state and the
557    // primaryCode is different from {@link Key#mCode}.
558    private void callListenerOnRelease(final Key key, final int primaryCode,
559            final boolean withSliding) {
560        // See the comment at {@link #callListenerOnPressAndCheckKeyboardLayoutChange(Key}}.
561        if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) {
562            return;
563        }
564        final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier();
565        if (DEBUG_LISTENER) {
566            Log.d(TAG, String.format("[%d] onRelease  : %s%s%s%s", mPointerId,
567                    Constants.printableCode(primaryCode),
568                    withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : "",
569                    key.isEnabled() ?  "": " disabled"));
570        }
571        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
572            ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding,
573                    ignoreModifierKey);
574        }
575        if (ignoreModifierKey) {
576            return;
577        }
578        if (key.isEnabled()) {
579            mListener.onReleaseKey(primaryCode, withSliding);
580        }
581    }
582
583    private void callListenerOnFinishSlidingInput() {
584        if (DEBUG_LISTENER) {
585            Log.d(TAG, String.format("[%d] onFinishSlidingInput", mPointerId));
586        }
587        mListener.onFinishSlidingInput();
588    }
589
590    private void callListenerOnCancelInput() {
591        if (DEBUG_LISTENER) {
592            Log.d(TAG, String.format("[%d] onCancelInput", mPointerId));
593        }
594        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
595            ResearchLogger.pointerTracker_callListenerOnCancelInput();
596        }
597        mListener.onCancelInput();
598    }
599
600    private void setKeyDetectorInner(final KeyDetector keyDetector) {
601        final Keyboard keyboard = keyDetector.getKeyboard();
602        if (keyDetector == mKeyDetector && keyboard == mKeyboard) {
603            return;
604        }
605        mKeyDetector = keyDetector;
606        mKeyboard = keyDetector.getKeyboard();
607        final int keyWidth = mKeyboard.mMostCommonKeyWidth;
608        final int keyHeight = mKeyboard.mMostCommonKeyHeight;
609        mGestureStrokeWithPreviewPoints.setKeyboardGeometry(keyWidth, mKeyboard.mOccupiedHeight);
610        final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY);
611        if (newKey != mCurrentKey) {
612            if (mDrawingProxy != null) {
613                setReleasedKeyGraphics(mCurrentKey);
614            }
615            // Keep {@link #mCurrentKey} that comes from previous keyboard.
616        }
617        mPhantonSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD);
618        mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight);
619    }
620
621    @Override
622    public boolean isInSlidingKeyInput() {
623        return mIsInSlidingKeyInput;
624    }
625
626    public Key getKey() {
627        return mCurrentKey;
628    }
629
630    @Override
631    public boolean isModifier() {
632        return mCurrentKey != null && mCurrentKey.isModifier();
633    }
634
635    public Key getKeyOn(final int x, final int y) {
636        return mKeyDetector.detectHitKey(x, y);
637    }
638
639    private void setReleasedKeyGraphics(final Key key) {
640        mDrawingProxy.dismissKeyPreview(this);
641        if (key == null) {
642            return;
643        }
644
645        // Even if the key is disabled, update the key release graphics just in case.
646        updateReleaseKeyGraphics(key);
647
648        if (key.isShift()) {
649            for (final Key shiftKey : mKeyboard.mShiftKeys) {
650                if (shiftKey != key) {
651                    updateReleaseKeyGraphics(shiftKey);
652                }
653            }
654        }
655
656        if (key.altCodeWhileTyping()) {
657            final int altCode = key.getAltCode();
658            final Key altKey = mKeyboard.getKey(altCode);
659            if (altKey != null) {
660                updateReleaseKeyGraphics(altKey);
661            }
662            for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
663                if (k != key && k.getAltCode() == altCode) {
664                    updateReleaseKeyGraphics(k);
665                }
666            }
667        }
668    }
669
670    private static boolean needsToSuppressKeyPreviewPopup(final long eventTime) {
671        if (!sShouldHandleGesture) return false;
672        return sTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime);
673    }
674
675    private void setPressedKeyGraphics(final Key key, final long eventTime) {
676        if (key == null) {
677            return;
678        }
679
680        // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
681        final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
682        final boolean needsToUpdateGraphics = key.isEnabled() || altersCode;
683        if (!needsToUpdateGraphics) {
684            return;
685        }
686
687        if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) {
688            mDrawingProxy.showKeyPreview(this);
689        }
690        updatePressKeyGraphics(key);
691
692        if (key.isShift()) {
693            for (final Key shiftKey : mKeyboard.mShiftKeys) {
694                if (shiftKey != key) {
695                    updatePressKeyGraphics(shiftKey);
696                }
697            }
698        }
699
700        if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) {
701            final int altCode = key.getAltCode();
702            final Key altKey = mKeyboard.getKey(altCode);
703            if (altKey != null) {
704                updatePressKeyGraphics(altKey);
705            }
706            for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
707                if (k != key && k.getAltCode() == altCode) {
708                    updatePressKeyGraphics(k);
709                }
710            }
711        }
712    }
713
714    private void updateReleaseKeyGraphics(final Key key) {
715        key.onReleased();
716        mDrawingProxy.invalidateKey(key);
717    }
718
719    private void updatePressKeyGraphics(final Key key) {
720        key.onPressed();
721        mDrawingProxy.invalidateKey(key);
722    }
723
724    public GestureStrokeWithPreviewPoints getGestureStrokeWithPreviewPoints() {
725        return mGestureStrokeWithPreviewPoints;
726    }
727
728    public void getLastCoordinates(final int[] outCoords) {
729        CoordinateUtils.set(outCoords, mLastX, mLastY);
730    }
731
732    public long getDownTime() {
733        return mDownTime;
734    }
735
736    public void getDownCoordinates(final int[] outCoords) {
737        CoordinateUtils.copy(outCoords, mDownCoordinates);
738    }
739
740    private Key onDownKey(final int x, final int y, final long eventTime) {
741        mDownTime = eventTime;
742        CoordinateUtils.set(mDownCoordinates, x, y);
743        mBogusMoveEventDetector.onDownKey();
744        return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
745    }
746
747    static int getDistance(final int x1, final int y1, final int x2, final int y2) {
748        return (int)Math.hypot(x1 - x2, y1 - y2);
749    }
750
751    private Key onMoveKeyInternal(final int x, final int y) {
752        mBogusMoveEventDetector.onMoveKey(getDistance(x, y, mLastX, mLastY));
753        mLastX = x;
754        mLastY = y;
755        return mKeyDetector.detectHitKey(x, y);
756    }
757
758    private Key onMoveKey(final int x, final int y) {
759        return onMoveKeyInternal(x, y);
760    }
761
762    private Key onMoveToNewKey(final Key newKey, final int x, final int y) {
763        mCurrentKey = newKey;
764        mKeyX = x;
765        mKeyY = y;
766        return newKey;
767    }
768
769    private static int getActivePointerTrackerCount() {
770        return sPointerTrackerQueue.size();
771    }
772
773    private boolean isOldestTrackerInQueue() {
774        return sPointerTrackerQueue.getOldestElement() == this;
775    }
776
777    private void mayStartBatchInput(final Key key) {
778        if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) {
779            return;
780        }
781        if (key == null || !Character.isLetter(key.getCode())) {
782            return;
783        }
784        if (DEBUG_LISTENER) {
785            Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId));
786        }
787        sInGesture = true;
788        synchronized (sAggregratedPointers) {
789            sAggregratedPointers.reset();
790            sLastRecognitionPointSize = 0;
791            sLastRecognitionTime = 0;
792            mListener.onStartBatchInput();
793            dismissAllMoreKeysPanels();
794        }
795        mTimerProxy.cancelLongPressTimer();
796        // A gesture floating preview text will be shown at the oldest pointer/finger on the screen.
797        mDrawingProxy.showGestureTrail(
798                this, isOldestTrackerInQueue() /* showsFloatingPreviewText */);
799    }
800
801    public void updateBatchInputByTimer(final long eventTime) {
802        final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
803        mGestureStrokeWithPreviewPoints.duplicateLastPointWith(gestureTime);
804        updateBatchInput(eventTime);
805    }
806
807    private void mayUpdateBatchInput(final long eventTime, final Key key) {
808        if (key != null) {
809            updateBatchInput(eventTime);
810        }
811        if (mIsTrackingForActionDisabled) {
812            return;
813        }
814        // A gesture floating preview text will be shown at the oldest pointer/finger on the screen.
815        mDrawingProxy.showGestureTrail(
816                this, isOldestTrackerInQueue() /* showsFloatingPreviewText */);
817    }
818
819    private void updateBatchInput(final long eventTime) {
820        synchronized (sAggregratedPointers) {
821            final GestureStroke stroke = mGestureStrokeWithPreviewPoints;
822            stroke.appendIncrementalBatchPoints(sAggregratedPointers);
823            final int size = sAggregratedPointers.getPointerSize();
824            if (size > sLastRecognitionPointSize
825                    && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) {
826                sLastRecognitionPointSize = size;
827                sLastRecognitionTime = eventTime;
828                if (DEBUG_LISTENER) {
829                    Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", mPointerId,
830                            size));
831                }
832                mTimerProxy.startUpdateBatchInputTimer(this);
833                mListener.onUpdateBatchInput(sAggregratedPointers);
834            }
835        }
836    }
837
838    private void mayEndBatchInput(final long eventTime) {
839        synchronized (sAggregratedPointers) {
840            mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers);
841            if (getActivePointerTrackerCount() == 1) {
842                sInGesture = false;
843                sTimeRecorder.onEndBatchInput(eventTime);
844                mTimerProxy.cancelAllUpdateBatchInputTimers();
845                if (!mIsTrackingForActionDisabled) {
846                    if (DEBUG_LISTENER) {
847                        Log.d(TAG, String.format("[%d] onEndBatchInput   : batchPoints=%d",
848                                mPointerId, sAggregratedPointers.getPointerSize()));
849                    }
850                    mListener.onEndBatchInput(sAggregratedPointers);
851                }
852            }
853        }
854        if (mIsTrackingForActionDisabled) {
855            return;
856        }
857        // A gesture floating preview text will be shown at the oldest pointer/finger on the screen.
858        mDrawingProxy.showGestureTrail(
859                this, isOldestTrackerInQueue() /* showsFloatingPreviewText */);
860    }
861
862    private void cancelBatchInput() {
863        cancelAllPointerTrackers();
864        mIsDetectingGesture = false;
865        if (!sInGesture) {
866            return;
867        }
868        sInGesture = false;
869        if (DEBUG_LISTENER) {
870            Log.d(TAG, String.format("[%d] onCancelBatchInput", mPointerId));
871        }
872        mListener.onCancelBatchInput();
873    }
874
875    public void processMotionEvent(final MotionEvent me, final KeyEventHandler handler) {
876        final int action = me.getActionMasked();
877        final long eventTime = me.getEventTime();
878        if (action == MotionEvent.ACTION_MOVE) {
879            final int pointerCount = me.getPointerCount();
880            for (int index = 0; index < pointerCount; index++) {
881                final int id = me.getPointerId(index);
882                final PointerTracker tracker = getPointerTracker(id, handler);
883                final int x = (int)me.getX(index);
884                final int y = (int)me.getY(index);
885                tracker.onMoveEvent(x, y, eventTime, me);
886            }
887            return;
888        }
889        final int index = me.getActionIndex();
890        final int x = (int)me.getX(index);
891        final int y = (int)me.getY(index);
892        switch (action) {
893        case MotionEvent.ACTION_DOWN:
894        case MotionEvent.ACTION_POINTER_DOWN:
895            onDownEvent(x, y, eventTime, handler);
896            break;
897        case MotionEvent.ACTION_UP:
898        case MotionEvent.ACTION_POINTER_UP:
899            onUpEvent(x, y, eventTime);
900            break;
901        case MotionEvent.ACTION_CANCEL:
902            onCancelEvent(x, y, eventTime);
903            break;
904        }
905    }
906
907    private void onDownEvent(final int x, final int y, final long eventTime,
908            final KeyEventHandler handler) {
909        if (DEBUG_EVENT) {
910            printTouchEvent("onDownEvent:", x, y, eventTime);
911        }
912        setKeyEventHandler(handler);
913        // Naive up-to-down noise filter.
914        final long deltaT = eventTime - mUpTime;
915        if (deltaT < sParams.mTouchNoiseThresholdTime) {
916            final int distance = getDistance(x, y, mLastX, mLastY);
917            if (distance < sParams.mTouchNoiseThresholdDistance) {
918                if (DEBUG_MODE)
919                    Log.w(TAG, String.format("[%d] onDownEvent:"
920                            + " ignore potential noise: time=%d distance=%d",
921                            mPointerId, deltaT, distance));
922                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
923                    ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance);
924                }
925                cancelTrackingForAction();
926                return;
927            }
928        }
929
930        final Key key = getKeyOn(x, y);
931        mBogusMoveEventDetector.onActualDownEvent(x, y);
932        if (key != null && key.isModifier()) {
933            // Before processing a down event of modifier key, all pointers already being
934            // tracked should be released.
935            sPointerTrackerQueue.releaseAllPointers(eventTime);
936        }
937        sPointerTrackerQueue.add(this);
938        onDownEventInternal(x, y, eventTime);
939        if (!sShouldHandleGesture) {
940            return;
941        }
942        // A gesture should start only from a non-modifier key. Note that the gesture detection is
943        // disabled when the key is repeating.
944        mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard()
945                && key != null && !key.isModifier();
946        if (mIsDetectingGesture) {
947            if (getActivePointerTrackerCount() == 1) {
948                sGestureFirstDownTime = eventTime;
949            }
950            mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime,
951                    sTimeRecorder.getLastLetterTypingTime());
952        }
953    }
954
955    private boolean isShowingMoreKeysPanel() {
956        return (mMoreKeysPanel != null);
957    }
958
959    private void onDownEventInternal(final int x, final int y, final long eventTime) {
960        Key key = onDownKey(x, y, eventTime);
961        // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
962        // from modifier key, or 3) this pointer's KeyDetector always allows sliding input.
963        mIsAllowedSlidingKeyInput = sParams.mSlidingKeyInputEnabled
964                || (key != null && key.isModifier())
965                || mKeyDetector.alwaysAllowsSlidingInput();
966        mKeyboardLayoutHasBeenChanged = false;
967        mIsTrackingForActionDisabled = false;
968        resetSlidingKeyInput();
969        if (key != null) {
970            // This onPress call may have changed keyboard layout. Those cases are detected at
971            // {@link #setKeyboard}. In those cases, we should update key according to the new
972            // keyboard layout.
973            if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
974                key = onDownKey(x, y, eventTime);
975            }
976
977            startRepeatKey(key);
978            startLongPressTimer(key);
979            setPressedKeyGraphics(key, eventTime);
980        }
981    }
982
983    private void startSlidingKeyInput(final Key key) {
984        if (!mIsInSlidingKeyInput) {
985            mIsInSlidingKeyInputFromModifier = key.isModifier();
986        }
987        mIsInSlidingKeyInput = true;
988    }
989
990    private void resetSlidingKeyInput() {
991        mIsInSlidingKeyInput = false;
992        mIsInSlidingKeyInputFromModifier = false;
993        mDrawingProxy.dismissSlidingKeyInputPreview();
994    }
995
996    private void onGestureMoveEvent(final int x, final int y, final long eventTime,
997            final boolean isMajorEvent, final Key key) {
998        final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
999        if (mIsDetectingGesture) {
1000            final int beforeLength = mGestureStrokeWithPreviewPoints.getLength();
1001            final boolean onValidArea = mGestureStrokeWithPreviewPoints.addPointOnKeyboard(
1002                    x, y, gestureTime, isMajorEvent);
1003            if (mGestureStrokeWithPreviewPoints.getLength() > beforeLength) {
1004                mTimerProxy.startUpdateBatchInputTimer(this);
1005            }
1006            // If the move event goes out from valid batch input area, cancel batch input.
1007            if (!onValidArea) {
1008                cancelBatchInput();
1009                return;
1010            }
1011            // If the MoreKeysPanel is showing then do not attempt to enter gesture mode. However,
1012            // the gestured touch points are still being recorded in case the panel is dismissed.
1013            if (isShowingMoreKeysPanel()) {
1014                return;
1015            }
1016            mayStartBatchInput(key);
1017            if (sInGesture) {
1018                mayUpdateBatchInput(eventTime, key);
1019            }
1020        }
1021    }
1022
1023    private void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
1024        if (DEBUG_MOVE_EVENT) {
1025            printTouchEvent("onMoveEvent:", x, y, eventTime);
1026        }
1027        if (mIsTrackingForActionDisabled) {
1028            return;
1029        }
1030
1031        if (sShouldHandleGesture && me != null) {
1032            // Add historical points to gesture path.
1033            final int pointerIndex = me.findPointerIndex(mPointerId);
1034            final int historicalSize = me.getHistorySize();
1035            for (int h = 0; h < historicalSize; h++) {
1036                final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
1037                final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
1038                final long historicalTime = me.getHistoricalEventTime(h);
1039                onGestureMoveEvent(historicalX, historicalY, historicalTime,
1040                        false /* isMajorEvent */, null);
1041            }
1042        }
1043
1044        if (isShowingMoreKeysPanel()) {
1045            final int translatedX = mMoreKeysPanel.translateX(x);
1046            final int translatedY = mMoreKeysPanel.translateY(y);
1047            mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime);
1048            onMoveKey(x, y);
1049            if (mIsInSlidingKeyInputFromModifier) {
1050                mDrawingProxy.showSlidingKeyInputPreview(this);
1051            }
1052            return;
1053        }
1054        onMoveEventInternal(x, y, eventTime);
1055    }
1056
1057    private void processSlidingKeyInput(final Key newKey, final int x, final int y,
1058            final long eventTime) {
1059        // This onPress call may have changed keyboard layout. Those cases are detected
1060        // at {@link #setKeyboard}. In those cases, we should update key according
1061        // to the new keyboard layout.
1062        Key key = newKey;
1063        if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
1064            key = onMoveKey(x, y);
1065        }
1066        onMoveToNewKey(key, x, y);
1067        if (mIsTrackingForActionDisabled) {
1068            return;
1069        }
1070        startLongPressTimer(key);
1071        setPressedKeyGraphics(key, eventTime);
1072    }
1073
1074    private void processPhantomSuddenMoveHack(final Key key, final int x, final int y,
1075            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1076        if (DEBUG_MODE) {
1077            Log.w(TAG, String.format("[%d] onMoveEvent:"
1078                    + " phantom sudden move event (distance=%d) is translated to "
1079                    + "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId,
1080                    getDistance(x, y, lastX, lastY),
1081                    lastX, lastY, Constants.printableCode(oldKey.getCode()),
1082                    x, y, Constants.printableCode(key.getCode())));
1083        }
1084        // TODO: This should be moved to outside of this nested if-clause?
1085        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1086            ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
1087        }
1088        onUpEventInternal(x, y, eventTime);
1089        onDownEventInternal(x, y, eventTime);
1090    }
1091
1092    private void processProximateBogusDownMoveUpEventHack(final Key key, final int x, final int y,
1093            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1094        if (DEBUG_MODE) {
1095            final float keyDiagonal = (float)Math.hypot(
1096                    mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1097            final float radiusRatio =
1098                    mBogusMoveEventDetector.getDistanceFromDownEvent(x, y)
1099                    / keyDiagonal;
1100            Log.w(TAG, String.format("[%d] onMoveEvent:"
1101                    + " bogus down-move-up event (raidus=%.2f key diagonal) is "
1102                    + " translated to up[%d,%d,%s]/down[%d,%d,%s] events",
1103                    mPointerId, radiusRatio,
1104                    lastX, lastY, Constants.printableCode(oldKey.getCode()),
1105                    x, y, Constants.printableCode(key.getCode())));
1106        }
1107        onUpEventInternal(x, y, eventTime);
1108        onDownEventInternal(x, y, eventTime);
1109    }
1110
1111    private void processSildeOutFromOldKey(final Key oldKey) {
1112        setReleasedKeyGraphics(oldKey);
1113        callListenerOnRelease(oldKey, oldKey.getCode(), true /* withSliding */);
1114        startSlidingKeyInput(oldKey);
1115        mTimerProxy.cancelKeyTimers();
1116    }
1117
1118    private void slideFromOldKeyToNewKey(final Key key, final int x, final int y,
1119            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1120        // The pointer has been slid in to the new key from the previous key, we must call
1121        // onRelease() first to notify that the previous key has been released, then call
1122        // onPress() to notify that the new key is being pressed.
1123        processSildeOutFromOldKey(oldKey);
1124        startRepeatKey(key);
1125        if (mIsAllowedSlidingKeyInput) {
1126            processSlidingKeyInput(key, x, y, eventTime);
1127        }
1128        // HACK: On some devices, quick successive touches may be reported as a sudden move by
1129        // touch panel firmware. This hack detects such cases and translates the move event to
1130        // successive up and down events.
1131        // TODO: Should find a way to balance gesture detection and this hack.
1132        else if (sNeedsPhantomSuddenMoveEventHack
1133                && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) {
1134            processPhantomSuddenMoveHack(key, x, y, eventTime, oldKey, lastX, lastY);
1135        }
1136        // HACK: On some devices, quick successive proximate touches may be reported as a bogus
1137        // down-move-up event by touch panel firmware. This hack detects such cases and breaks
1138        // these events into separate up and down events.
1139        else if (sNeedsProximateBogusDownMoveUpEventHack && sTimeRecorder.isInFastTyping(eventTime)
1140                && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) {
1141            processProximateBogusDownMoveUpEventHack(key, x, y, eventTime, oldKey, lastX, lastY);
1142        }
1143        // HACK: If there are currently multiple touches, register the key even if the finger
1144        // slides off the key. This defends against noise from some touch panels when there are
1145        // close multiple touches.
1146        // Caveat: When in chording input mode with a modifier key, we don't use this hack.
1147        else if (getActivePointerTrackerCount() > 1
1148                && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
1149            if (DEBUG_MODE) {
1150                Log.w(TAG, String.format("[%d] onMoveEvent:"
1151                        + " detected sliding finger while multi touching", mPointerId));
1152            }
1153            onUpEvent(x, y, eventTime);
1154            cancelTrackingForAction();
1155            setReleasedKeyGraphics(oldKey);
1156        } else {
1157            if (!mIsDetectingGesture) {
1158                cancelTrackingForAction();
1159            }
1160            setReleasedKeyGraphics(oldKey);
1161        }
1162    }
1163
1164    private void slideOutFromOldKey(final Key oldKey, final int x, final int y) {
1165        // The pointer has been slid out from the previous key, we must call onRelease() to
1166        // notify that the previous key has been released.
1167        processSildeOutFromOldKey(oldKey);
1168        if (mIsAllowedSlidingKeyInput) {
1169            onMoveToNewKey(null, x, y);
1170        } else {
1171            if (!mIsDetectingGesture) {
1172                cancelTrackingForAction();
1173            }
1174        }
1175    }
1176
1177    private void onMoveEventInternal(final int x, final int y, final long eventTime) {
1178        final int lastX = mLastX;
1179        final int lastY = mLastY;
1180        final Key oldKey = mCurrentKey;
1181        final Key newKey = onMoveKey(x, y);
1182
1183        if (sShouldHandleGesture) {
1184            // Register move event on gesture tracker.
1185            onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey);
1186            if (sInGesture) {
1187                mCurrentKey = null;
1188                setReleasedKeyGraphics(oldKey);
1189                return;
1190            }
1191        }
1192
1193        if (newKey != null) {
1194            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1195                slideFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY);
1196            } else if (oldKey == null) {
1197                // The pointer has been slid in to the new key, but the finger was not on any keys.
1198                // In this case, we must call onPress() to notify that the new key is being pressed.
1199                processSlidingKeyInput(newKey, x, y, eventTime);
1200            }
1201        } else { // newKey == null
1202            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1203                slideOutFromOldKey(oldKey, x, y);
1204            }
1205        }
1206        if (mIsInSlidingKeyInputFromModifier) {
1207            mDrawingProxy.showSlidingKeyInputPreview(this);
1208        }
1209    }
1210
1211    private void onUpEvent(final int x, final int y, final long eventTime) {
1212        if (DEBUG_EVENT) {
1213            printTouchEvent("onUpEvent  :", x, y, eventTime);
1214        }
1215
1216        mTimerProxy.cancelUpdateBatchInputTimer(this);
1217        if (!sInGesture) {
1218            if (mCurrentKey != null && mCurrentKey.isModifier()) {
1219                // Before processing an up event of modifier key, all pointers already being
1220                // tracked should be released.
1221                sPointerTrackerQueue.releaseAllPointersExcept(this, eventTime);
1222            } else {
1223                sPointerTrackerQueue.releaseAllPointersOlderThan(this, eventTime);
1224            }
1225        }
1226        onUpEventInternal(x, y, eventTime);
1227        sPointerTrackerQueue.remove(this);
1228    }
1229
1230    // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
1231    // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
1232    // "virtual" up event.
1233    @Override
1234    public void onPhantomUpEvent(final long eventTime) {
1235        if (DEBUG_EVENT) {
1236            printTouchEvent("onPhntEvent:", mLastX, mLastY, eventTime);
1237        }
1238        if (isShowingMoreKeysPanel()) {
1239            return;
1240        }
1241        onUpEventInternal(mLastX, mLastY, eventTime);
1242        cancelTrackingForAction();
1243    }
1244
1245    private void onUpEventInternal(final int x, final int y, final long eventTime) {
1246        mTimerProxy.cancelKeyTimers();
1247        final boolean isInSlidingKeyInput = mIsInSlidingKeyInput;
1248        final boolean isInSlidingKeyInputFromModifier = mIsInSlidingKeyInputFromModifier;
1249        resetSlidingKeyInput();
1250        mIsDetectingGesture = false;
1251        final Key currentKey = mCurrentKey;
1252        mCurrentKey = null;
1253        final int currentRepeatingKeyCode = mCurrentRepeatingKeyCode;
1254        mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
1255        // Release the last pressed key.
1256        setReleasedKeyGraphics(currentKey);
1257
1258        if (isShowingMoreKeysPanel()) {
1259            if (!mIsTrackingForActionDisabled) {
1260                final int translatedX = mMoreKeysPanel.translateX(x);
1261                final int translatedY = mMoreKeysPanel.translateY(y);
1262                mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId, eventTime);
1263            }
1264            mMoreKeysPanel.dismissMoreKeysPanel();
1265            mMoreKeysPanel = null;
1266            return;
1267        }
1268
1269        if (sInGesture) {
1270            if (currentKey != null) {
1271                callListenerOnRelease(currentKey, currentKey.getCode(), true /* withSliding */);
1272            }
1273            mayEndBatchInput(eventTime);
1274            return;
1275        }
1276
1277        if (mIsTrackingForActionDisabled) {
1278            return;
1279        }
1280        if (currentKey != null && currentKey.isRepeatable()
1281                && (currentKey.getCode() == currentRepeatingKeyCode) && !isInSlidingKeyInput) {
1282            return;
1283        }
1284        detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
1285        if (isInSlidingKeyInputFromModifier) {
1286            callListenerOnFinishSlidingInput();
1287        }
1288    }
1289
1290    public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
1291        setReleasedKeyGraphics(mCurrentKey);
1292        final int translatedX = panel.translateX(mLastX);
1293        final int translatedY = panel.translateY(mLastY);
1294        panel.onDownEvent(translatedX, translatedY, mPointerId, SystemClock.uptimeMillis());
1295        mMoreKeysPanel = panel;
1296    }
1297
1298    @Override
1299    public void cancelTrackingForAction() {
1300        if (isShowingMoreKeysPanel()) {
1301            return;
1302        }
1303        mIsTrackingForActionDisabled = true;
1304    }
1305
1306    public void onLongPressed() {
1307        resetSlidingKeyInput();
1308        cancelTrackingForAction();
1309        setReleasedKeyGraphics(mCurrentKey);
1310        sPointerTrackerQueue.remove(this);
1311    }
1312
1313    private void onCancelEvent(final int x, final int y, final long eventTime) {
1314        if (DEBUG_EVENT) {
1315            printTouchEvent("onCancelEvt:", x, y, eventTime);
1316        }
1317
1318        cancelBatchInput();
1319        cancelAllPointerTrackers();
1320        sPointerTrackerQueue.releaseAllPointers(eventTime);
1321        onCancelEventInternal();
1322    }
1323
1324    private void onCancelEventInternal() {
1325        mTimerProxy.cancelKeyTimers();
1326        setReleasedKeyGraphics(mCurrentKey);
1327        resetSlidingKeyInput();
1328        if (isShowingMoreKeysPanel()) {
1329            mMoreKeysPanel.dismissMoreKeysPanel();
1330            mMoreKeysPanel = null;
1331        }
1332    }
1333
1334    private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
1335            final Key newKey) {
1336        if (mKeyDetector == null) {
1337            throw new NullPointerException("keyboard and/or key detector not set");
1338        }
1339        final Key curKey = mCurrentKey;
1340        if (newKey == curKey) {
1341            return false;
1342        }
1343        if (curKey == null /* && newKey != null */) {
1344            return true;
1345        }
1346        // Here curKey points to the different key from newKey.
1347        final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared(
1348                mIsInSlidingKeyInputFromModifier);
1349        final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y);
1350        if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) {
1351            if (DEBUG_MODE) {
1352                final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared)
1353                        / mKeyboard.mMostCommonKeyWidth;
1354                Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1355                        +" %.2f key width from key edge", mPointerId, distanceToEdgeRatio));
1356            }
1357            return true;
1358        }
1359        if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput
1360                && sTimeRecorder.isInFastTyping(eventTime)
1361                && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) {
1362            if (DEBUG_MODE) {
1363                final float keyDiagonal = (float)Math.hypot(
1364                        mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1365                final float lengthFromDownRatio =
1366                        mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal;
1367                Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1368                        + " %.2f key diagonal from virtual down point",
1369                        mPointerId, lengthFromDownRatio));
1370            }
1371            return true;
1372        }
1373        return false;
1374    }
1375
1376    private void startLongPressTimer(final Key key) {
1377        if (sInGesture) return;
1378        if (key == null) return;
1379        if (!key.isLongPressEnabled()) return;
1380        // Caveat: Please note that isLongPressEnabled() can be true even if the current key
1381        // doesn't have its more keys. (e.g. spacebar, globe key)
1382        // We always need to start the long press timer if the key has its more keys regardless of
1383        // whether or not we are in the sliding input mode.
1384        if (mIsInSlidingKeyInput && key.getMoreKeys() == null) return;
1385        final int delay;
1386        switch (key.getCode()) {
1387        case Constants.CODE_SHIFT:
1388            delay = sParams.mLongPressShiftLockTimeout;
1389            break;
1390        default:
1391            final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout;
1392            if (mIsInSlidingKeyInputFromModifier) {
1393                // We use longer timeout for sliding finger input started from the modifier key.
1394                delay = longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
1395            } else {
1396                delay = longpressTimeout;
1397            }
1398            break;
1399        }
1400        mTimerProxy.startLongPressTimer(this, delay);
1401    }
1402
1403    private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
1404        if (key == null) {
1405            callListenerOnCancelInput();
1406            return;
1407        }
1408
1409        final int code = key.getCode();
1410        callListenerOnCodeInput(key, code, x, y, eventTime);
1411        callListenerOnRelease(key, code, false /* withSliding */);
1412    }
1413
1414    private void startRepeatKey(final Key key) {
1415        if (sInGesture) return;
1416        if (key == null) return;
1417        if (!key.isRepeatable()) return;
1418        // Don't start key repeat when we are in sliding input mode.
1419        if (mIsInSlidingKeyInput) return;
1420        final int startRepeatCount = 1;
1421        mTimerProxy.startKeyRepeatTimer(this, startRepeatCount, sParams.mKeyRepeatStartTimeout);
1422    }
1423
1424    public void onKeyRepeat(final int code, final int repeatCount) {
1425        final Key key = getKey();
1426        if (key == null || key.getCode() != code) {
1427            mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
1428            return;
1429        }
1430        mCurrentRepeatingKeyCode = code;
1431        mIsDetectingGesture = false;
1432        final int nextRepeatCount = repeatCount + 1;
1433        mTimerProxy.startKeyRepeatTimer(this, nextRepeatCount, sParams.mKeyRepeatInterval);
1434        callListenerOnPressAndCheckKeyboardLayoutChange(key, repeatCount);
1435        callListenerOnCodeInput(key, code, mKeyX, mKeyY, SystemClock.uptimeMillis());
1436    }
1437
1438    private void printTouchEvent(final String title, final int x, final int y,
1439            final long eventTime) {
1440        final Key key = mKeyDetector.detectHitKey(x, y);
1441        final String code = KeyDetector.printableCode(key);
1442        Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
1443                (mIsTrackingForActionDisabled ? "-" : " "), title, x, y, eventTime, code));
1444    }
1445}
1446