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