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