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