Provide gesture data even if the finger stays still

Bug: 7595187
Change-Id: I6f85124815f18706b2b2b5b5da2783dffd246e8c
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 53051d0..27a5cad 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -145,6 +145,7 @@
         <attr name="gestureSamplingMinimumDistance" format="fraction" />
         <!-- Parameters for gesture recognition (msec) and (keyWidth%/sec) -->
         <attr name="gestureRecognitionMinimumTime" format="integer" />
+        <attr name="gestureRecognitionUpdateTime" format="integer" />
         <attr name="gestureRecognitionSpeedThreshold" format="fraction" />
         <!-- Suppress showing key preview duration after batch input in millisecond -->
         <attr name="suppressKeyPreviewAfterBatchInputDuration" format="integer" />
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index cb13587..78c5f18 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -86,6 +86,7 @@
     <fraction name="config_gesture_sampling_minimum_distance">16.6666%</fraction>
     <!-- Parameters for gesture recognition (msec) and (keyWidth%/sec) -->
     <integer name="config_gesture_recognition_minimum_time">100</integer>
+    <integer name="config_gesture_recognition_update_time">300</integer>
     <fraction name="config_gesture_recognition_speed_threshold">550%</fraction>
     <!-- Suppress showing key preview duration after batch input in millisecond -->
     <integer name="config_suppress_key_preview_after_batch_input_duration">1000</integer>
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index b771463..a60e449 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -104,6 +104,7 @@
         <item name="gestureDynamicDistanceThresholdTo">@fraction/config_gesture_dynamic_distance_threshold_to</item>
         <item name="gestureSamplingMinimumDistance">@fraction/config_gesture_sampling_minimum_distance</item>
         <item name="gestureRecognitionMinimumTime">@integer/config_gesture_recognition_minimum_time</item>
+        <item name="gestureRecognitionUpdateTime">@integer/config_gesture_recognition_update_time</item>
         <item name="gestureRecognitionSpeedThreshold">@fraction/config_gesture_recognition_speed_threshold</item>
         <item name="suppressKeyPreviewAfterBatchInputDuration">@integer/config_suppress_key_preview_after_batch_input_duration</item>
     </style>
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index d5f40ad..b09f9b2 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -29,6 +29,7 @@
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.os.Message;
+import android.os.SystemClock;
 import android.preference.PreferenceManager;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -156,12 +157,14 @@
         private static final int MSG_REPEAT_KEY = 1;
         private static final int MSG_LONGPRESS_KEY = 2;
         private static final int MSG_DOUBLE_TAP = 3;
+        private static final int MSG_UPDATE_BATCH_INPUT = 4;
 
         private final int mKeyRepeatStartTimeout;
         private final int mKeyRepeatInterval;
         private final int mLongPressKeyTimeout;
         private final int mLongPressShiftKeyTimeout;
         private final int mIgnoreAltCodeKeyTimeout;
+        private final int mGestureRecognitionUpdateTime;
 
         public KeyTimerHandler(final MainKeyboardView outerInstance,
                 final TypedArray mainKeyboardViewAttr) {
@@ -177,6 +180,8 @@
                     R.styleable.MainKeyboardView_longPressShiftKeyTimeout, 0);
             mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
                     R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
+            mGestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0);
         }
 
         @Override
@@ -201,6 +206,10 @@
                     KeyboardSwitcher.getInstance().onLongPressTimeout(msg.arg1);
                 }
                 break;
+            case MSG_UPDATE_BATCH_INPUT:
+                tracker.updateBatchInputByTimer(SystemClock.uptimeMillis());
+                startUpdateBatchInputTimer(tracker);
+                break;
             }
         }
 
@@ -349,8 +358,24 @@
             cancelLongPressTimer();
         }
 
+        @Override
+        public void startUpdateBatchInputTimer(final PointerTracker tracker) {
+            if (mGestureRecognitionUpdateTime <= 0) {
+                return;
+            }
+            removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
+            sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker),
+                    mGestureRecognitionUpdateTime);
+        }
+
+        @Override
+        public void cancelAllUpdateBatchInputTimer() {
+            removeMessages(MSG_UPDATE_BATCH_INPUT);
+        }
+
         public void cancelAllMessages() {
             cancelKeyTimers();
+            cancelAllUpdateBatchInputTimer();
         }
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index c8052af..bd4a713 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -94,6 +94,8 @@
         public void cancelDoubleTapTimer();
         public boolean isInDoubleTapTimeout();
         public void cancelKeyTimers();
