PointerTracker.java revision 33482a9b9ccf605c63fab7c9b8273a240bbc2035
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 MotionEvent me, final KeyEventHandler handler) {
874        final int action = me.getActionMasked();
875        final long eventTime = me.getEventTime();
876        if (action == MotionEvent.ACTION_MOVE) {
877            final int pointerCount = me.getPointerCount();
878            for (int index = 0; index < pointerCount; index++) {
879                final int id = me.getPointerId(index);
880                final PointerTracker tracker = getPointerTracker(id, handler);
881                final int x = (int)me.getX(index);
882                final int y = (int)me.getY(index);
883                tracker.onMoveEvent(x, y, eventTime, me);
884            }
885            return;
886        }
887        final int index = me.getActionIndex();
888        final int x = (int)me.getX(index);
889        final int y = (int)me.getY(index);
890        switch (action) {
891        case MotionEvent.ACTION_DOWN:
892        case MotionEvent.ACTION_POINTER_DOWN:
893            onDownEvent(x, y, eventTime, handler);
894            break;
895        case MotionEvent.ACTION_UP:
896        case MotionEvent.ACTION_POINTER_UP:
897            onUpEvent(x, y, eventTime);
898            break;
899        case MotionEvent.ACTION_CANCEL:
900            onCancelEvent(x, y, eventTime);
901            break;
902        }
903    }
904
905    private void onDownEvent(final int x, final int y, final long eventTime,
906            final KeyEventHandler handler) {
907        if (DEBUG_EVENT) {
908            printTouchEvent("onDownEvent:", x, y, eventTime);
909        }
910        setKeyEventHandler(handler);
911        // Naive up-to-down noise filter.
912        final long deltaT = eventTime - mUpTime;
913        if (deltaT < sParams.mTouchNoiseThresholdTime) {
914            final int distance = getDistance(x, y, mLastX, mLastY);
915            if (distance < sParams.mTouchNoiseThresholdDistance) {
916                if (DEBUG_MODE)
917                    Log.w(TAG, String.format("[%d] onDownEvent:"
918                            + " ignore potential noise: time=%d distance=%d",
919                            mPointerId, deltaT, distance));
920                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
921                    ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance);
922                }
923                cancelTrackingForAction();
924                return;
925            }
926        }
927
928        final Key key = getKeyOn(x, y);
929        mBogusMoveEventDetector.onActualDownEvent(x, y);
930        if (key != null && key.isModifier()) {
931            // Before processing a down event of modifier key, all pointers already being
932            // tracked should be released.
933            sPointerTrackerQueue.releaseAllPointers(eventTime);
934        }
935        sPointerTrackerQueue.add(this);
936        onDownEventInternal(x, y, eventTime);
937        if (!sShouldHandleGesture) {
938            return;
939        }
940        // A gesture should start only from a non-modifier key.
941        mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard()
942                && key != null && !key.isModifier() && !key.isRepeatable();
943        if (mIsDetectingGesture) {
944            if (getActivePointerTrackerCount() == 1) {
945                sGestureFirstDownTime = eventTime;
946            }
947            mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime,
948                    sTimeRecorder.getLastLetterTypingTime());
949        }
950    }
951
952    private boolean isShowingMoreKeysPanel() {
953        return (mMoreKeysPanel != null);
954    }
955
956    private void onDownEventInternal(final int x, final int y, final long eventTime) {
957        Key key = onDownKey(x, y, eventTime);
958        // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
959        // from modifier key, or 3) this pointer's KeyDetector always allows sliding input.
960        mIsAllowedSlidingKeyInput = sParams.mSlidingKeyInputEnabled
961                || (key != null && key.isModifier())
962                || mKeyDetector.alwaysAllowsSlidingInput();
963        mKeyboardLayoutHasBeenChanged = false;
964        mIsTrackingForActionDisabled = false;
965        resetSlidingKeyInput();
966        if (key != null) {
967            // This onPress call may have changed keyboard layout. Those cases are detected at
968            // {@link #setKeyboard}. In those cases, we should update key according to the new
969            // keyboard layout.
970            if (callListenerOnPressAndCheckKeyboardLayoutChange(key, false /* isRepeatKey */)) {
971                key = onDownKey(x, y, eventTime);
972            }
973
974            startRepeatKey(key);
975            startLongPressTimer(key);
976            setPressedKeyGraphics(key, eventTime);
977        }
978    }
979
980    private void startSlidingKeyInput(final Key key) {
981        if (!mIsInSlidingKeyInput) {
982            mIsInSlidingKeyInputFromModifier = key.isModifier();
983        }
984        mIsInSlidingKeyInput = true;
985    }
986
987    private void resetSlidingKeyInput() {
988        mIsInSlidingKeyInput = false;
989        mIsInSlidingKeyInputFromModifier = false;
990        mDrawingProxy.dismissSlidingKeyInputPreview();
991    }
992
993    private void onGestureMoveEvent(final int x, final int y, final long eventTime,
994            final boolean isMajorEvent, final Key key) {
995        final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
996        if (mIsDetectingGesture) {
997            final int beforeLength = mGestureStrokeWithPreviewPoints.getLength();
998            final boolean onValidArea = mGestureStrokeWithPreviewPoints.addPointOnKeyboard(
999                    x, y, gestureTime, isMajorEvent);
1000            if (mGestureStrokeWithPreviewPoints.getLength() > beforeLength) {
1001                mTimerProxy.startUpdateBatchInputTimer(this);
1002            }
1003            // If the move event goes out from valid batch input area, cancel batch input.
1004            if (!onValidArea) {
1005                cancelBatchInput();
1006                return;
1007            }
1008            // If the MoreKeysPanel is showing then do not attempt to enter gesture mode. However,
1009            // the gestured touch points are still being recorded in case the panel is dismissed.
1010            if (isShowingMoreKeysPanel()) {
1011                return;
1012            }
1013            mayStartBatchInput(key);
1014            if (sInGesture) {
1015                mayUpdateBatchInput(eventTime, key);
1016            }
1017        }
1018    }
1019
1020    private void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
1021        if (DEBUG_MOVE_EVENT) {
1022            printTouchEvent("onMoveEvent:", x, y, eventTime);
1023        }
1024        if (mIsTrackingForActionDisabled) {
1025            return;
1026        }
1027
1028        if (sShouldHandleGesture && me != null) {
1029            // Add historical points to gesture path.
1030            final int pointerIndex = me.findPointerIndex(mPointerId);
1031            final int historicalSize = me.getHistorySize();
1032            for (int h = 0; h < historicalSize; h++) {
1033                final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
1034                final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
1035                final long historicalTime = me.getHistoricalEventTime(h);
1036                onGestureMoveEvent(historicalX, historicalY, historicalTime,
1037                        false /* isMajorEvent */, null);
1038            }
1039        }
1040
1041        if (isShowingMoreKeysPanel()) {
1042            final int translatedX = mMoreKeysPanel.translateX(x);
1043            final int translatedY = mMoreKeysPanel.translateY(y);
1044            mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime);
1045            onMoveKey(x, y);
1046            if (mIsInSlidingKeyInputFromModifier) {
1047                mDrawingProxy.showSlidingKeyInputPreview(this);
1048            }
1049            return;
1050        }
1051        onMoveEventInternal(x, y, eventTime);
1052    }
1053
1054    private void processSlidingKeyInput(final Key newKey, final int x, final int y,
1055            final long eventTime) {
1056        // This onPress call may have changed keyboard layout. Those cases are detected
1057        // at {@link #setKeyboard}. In those cases, we should update key according
1058        // to the new keyboard layout.
1059        Key key = newKey;
1060        if (callListenerOnPressAndCheckKeyboardLayoutChange(key, false /* isRepeatKey */)) {
1061            key = onMoveKey(x, y);
1062        }
1063        onMoveToNewKey(key, x, y);
1064        if (mIsTrackingForActionDisabled) {
1065            return;
1066        }
1067        startLongPressTimer(key);
1068        setPressedKeyGraphics(key, eventTime);
1069    }
1070
1071    private void processPhantomSuddenMoveHack(final Key key, final int x, final int y,
1072            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1073        if (DEBUG_MODE) {
1074            Log.w(TAG, String.format("[%d] onMoveEvent:"
1075                    + " phantom sudden move event (distance=%d) is translated to "
1076                    + "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId,
1077                    getDistance(x, y, lastX, lastY),
1078                    lastX, lastY, Constants.printableCode(oldKey.mCode),
1079                    x, y, Constants.printableCode(key.mCode)));
1080        }
1081        // TODO: This should be moved to outside of this nested if-clause?
1082        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1083            ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
1084        }
1085        onUpEventInternal(x, y, eventTime);
1086        onDownEventInternal(x, y, eventTime);
1087    }
1088
1089    private void processProximateBogusDownMoveUpEventHack(final Key key, final int x, final int y,
1090            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1091        if (DEBUG_MODE) {
1092            final float keyDiagonal = (float)Math.hypot(
1093                    mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1094            final float radiusRatio =
1095                    mBogusMoveEventDetector.getDistanceFromDownEvent(x, y)
1096                    / keyDiagonal;
1097            Log.w(TAG, String.format("[%d] onMoveEvent:"
1098                    + " bogus down-move-up event (raidus=%.2f key diagonal) is "
1099                    + " translated to up[%d,%d,%s]/down[%d,%d,%s] events",
1100                    mPointerId, radiusRatio,
1101                    lastX, lastY, Constants.printableCode(oldKey.mCode),
1102                    x, y, Constants.printableCode(key.mCode)));
1103        }
1104        onUpEventInternal(x, y, eventTime);
1105        onDownEventInternal(x, y, eventTime);
1106    }
1107
1108    private void processSildeOutFromOldKey(final Key oldKey) {
1109        setReleasedKeyGraphics(oldKey);
1110        callListenerOnRelease(oldKey, oldKey.mCode, true /* withSliding */);
1111        startSlidingKeyInput(oldKey);
1112        mTimerProxy.cancelKeyTimers();
1113    }
1114
1115    private void slideFromOldKeyToNewKey(final Key key, final int x, final int y,
1116            final long eventTime, final Key oldKey, final int lastX, final int lastY) {
1117        // The pointer has been slid in to the new key from the previous key, we must call
1118        // onRelease() first to notify that the previous key has been released, then call
1119        // onPress() to notify that the new key is being pressed.
1120        processSildeOutFromOldKey(oldKey);
1121        startRepeatKey(key);
1122        if (mIsAllowedSlidingKeyInput) {
1123            processSlidingKeyInput(key, x, y, eventTime);
1124        }
1125        // HACK: On some devices, quick successive touches may be reported as a sudden move by
1126        // touch panel firmware. This hack detects such cases and translates the move event to
1127        // successive up and down events.
1128        // TODO: Should find a way to balance gesture detection and this hack.
1129        else if (sNeedsPhantomSuddenMoveEventHack
1130                && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) {
1131            processPhantomSuddenMoveHack(key, x, y, eventTime, oldKey, lastX, lastY);
1132        }
1133        // HACK: On some devices, quick successive proximate touches may be reported as a bogus
1134        // down-move-up event by touch panel firmware. This hack detects such cases and breaks
1135        // these events into separate up and down events.
1136        else if (sNeedsProximateBogusDownMoveUpEventHack && sTimeRecorder.isInFastTyping(eventTime)
1137                && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) {
1138            processProximateBogusDownMoveUpEventHack(key, x, y, eventTime, oldKey, lastX, lastY);
1139        }
1140        // HACK: If there are currently multiple touches, register the key even if the finger
1141        // slides off the key. This defends against noise from some touch panels when there are
1142        // close multiple touches.
1143        // Caveat: When in chording input mode with a modifier key, we don't use this hack.
1144        else if (getActivePointerTrackerCount() > 1
1145                && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
1146            if (DEBUG_MODE) {
1147                Log.w(TAG, String.format("[%d] onMoveEvent:"
1148                        + " detected sliding finger while multi touching", mPointerId));
1149            }
1150            onUpEvent(x, y, eventTime);
1151            cancelTrackingForAction();
1152            setReleasedKeyGraphics(oldKey);
1153        } else {
1154            if (!mIsDetectingGesture) {
1155                cancelTrackingForAction();
1156            }
1157            setReleasedKeyGraphics(oldKey);
1158        }
1159    }
1160
1161    private void slideOutFromOldKey(final Key oldKey, final int x, final int y) {
1162        // The pointer has been slid out from the previous key, we must call onRelease() to
1163        // notify that the previous key has been released.
1164        processSildeOutFromOldKey(oldKey);
1165        if (mIsAllowedSlidingKeyInput) {
1166            onMoveToNewKey(null, x, y);
1167        } else {
1168            if (!mIsDetectingGesture) {
1169                cancelTrackingForAction();
1170            }
1171        }
1172    }
1173
1174    private void onMoveEventInternal(final int x, final int y, final long eventTime) {
1175        final int lastX = mLastX;
1176        final int lastY = mLastY;
1177        final Key oldKey = mCurrentKey;
1178        final Key newKey = onMoveKey(x, y);
1179
1180        if (sShouldHandleGesture) {
1181            // Register move event on gesture tracker.
1182            onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey);
1183            if (sInGesture) {
1184                mCurrentKey = null;
1185                setReleasedKeyGraphics(oldKey);
1186                return;
1187            }
1188        }
1189
1190        if (newKey != null) {
1191            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1192                slideFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY);
1193            } else if (oldKey == null) {
1194                // The pointer has been slid in to the new key, but the finger was not on any keys.
1195                // In this case, we must call onPress() to notify that the new key is being pressed.
1196                processSlidingKeyInput(newKey, x, y, eventTime);
1197            }
1198        } else { // newKey == null
1199            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) {
1200                slideOutFromOldKey(oldKey, x, y);
1201            }
1202        }
1203        if (mIsInSlidingKeyInputFromModifier) {
1204            mDrawingProxy.showSlidingKeyInputPreview(this);
1205        }
1206    }
1207
1208    private void onUpEvent(final int x, final int y, final long eventTime) {
1209        if (DEBUG_EVENT) {
1210            printTouchEvent("onUpEvent  :", x, y, eventTime);
1211        }
1212
1213        mTimerProxy.cancelUpdateBatchInputTimer(this);
1214        if (!sInGesture) {
1215            if (mCurrentKey != null && mCurrentKey.isModifier()) {
1216                // Before processing an up event of modifier key, all pointers already being
1217                // tracked should be released.
1218                sPointerTrackerQueue.releaseAllPointersExcept(this, eventTime);
1219            } else {
1220                sPointerTrackerQueue.releaseAllPointersOlderThan(this, eventTime);
1221            }
1222        }
1223        onUpEventInternal(x, y, eventTime);
1224        sPointerTrackerQueue.remove(this);
1225    }
1226
1227    // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
1228    // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
1229    // "virtual" up event.
1230    @Override
1231    public void onPhantomUpEvent(final long eventTime) {
1232        if (DEBUG_EVENT) {
1233            printTouchEvent("onPhntEvent:", mLastX, mLastY, eventTime);
1234        }
1235        if (isShowingMoreKeysPanel()) {
1236            return;
1237        }
1238        onUpEventInternal(mLastX, mLastY, eventTime);
1239        cancelTrackingForAction();
1240    }
1241
1242    private void onUpEventInternal(final int x, final int y, final long eventTime) {
1243        mTimerProxy.cancelKeyTimers();
1244        final boolean isInSlidingKeyInput = mIsInSlidingKeyInput;
1245        final boolean isInSlidingKeyInputFromModifier = mIsInSlidingKeyInputFromModifier;
1246        resetSlidingKeyInput();
1247        mIsDetectingGesture = false;
1248        final Key currentKey = mCurrentKey;
1249        mCurrentKey = null;
1250        // Release the last pressed key.
1251        setReleasedKeyGraphics(currentKey);
1252
1253        if (isShowingMoreKeysPanel()) {
1254            if (!mIsTrackingForActionDisabled) {
1255                final int translatedX = mMoreKeysPanel.translateX(x);
1256                final int translatedY = mMoreKeysPanel.translateY(y);
1257                mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId, eventTime);
1258            }
1259            mMoreKeysPanel.dismissMoreKeysPanel();
1260            mMoreKeysPanel = null;
1261            return;
1262        }
1263
1264        if (sInGesture) {
1265            if (currentKey != null) {
1266                callListenerOnRelease(currentKey, currentKey.mCode, true /* withSliding */);
1267            }
1268            mayEndBatchInput(eventTime);
1269            return;
1270        }
1271
1272        if (mIsTrackingForActionDisabled) {
1273            return;
1274        }
1275        if (currentKey != null && currentKey.isRepeatable() && !isInSlidingKeyInput) {
1276            // Repeatable key has been registered in {@link #onDownEventInternal(int,int,long)}.
1277            return;
1278        }
1279        detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
1280        if (isInSlidingKeyInputFromModifier) {
1281            callListenerOnFinishSlidingInput();
1282        }
1283    }
1284
1285    public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
1286        setReleasedKeyGraphics(mCurrentKey);
1287        final int translatedX = panel.translateX(mLastX);
1288        final int translatedY = panel.translateY(mLastY);
1289        panel.onDownEvent(translatedX, translatedY, mPointerId, SystemClock.uptimeMillis());
1290        mMoreKeysPanel = panel;
1291    }
1292
1293    @Override
1294    public void cancelTrackingForAction() {
1295        if (isShowingMoreKeysPanel()) {
1296            return;
1297        }
1298        mIsTrackingForActionDisabled = true;
1299    }
1300
1301    public void onLongPressed() {
1302        resetSlidingKeyInput();
1303        cancelTrackingForAction();
1304        setReleasedKeyGraphics(mCurrentKey);
1305        sPointerTrackerQueue.remove(this);
1306    }
1307
1308    private void onCancelEvent(final int x, final int y, final long eventTime) {
1309        if (DEBUG_EVENT) {
1310            printTouchEvent("onCancelEvt:", x, y, eventTime);
1311        }
1312
1313        cancelBatchInput();
1314        cancelAllPointerTrackers();
1315        sPointerTrackerQueue.releaseAllPointers(eventTime);
1316        onCancelEventInternal();
1317    }
1318
1319    private void onCancelEventInternal() {
1320        mTimerProxy.cancelKeyTimers();
1321        setReleasedKeyGraphics(mCurrentKey);
1322        resetSlidingKeyInput();
1323        if (isShowingMoreKeysPanel()) {
1324            mMoreKeysPanel.dismissMoreKeysPanel();
1325            mMoreKeysPanel = null;
1326        }
1327    }
1328
1329    private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
1330            final Key newKey) {
1331        if (mKeyDetector == null) {
1332            throw new NullPointerException("keyboard and/or key detector not set");
1333        }
1334        final Key curKey = mCurrentKey;
1335        if (newKey == curKey) {
1336            return false;
1337        }
1338        if (curKey == null /* && newKey != null */) {
1339            return true;
1340        }
1341        // Here curKey points to the different key from newKey.
1342        final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared(
1343                mIsInSlidingKeyInputFromModifier);
1344        final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y);
1345        if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) {
1346            if (DEBUG_MODE) {
1347                final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared)
1348                        / mKeyboard.mMostCommonKeyWidth;
1349                Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1350                        +" %.2f key width from key edge", mPointerId, distanceToEdgeRatio));
1351            }
1352            return true;
1353        }
1354        if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput
1355                && sTimeRecorder.isInFastTyping(eventTime)
1356                && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) {
1357            if (DEBUG_MODE) {
1358                final float keyDiagonal = (float)Math.hypot(
1359                        mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
1360                final float lengthFromDownRatio =
1361                        mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal;
1362                Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
1363                        + " %.2f key diagonal from virtual down point",
1364                        mPointerId, lengthFromDownRatio));
1365            }
1366            return true;
1367        }
1368        return false;
1369    }
1370
1371    private void startLongPressTimer(final Key key) {
1372        if (sInGesture) return;
1373        if (key == null) return;
1374        if (!key.isLongPressEnabled()) return;
1375        // Caveat: Please note that isLongPressEnabled() can be true even if the current key
1376        // doesn't have its more keys. (e.g. spacebar, globe key)
1377        // We always need to start the long press timer if the key has its more keys regardless of
1378        // whether or not we are in the sliding input mode.
1379        if (mIsInSlidingKeyInput && key.mMoreKeys == null) return;
1380        final int delay;
1381        switch (key.mCode) {
1382        case Constants.CODE_SHIFT:
1383            delay = sParams.mLongPressShiftLockTimeout;
1384            break;
1385        default:
1386            final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout;
1387            if (mIsInSlidingKeyInputFromModifier) {
1388                // We use longer timeout for sliding finger input started from the modifier key.
1389                delay = longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
1390            } else {
1391                delay = longpressTimeout;
1392            }
1393            break;
1394        }
1395        mTimerProxy.startLongPressTimer(this, delay);
1396    }
1397
1398    private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
1399        if (key == null) {
1400            callListenerOnCancelInput();
1401            return;
1402        }
1403
1404        final int code = key.mCode;
1405        callListenerOnCodeInput(key, code, x, y, eventTime);
1406        callListenerOnRelease(key, code, false /* withSliding */);
1407    }
1408
1409    private void startRepeatKey(final Key key) {
1410        if (sInGesture) return;
1411        if (key == null) return;
1412        if (!key.isRepeatable()) return;
1413        // Don't start key repeat when we are in sliding input mode.
1414        if (mIsInSlidingKeyInput) return;
1415        detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
1416        mTimerProxy.startKeyRepeatTimer(this, sParams.mKeyRepeatStartTimeout);
1417    }
1418
1419    public void onKeyRepeat(final int code) {
1420        final Key key = getKey();
1421        if (key == null || key.mCode != code) {
1422            return;
1423        }
1424        mTimerProxy.startKeyRepeatTimer(this, sParams.mKeyRepeatInterval);
1425        callListenerOnPressAndCheckKeyboardLayoutChange(key, true /* isRepeatKey */);
1426        callListenerOnCodeInput(key, code, mKeyX, mKeyY, SystemClock.uptimeMillis());
1427    }
1428
1429    private void printTouchEvent(final String title, final int x, final int y,
1430            final long eventTime) {
1431        final Key key = mKeyDetector.detectHitKey(x, y);
1432        final String code = KeyDetector.printableCode(key);
1433        Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
1434                (mIsTrackingForActionDisabled ? "-" : " "), title, x, y, eventTime, code));
1435    }
1436}
1437