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