Fix gesture detection algorithm

This change also
  * Introduces adaptive gesture detecting threshold for time domain in addition to length domain.
  * Tunes the parameters for detecting gesture after fast typing.
  * Fixes a bug in dismissing gesture floating preview text.
  * Cleanup debug messages

Bug: 7218902
Change-Id: Iafccd872c6efe0c3b5ae65fa40b04c80d9f139c7
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 5248b8a..100a9c6 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -168,8 +168,8 @@
     private static long sLastLetterTypingUpTime;
     private static final InputPointers sAggregratedPointers = new InputPointers(
             GestureStroke.DEFAULT_CAPACITY);
-    private static int sLastRecognitionPointSize = 0;
-    private static long sLastRecognitionTime = 0;
+    private static int sLastRecognitionPointSize = 0; // synchronized using sAggregratedPointers
+    private static long sLastRecognitionTime = 0; // synchronized using sAggregratedPointers
 
     // The position and time at which first down event occurred.
     private long mDownTime;
@@ -306,9 +306,10 @@
         }
         final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
         if (DEBUG_LISTENER) {
-            Log.d(TAG, "onPress    : " + KeyDetector.printableCode(key)
-                    + " ignoreModifier=" + ignoreModifierKey
-                    + " enabled=" + key.isEnabled());
+            Log.d(TAG, String.format("[%d] onPress    : %s%s%s", mPointerId,
+                    KeyDetector.printableCode(key),
+                    ignoreModifierKey ? " ignoreModifier" : "",
+                    key.isEnabled() ? "" : " disabled"));
         }
         if (ignoreModifierKey) {
             return false;
@@ -331,10 +332,11 @@
         final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
         final int code = altersCode ? key.getAltCode() : primaryCode;
         if (DEBUG_LISTENER) {
-            Log.d(TAG, "onCodeInput: " + Keyboard.printableCode(code)
-                    + " text=" + key.getOutputText() + " x=" + x + " y=" + y
-                    + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode
-                    + " enabled=" + key.isEnabled());
+            final String output = code == Keyboard.CODE_OUTPUT_TEXT
+                    ? key.getOutputText() : Keyboard.printableCode(code);
+            Log.d(TAG, String.format("[%d] onCodeInput: %4d %4d %s%s%s", mPointerId, x, y,
+                    output, ignoreModifierKey ? " ignoreModifier" : "",
+                    altersCode ? " altersCode" : "", key.isEnabled() ? "" : " disabled"));
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.pointerTracker_callListenerOnCodeInput(key, x, y, ignoreModifierKey,
@@ -362,9 +364,10 @@
         }
         final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
         if (DEBUG_LISTENER) {
-            Log.d(TAG, "onRelease  : " + Keyboard.printableCode(primaryCode)
-                    + " sliding=" + withSliding + " ignoreModifier=" + ignoreModifierKey
-                    + " enabled="+ key.isEnabled());
+            Log.d(TAG, String.format("[%d] onRelease  : %s%s%s%s", mPointerId,
+                    Keyboard.printableCode(primaryCode),
+                    withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : "",
+                    key.isEnabled() ?  "": " disabled"));
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding,
@@ -380,7 +383,7 @@
 
     private void callListenerOnCancelInput() {
         if (DEBUG_LISTENER) {
-            Log.d(TAG, "onCancelInput");
+            Log.d(TAG, String.format("[%d] onCancelInput", mPointerId));
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.pointerTracker_callListenerOnCancelInput();
@@ -389,6 +392,10 @@
     }
 
     private void setKeyDetectorInner(final KeyDetector keyDetector) {
+        final Keyboard keyboard = keyDetector.getKeyboard();
+        if (keyDetector == mKeyDetector && keyboard == mKeyboard) {
+            return;
+        }
         mKeyDetector = keyDetector;
         mKeyboard = keyDetector.getKeyboard();
         mGestureStrokeWithPreviewPoints.setKeyboardGeometry(mKeyboard.mMostCommonKeyWidth);
@@ -551,10 +558,15 @@
             return;
         }
         if (DEBUG_LISTENER) {
-            Log.d(TAG, "onStartBatchInput");
+            Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId));
         }
         sInGesture = true;
-        mListener.onStartBatchInput();
+        synchronized (sAggregratedPointers) {
+            sAggregratedPointers.reset();
+            sLastRecognitionPointSize = 0;
+            sLastRecognitionTime = 0;
+            mListener.onStartBatchInput();
+        }
         final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
         mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
     }
@@ -569,7 +581,8 @@
                     sLastRecognitionPointSize = size;
                     sLastRecognitionTime = eventTime;
                     if (DEBUG_LISTENER) {
-                        Log.d(TAG, "onUpdateBatchInput: batchPoints=" + size);
+                        Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d",
+                                mPointerId, size));
                     }
                     mListener.onUpdateBatchInput(sAggregratedPointers);
                 }