+        public void startUpdateBatchInputTimer(PointerTracker tracker);
+        public void cancelAllUpdateBatchInputTimer();
 
         public static class Adapter implements TimerProxy {
             @Override
@@ -116,6 +118,10 @@
             public boolean isInDoubleTapTimeout() { return false; }
             @Override
             public void cancelKeyTimers() {}
+            @Override
+            public void startUpdateBatchInputTimer(PointerTracker tracker) {}
+            @Override
+            public void cancelAllUpdateBatchInputTimer() {}
         }
     }
 
@@ -705,27 +711,38 @@
         mDrawingProxy.showGesturePreviewTrail(this, isOldestTrackerInQueue(this));
     }
 
+    public void updateBatchInputByTimer(final long eventTime) {
+        final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
+        mGestureStrokeWithPreviewPoints.duplicateLastPointWith(gestureTime);
+        updateBatchInput(eventTime);
+    }
+
     private void mayUpdateBatchInput(final long eventTime, final Key key) {
         if (key != null) {
-            synchronized (sAggregratedPointers) {
-                final GestureStroke stroke = mGestureStrokeWithPreviewPoints;
-                stroke.appendIncrementalBatchPoints(sAggregratedPointers);
-                final int size = sAggregratedPointers.getPointerSize();
-                if (size > sLastRecognitionPointSize
-                        && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) {
-                    sLastRecognitionPointSize = size;
-                    sLastRecognitionTime = eventTime;
-                    if (DEBUG_LISTENER) {
-                        Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d",
-                                mPointerId, size));
-                    }
-                    mListener.onUpdateBatchInput(sAggregratedPointers);
-                }
-            }
+            updateBatchInput(eventTime);
         }
         mDrawingProxy.showGesturePreviewTrail(this, isOldestTrackerInQueue(this));
     }
 
+    private void updateBatchInput(final long eventTime) {
+        synchronized (sAggregratedPointers) {
+            final GestureStroke stroke = mGestureStrokeWithPreviewPoints;
+            stroke.appendIncrementalBatchPoints(sAggregratedPointers);
+            final int size = sAggregratedPointers.getPointerSize();
+            if (size > sLastRecognitionPointSize
+                    && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) {
+                sLastRecognitionPointSize = size;
+                sLastRecognitionTime = eventTime;
+                if (DEBUG_LISTENER) {
+                    Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", mPointerId,
+                            size));
+                }
+                mTimerProxy.startUpdateBatchInputTimer(this);
+                mListener.onUpdateBatchInput(sAggregratedPointers);
+            }
+        }
+    }
+
     private void mayEndBatchInput(final long eventTime) {
         synchronized (sAggregratedPointers) {
             mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers);
@@ -737,6 +754,7 @@
                         Log.d(TAG, String.format("[%d] onEndBatchInput   : batchPoints=%d",
                                 mPointerId, sAggregratedPointers.getPointerSize()));
                     }
+                    mTimerProxy.cancelAllUpdateBatchInputTimer();
                     mListener.onEndBatchInput(sAggregratedPointers);
                 }
             }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
index aab14e9..a43e94a 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
@@ -228,6 +228,17 @@
         return isStartOfAGesture;
     }
 
+    public void duplicateLastPointWith(final int time) {
+        final int lastIndex = mEventTimes.getLength() - 1;
+        if (lastIndex >= 0) {
+            final int x = mXCoordinates.get(lastIndex);
+            final int y = mYCoordinates.get(lastIndex);
+            // TODO: Have appendMajorPoint()
+            appendPoint(x, y, time);
+            updateIncrementalRecognitionSize(x, y, time);
+        }
+    }
+
     protected void reset() {
         mIncrementalRecognitionSize = 0;
         mLastIncrementalBatchSize = 0;