@@ -582,36 +595,19 @@
     private void mayEndBatchInput() {
         synchronized (sAggregratedPointers) {
             mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers);
-            mGestureStrokeWithPreviewPoints.reset();
             if (getActivePointerTrackerCount() == 1) {
                 if (DEBUG_LISTENER) {
-                    Log.d(TAG, "onEndBatchInput: batchPoints="
-                            + sAggregratedPointers.getPointerSize());
+                    Log.d(TAG, String.format("[%d] onEndBatchInput   : batchPoints=%d",
+                            mPointerId, sAggregratedPointers.getPointerSize()));
                 }
                 sInGesture = false;
                 mListener.onEndBatchInput(sAggregratedPointers);
-                clearBatchInputPointsOfAllPointerTrackers();
             }
         }
         final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
         mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
     }
 
-    private static void abortBatchInput() {
-        clearBatchInputPointsOfAllPointerTrackers();
-    }
-
-    private static void clearBatchInputPointsOfAllPointerTrackers() {
-        final int trackersSize = sTrackers.size();
-        for (int i = 0; i < trackersSize; ++i) {
-            final PointerTracker tracker = sTrackers.get(i);
-            tracker.mGestureStrokeWithPreviewPoints.reset();
-        }
-        sAggregratedPointers.reset();
-        sLastRecognitionPointSize = 0;
-        sLastRecognitionTime = 0;
-    }
-
     public void processMotionEvent(final int action, final int x, final int y, final long eventTime,
             final KeyEventHandler handler) {
         switch (action) {
@@ -681,18 +677,11 @@
             if (getActivePointerTrackerCount() == 1) {
                 sGestureFirstDownTime = eventTime;
             }
-            onGestureDownEvent(x, y, eventTime);
+            mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime,
+                    sLastLetterTypingUpTime);
         }
     }
 
-    private void onGestureDownEvent(final int x, final int y, final long eventTime) {
-        mIsDetectingGesture = true;
-        mGestureStrokeWithPreviewPoints.setLastLetterTypingTime(eventTime, sLastLetterTypingUpTime);
-        final int elapsedTimeFromFirstDown = (int)(eventTime - sGestureFirstDownTime);
-        mGestureStrokeWithPreviewPoints.addPoint(x, y, elapsedTimeFromFirstDown,
-                true /* isMajorEvent */);
-    }
-
     private void onDownEventInternal(final int x, final int y, final long eventTime) {
         Key key = onDownKey(x, y, eventTime);
         // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
@@ -925,9 +914,7 @@
             mayEndBatchInput();
             return;
         }
-        // This event will be recognized as a regular code input. Clear unused possible batch points
-        // so they are not mistakenly displayed as preview.
-        clearBatchInputPointsOfAllPointerTrackers();
+
         if (mKeyAlreadyProcessed) {
             return;
         }
@@ -941,7 +928,6 @@
     }
 
     public void onShowMoreKeysPanel(final int x, final int y, final KeyEventHandler handler) {
-        abortBatchInput();
         onLongPressed();
         mIsShowingMoreKeysPanel = true;
         onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
@@ -1029,7 +1015,7 @@
             final long eventTime) {
         final Key key = mKeyDetector.detectHitKey(x, y);
         final String code = KeyDetector.printableCode(key);
-        Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %s", title,
-                (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, eventTime, code));
+        Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
+                (mKeyAlreadyProcessed ? "-" : " "), title, x, y, eventTime, code));
     }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
index 9fe6fa3..c0e92df 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
@@ -22,6 +22,7 @@
 public class GestureStroke {
     private static final String TAG = GestureStroke.class.getSimpleName();
     private static final boolean DEBUG = false;
+    private static final boolean DEBUG_SPEED = false;
 
     public static final int DEFAULT_CAPACITY = 128;
 
@@ -29,42 +30,52 @@
     private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
     private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
     private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
-    private int mIncrementalRecognitionSize;
-    private int mLastIncrementalBatchSize;
-    private long mLastMajorEventTime;
-    private int mLastMajorEventX;
-    private int mLastMajorEventY;
-    private boolean mAfterFastTyping;
 
-    private int mKeyWidth;
-    private int mStartGestureLengthThresholdAfterFastTyping; // pixel
-    private int mStartGestureLengthThreshold; // pixel
-    private int mMinGestureSamplingLength; // pixel
-    private int mGestureRecognitionSpeedThreshold; // pixel / sec
+    private int mKeyWidth; // pixel
+    // Static threshold for starting gesture detection
     private int mDetectFastMoveSpeedThreshold; // pixel /sec
     private int mDetectFastMoveTime;
     private int mDetectFastMoveX;
     private int mDetectFastMoveY;
+    // Dynamic threshold for gesture after fast typing
+    private boolean mAfterFastTyping;
+    private int mGestureDynamicDistanceThresholdFrom; // pixel
+    private int mGestureDynamicDistanceThresholdTo; // pixel
+    // Variables for gesture sampling
+    private int mGestureSamplingMinimumDistance; // pixel
+    private long mLastMajorEventTime;
+    private int mLastMajorEventX;
+    private int mLastMajorEventY;
+    // Variables for gesture recognition
+    private int mGestureRecognitionSpeedThreshold; // pixel / sec
+    private int mIncrementalRecognitionSize;
+    private int mLastIncrementalBatchSize;
 
     // TODO: Move some of these to resource.
-    private static final int GESTURE_AFTER_FAST_TYPING_DURATION_THRESHOLD = 350; // msec
-    private static final float START_GESTURE_LENGTH_THRESHOLD_AFTER_FAST_TYPING_RATIO_TO_KEY_WIDTH =
-            8.0f;
-    private static final int START_GESTURE_LENGTH_THRESHOLD_DECAY_DURATION = 400; // msec
-    private static final float START_GESTURE_LENGTH_THRESHOLD_RATIO_TO_KEY_WIDTH = 0.6f;
-    private static final int START_GESTURE_DURATION_THRESHOLD = 70; // msec
-    private static final int MIN_GESTURE_RECOGNITION_TIME = 100; // msec
-    private static final float MIN_GESTURE_SAMPLING_RATIO_TO_KEY_WIDTH = 1.0f / 6.0f;
-    private static final float GESTURE_RECOGNITION_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH =
-            5.5f; // keyWidth / sec
-    private static final float DETECT_FAST_MOVE_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH =
-            5.0f; // keyWidth / sec
-    private static final int MSEC_PER_SEC = 1000;
 
-    public static final boolean hasRecognitionTimePast(
-            final long currentTime, final long lastRecognitionTime) {
-        return currentTime > lastRecognitionTime + MIN_GESTURE_RECOGNITION_TIME;
-    }
+    // Static threshold for gesture after fast typing
+    public static final int GESTURE_STATIC_TIME_THRESHOLD_AFTER_FAST_TYPING = 350; // msec
+
+    // Static threshold for starting gesture detection
+    private static final float DETECT_FAST_MOVE_SPEED_THRESHOLD = 1.5f; // keyWidth / sec
+
+    // Dynamic threshold for gesture after fast typing
+    private static final int GESTURE_DYNAMIC_THRESHOLD_DECAY_DURATION = 450; // msec
+    // Time based threshold values
+    private static final int GESTURE_DYNAMIC_TIME_THRESHOLD_FROM = 300; // msec
+    private static final int GESTURE_DYNAMIC_TIME_THRESHOLD_TO = 20; // msec
+    // Distance based threshold values
+    private static final float GESTURE_DYNAMIC_DISTANCE_THRESHOLD_FROM = 6.0f; // keyWidth
+    private static final float GESTURE_DYNAMIC_DISTANCE_THRESHOLD_TO = 0.35f; // keyWidth
+
+    // Parameters for gesture sampling
+    private static final float GESTURE_SAMPLING_MINIMUM_DISTANCE = 1.0f / 6.0f; // keyWidth
+
+    // Parameters for gesture recognition
+    private static final int GESTURE_RECOGNITION_MINIMUM_TIME = 100; // msec
+    private static final float GESTURE_RECOGNITION_SPEED_THRESHOLD = 5.5f; // keyWidth / sec
+
+    private static final int MSEC_PER_SEC = 1000;
 
     public GestureStroke(final int pointerId) {
         mPointerId = pointerId;
@@ -73,41 +84,58 @@
     public void setKeyboardGeometry(final int keyWidth) {
         mKeyWidth = keyWidth;
         // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key?
-        mStartGestureLengthThresholdAfterFastTyping = (int)(keyWidth
-                * START_GESTURE_LENGTH_THRESHOLD_AFTER_FAST_TYPING_RATIO_TO_KEY_WIDTH);
-        mStartGestureLengthThreshold =
-                (int)(keyWidth * START_GESTURE_LENGTH_THRESHOLD_RATIO_TO_KEY_WIDTH);
-        mMinGestureSamplingLength = (int)(keyWidth * MIN_GESTURE_SAMPLING_RATIO_TO_KEY_WIDTH);
+        mDetectFastMoveSpeedThreshold = (int)(keyWidth * DETECT_FAST_MOVE_SPEED_THRESHOLD);
+        mGestureDynamicDistanceThresholdFrom =
+                (int)(keyWidth * GESTURE_DYNAMIC_DISTANCE_THRESHOLD_FROM);
+        mGestureDynamicDistanceThresholdTo =
+                (int)(keyWidth * GESTURE_DYNAMIC_DISTANCE_THRESHOLD_TO);
+        mGestureSamplingMinimumDistance = (int)(keyWidth * GESTURE_SAMPLING_MINIMUM_DISTANCE);
         mGestureRecognitionSpeedThreshold =
-                (int)(keyWidth * GESTURE_RECOGNITION_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH);
-        mDetectFastMoveSpeedThreshold =
-                (int)(keyWidth * DETECT_FAST_MOVE_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH);
+                (int)(keyWidth * GESTURE_RECOGNITION_SPEED_THRESHOLD);
         if (DEBUG) {
-            Log.d(TAG, "[" + mPointerId + "] setKeyboardGeometry: keyWidth=" + keyWidth
-                    + " tL0=" + mStartGestureLengthThresholdAfterFastTyping
-                    + " tL=" + mStartGestureLengthThreshold);
+            Log.d(TAG, String.format(
+                    "[%d] setKeyboardGeometry: keyWidth=%3d tT=%3d >> %3d tD=%3d >> %3d",
+                    mPointerId, keyWidth,
+                    GESTURE_DYNAMIC_TIME_THRESHOLD_FROM,
+                    GESTURE_DYNAMIC_TIME_THRESHOLD_TO,
+                    mGestureDynamicDistanceThresholdFrom,
+                    mGestureDynamicDistanceThresholdTo));
         }
     }
 
-    public void setLastLetterTypingTime(final long downTime, final long lastTypingTime) {
-        final long elpasedTimeAfterTyping = downTime - lastTypingTime;
-        if (elpasedTimeAfterTyping < GESTURE_AFTER_FAST_TYPING_DURATION_THRESHOLD) {
+    public void onDownEvent(final int x, final int y, final long downTime,
+            final long gestureFirstDownTime, final long lastTypingTime) {
+        reset();
+        final long elapsedTimeAfterTyping = downTime - lastTypingTime;
+        if (elapsedTimeAfterTyping < GESTURE_STATIC_TIME_THRESHOLD_AFTER_FAST_TYPING) {
             mAfterFastTyping = true;
         }
         if (DEBUG) {
-            Log.d(TAG, "[" + mPointerId + "] setLastTypingTime: dT=" + elpasedTimeAfterTyping
-                    + " afterFastTyping=" + mAfterFastTyping);
+            Log.d(TAG, String.format("[%d] onDownEvent: dT=%3d%s", mPointerId,
+                    elapsedTimeAfterTyping, mAfterFastTyping ? " afterFastTyping" : ""));
         }
+        final int elapsedTimeFromFirstDown = (int)(downTime - gestureFirstDownTime);
+        addPoint(x, y, elapsedTimeFromFirstDown, true /* isMajorEvent */);
     }
 
-    private int getStartGestureLengthThreshold(final int deltaTime) {
-        if (!mAfterFastTyping || deltaTime >= START_GESTURE_LENGTH_THRESHOLD_DECAY_DURATION) {
-            return mStartGestureLengthThreshold;
+    private int getGestureDynamicDistanceThreshold(final int deltaTime) {
+        if (!mAfterFastTyping || deltaTime >= GESTURE_DYNAMIC_THRESHOLD_DECAY_DURATION) {
+            return mGestureDynamicDistanceThresholdTo;
         }
         final int decayedThreshold =
-                (mStartGestureLengthThresholdAfterFastTyping - mStartGestureLengthThreshold)
-                * deltaTime / START_GESTURE_LENGTH_THRESHOLD_DECAY_DURATION;
-        return mStartGestureLengthThresholdAfterFastTyping - decayedThreshold;
+                (mGestureDynamicDistanceThresholdFrom - mGestureDynamicDistanceThresholdTo)
+                * deltaTime / GESTURE_DYNAMIC_THRESHOLD_DECAY_DURATION;
+        return mGestureDynamicDistanceThresholdFrom - decayedThreshold;
+    }
+
+    private int getGestureDynamicTimeThreshold(final int deltaTime) {
+        if (!mAfterFastTyping || deltaTime >= GESTURE_DYNAMIC_THRESHOLD_DECAY_DURATION) {
+            return GESTURE_DYNAMIC_TIME_THRESHOLD_TO;
+        }
+        final int decayedThreshold =
+                (GESTURE_DYNAMIC_TIME_THRESHOLD_FROM - GESTURE_DYNAMIC_TIME_THRESHOLD_TO)
+                * deltaTime / GESTURE_DYNAMIC_THRESHOLD_DECAY_DURATION;
+        return GESTURE_DYNAMIC_TIME_THRESHOLD_FROM - decayedThreshold;
     }
 
     public boolean isStartOfAGesture() {
@@ -120,21 +148,24 @@
         }
         final int lastIndex = size - 1;
         final int deltaTime = mEventTimes.get(lastIndex) - mDetectFastMoveTime;
-        final int deltaLength = getDistance(
+        final int deltaDistance = getDistance(
                 mXCoordinates.get(lastIndex), mYCoordinates.get(lastIndex),
                 mDetectFastMoveX, mDetectFastMoveY);
-        final int startGestureLengthThreshold = getStartGestureLengthThreshold(deltaTime);
-        final boolean isStartOfAGesture = deltaTime > START_GESTURE_DURATION_THRESHOLD
-                && deltaLength > startGestureLengthThreshold;
+        final int distanceThreshold = getGestureDynamicDistanceThreshold(deltaTime);
+        final int timeThreshold = getGestureDynamicTimeThreshold(deltaTime);
+        final boolean isStartOfAGesture = deltaTime >= timeThreshold
+                && deltaDistance >= distanceThreshold;
         if (DEBUG) {
-            Log.d(TAG, "[" + mPointerId + "] isStartOfAGesture: dT=" + deltaTime
-                    + " dL=" + deltaLength + " tL=" + startGestureLengthThreshold
-                    + " points=" + size + (isStartOfAGesture ? " Detect start of a gesture" : ""));
+            Log.d(TAG, String.format("[%d] isStartOfAGesture: dT=%3d tT=%3d dD=%3d tD=%3d%s%s",
+                    mPointerId, deltaTime, timeThreshold,
+                    deltaDistance, distanceThreshold,
+                    mAfterFastTyping ? " afterFastTyping" : "",
+                    isStartOfAGesture ? " startOfAGesture" : ""));
         }
         return isStartOfAGesture;
     }
 
-    public void reset() {
+    protected void reset() {
         mIncrementalRecognitionSize = 0;
         mLastIncrementalBatchSize = 0;
         mEventTimes.setLength(0);
@@ -167,15 +198,17 @@
         if (msecs > 0) {
             final int pixels = getDistance(lastX, lastY, x, y);
             final int pixelsPerSec = pixels * MSEC_PER_SEC;
-            if (DEBUG) {
+            if (DEBUG_SPEED) {
                 final float speed = (float)pixelsPerSec / msecs / mKeyWidth;
-                Log.d(TAG, String.format("[" + mPointerId + "] speed=%.3f", speed));
+                Log.d(TAG, String.format("[%d] detectFastMove: speed=%5.2f", mPointerId, speed));
             }
             // Equivalent to (pixels / msecs < mStartSpeedThreshold / MSEC_PER_SEC)
             if (mDetectFastMoveTime == 0 && pixelsPerSec > mDetectFastMoveSpeedThreshold * msecs) {
                 if (DEBUG) {
-                    Log.d(TAG, "[" + mPointerId + "] detect fast move: T="
-                            + time + " points = " + size);
+                    final float speed = (float)pixelsPerSec / msecs / mKeyWidth;
+                    Log.d(TAG, String.format(
+                            "[%d] detectFastMove: speed=%5.2f T=%3d points=%3d fastMove",
+                            mPointerId, speed, time, size));
                 }
                 mDetectFastMoveTime = time;
                 mDetectFastMoveX = x;
@@ -192,8 +225,8 @@
             appendPoint(x, y, time);
             updateMajorEvent(x, y, time);
         } else {
-            final int dist = detectFastMove(x, y, time);
-            if (dist > mMinGestureSamplingLength) {
+            final int distance = detectFastMove(x, y, time);
+            if (distance > mGestureSamplingMinimumDistance) {
                 appendPoint(x, y, time);
             }
         }
@@ -216,6 +249,11 @@
         }
     }
 
+    public static final boolean hasRecognitionTimePast(
+            final long currentTime, final long lastRecognitionTime) {
+        return currentTime > lastRecognitionTime + GESTURE_RECOGNITION_MINIMUM_TIME;
+    }
+
     public void appendAllBatchPoints(final InputPointers out) {
         appendBatchPoints(out, mEventTimes.getLength());
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
index 5b3f318..05e0a2e 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
@@ -38,7 +38,7 @@
     }
 
     @Override
-    public void reset() {
+    protected void reset() {
         super.reset();
         mStrokeId++;
         mLastPreviewSize = 0;
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 3e89330..268d36a 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1402,6 +1402,7 @@
 
     @Override
     public void onStartBatchInput() {
+        BatchInputUpdater.getInstance().onStartBatchInput();
         mConnection.beginBatchEdit();
         if (mWordComposer.isComposingWord()) {
             if (ProductionFlag.IS_INTERNAL) {
@@ -1433,6 +1434,7 @@
     private static final class BatchInputUpdater implements Handler.Callback {
         private final Handler mHandler;
         private LatinIME mLatinIme;
+        private boolean mInBatchInput; // synchornized using "this".
 
         private BatchInputUpdater() {
             final HandlerThread handlerThread = new HandlerThread(
@@ -1456,17 +1458,32 @@
         public boolean handleMessage(final Message msg) {
             switch (msg.what) {
             case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
-                final SuggestedWords suggestedWords = getSuggestedWordsGesture(
-                        (InputPointers)msg.obj, mLatinIme);
-                showGesturePreviewAndSuggestionStrip(
-                        suggestedWords, false /* dismissGestureFloatingPreviewText */, mLatinIme);
+                updateBatchInput((InputPointers)msg.obj, mLatinIme);
                 break;
             }
             return true;
         }
 
-        public void updateGesturePreviewAndSuggestionStrip(final InputPointers batchPointers,
+        // Run in the UI thread.
+        public synchronized void onStartBatchInput() {
+            mInBatchInput = true;
+        }
+
+        // Run in the Handler thread.
+        private synchronized void updateBatchInput(final InputPointers batchPointers,
                 final LatinIME latinIme) {
+            if (!mInBatchInput) {
+                // Batch input has ended while the message was being delivered.
+                return;
+            }
+            final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(
+                    batchPointers, latinIme);
+            latinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+                    suggestedWords, false /* dismissGestureFloatingPreviewText */);
+        }
+
+        // Run in the UI thread.
+        public void onUpdateBatchInput(final InputPointers batchPointers, final LatinIME latinIme) {
             mLatinIme = latinIme;
             if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
                 return;
@@ -1476,15 +1493,20 @@
                     .sendToTarget();
         }
 
-        public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
-                final boolean dismissGestureFloatingPreviewText, final LatinIME latinIme) {
+        // Run in the UI thread.
+        public synchronized SuggestedWords onEndBatchInput(final InputPointers batchPointers,
+                final LatinIME latinIme) {
+            mInBatchInput = false;
+            final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(
+                    batchPointers, latinIme);
             latinIme.mHandler.showGesturePreviewAndSuggestionStrip(
-                    suggestedWords, dismissGestureFloatingPreviewText);
+                    suggestedWords, true /* dismissGestureFloatingPreviewText */);
+            return suggestedWords;
         }
 
         // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
         // be synchronized.
-        public synchronized SuggestedWords getSuggestedWordsGesture(
+        private static SuggestedWords getSuggestedWordsGestureLocked(
                 final InputPointers batchPointers, final LatinIME latinIme) {
             latinIme.mWordComposer.setBatchInputPointers(batchPointers);
             return latinIme.getSuggestedWords(Suggest.SESSION_GESTURE);
@@ -1505,16 +1527,13 @@
 
     @Override
     public void onUpdateBatchInput(final InputPointers batchPointers) {
-        BatchInputUpdater.getInstance().updateGesturePreviewAndSuggestionStrip(batchPointers, this);
+        BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers, this);
     }
 
     @Override
     public void onEndBatchInput(final InputPointers batchPointers) {
-        final BatchInputUpdater batchInputUpdater = BatchInputUpdater.getInstance();
-        final SuggestedWords suggestedWords = batchInputUpdater.getSuggestedWordsGesture(
+        final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput(
                 batchPointers, this);
-        batchInputUpdater.showGesturePreviewAndSuggestionStrip(
-                suggestedWords, true /* dismissGestureFloatingPreviewText */, this);
         final String batchInputText = (suggestedWords.size() > 0)
                 ? suggestedWords.getWord(0) : null;
         if (TextUtils.isEmpty(batchInputText)) {