From eea34598bf63f670f47d7b3f37b6436921e5fe02 Mon Sep 17 00:00:00 2001
From: Tom Ouyang <ouyang@google.com>
Date: Tue, 12 Jun 2012 03:40:37 -0700
Subject: [PATCH] Merging minimal gesture input

Change-Id: Iee6ae48bb6309c2867b5d2e344fe7d86dfabd654
---
 .../keyboard/KeyboardActionListener.java      |  14 +-
 .../inputmethod/keyboard/PointerTracker.java  |  91 ++++-
 .../keyboard/internal/GestureTracker.java     | 325 ++++++++++++++++++
 .../internal/PointerTrackerQueue.java         |   4 +
 .../android/inputmethod/latin/LatinIME.java   |  37 +-
 .../android/inputmethod/latin/Suggest.java    |  18 +-
 .../inputmethod/latin/WordComposer.java       |  26 +-
 7 files changed, 483 insertions(+), 32 deletions(-)
 create mode 100644 java/src/com/android/inputmethod/keyboard/internal/GestureTracker.java

diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index dc27769ab2..1f3ee7680c 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -16,6 +16,9 @@
 
 package com.android.inputmethod.keyboard;
 
+import com.android.inputmethod.latin.InputPointers;
+import com.android.inputmethod.latin.SuggestedWords;
+
 public interface KeyboardActionListener {
 
     /**
@@ -64,13 +67,18 @@ public interface KeyboardActionListener {
      */
     public void onTextInput(CharSequence text);
 
-    // TODO: Should move this method to some more appropriate interface.
     /**
      * Called when user started batch input.
      */
     public void onStartBatchInput();
 
-    // TODO: Should move this method to some more appropriate interface.
+    /**
+     * Sends the batch input points data to get updated suggestions
+     * @param batchPointers the batch input points representing the user input
+     * @return updated suggestions that reflects the user input
+     */
+    public SuggestedWords onUpdateBatchInput(InputPointers batchPointers);
+
     /**
      * Sends a sequence of characters to the listener as batch input.
      *
@@ -101,6 +109,8 @@ public interface KeyboardActionListener {
         @Override
         public void onStartBatchInput() {}
         @Override
+        public SuggestedWords onUpdateBatchInput(InputPointers batchPointers) { return null; }
+        @Override
         public void onEndBatchInput(CharSequence text) {}
         @Override
         public void onCancelInput() {}
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 1ae0020a49..733d3b09b8 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -22,6 +22,7 @@ import android.view.MotionEvent;
 import android.view.View;
 import android.widget.TextView;
 
+import com.android.inputmethod.keyboard.internal.GestureTracker;
 import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.ResearchLogger;
@@ -161,6 +162,9 @@ public class PointerTracker {
     private static final KeyboardActionListener EMPTY_LISTENER =
             new KeyboardActionListener.Adapter();
 
+    // Gesture tracker singleton instance
+    private static final GestureTracker sGestureTracker = GestureTracker.getInstance();
+
     public static void init(boolean hasDistinctMultitouch,
             boolean needsPhantomSuddenMoveEventHack) {
         if (hasDistinctMultitouch) {
@@ -199,6 +203,7 @@ public class PointerTracker {
         for (final PointerTracker tracker : sTrackers) {
             tracker.mListener = listener;
         }
+        GestureTracker.init(listener);
     }
 
     public static void setKeyDetector(KeyDetector keyDetector) {
@@ -207,6 +212,7 @@ public class PointerTracker {
             // Mark that keyboard layout has been changed.
             tracker.mKeyboardLayoutHasBeenChanged = true;
         }
+        sGestureTracker.setKeyboard(keyDetector.getKeyboard());
     }
 
     public static void dismissAllKeyPreviews() {
@@ -233,6 +239,9 @@ public class PointerTracker {
 
     // Returns true if keyboard has been changed by this callback.
     private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) {
+        if (sGestureTracker.isInGesture()) {
+            return false;
+        }
         final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
         if (DEBUG_LISTENER) {
             Log.d(TAG, "onPress    : " + KeyDetector.printableCode(key)
@@ -286,6 +295,9 @@ public class PointerTracker {
     // Note that we need primaryCode argument because the keyboard may in shifted state and the
     // primaryCode is different from {@link Key#mCode}.
     private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) {
+        if (sGestureTracker.isInGesture()) {
+            return;
+        }
         final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
         if (DEBUG_LISTENER) {
             Log.d(TAG, "onRelease  : " + Keyboard.printableCode(primaryCode)
@@ -386,7 +398,7 @@ public class PointerTracker {
             return;
         }
 
-        if (!key.noKeyPreview()) {
+        if (!key.noKeyPreview() && !sGestureTracker.isInGesture()) {
             mDrawingProxy.showKeyPreview(this);
         }
         updatePressKeyGraphics(key);
@@ -504,8 +516,8 @@ public class PointerTracker {
         }
 
         final PointerTrackerQueue queue = sPointerTrackerQueue;
+        final Key key = getKeyOn(x, y);
         if (queue != null) {
-            final Key key = getKeyOn(x, y);
             if (key != null && key.isModifier()) {
                 // Before processing a down event of modifier key, all pointers already being
                 // tracked should be released.
@@ -514,6 +526,9 @@ public class PointerTracker {
             queue.add(this);
         }
         onDownEventInternal(x, y, eventTime);
+        if (queue != null && queue.size() == 1) {
+            sGestureTracker.onDownEvent(this, x, y, eventTime, key);
+        }
     }
 
     private void onDownEventInternal(int x, int y, long eventTime) {
@@ -554,10 +569,34 @@ public class PointerTracker {
         if (mKeyAlreadyProcessed)
             return;
 
+        if (me != null) {
+            // Add historical points to gesture path.
+            final int pointerIndex = me.findPointerIndex(mPointerId);
+            final int historicalSize = me.getHistorySize();
+            for (int h = 0; h < historicalSize; h++) {
+                final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
+                final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
+                final long historicalTime = me.getHistoricalEventTime(h);
+                sGestureTracker.onMoveEvent(this, historicalX, historicalY, historicalTime,
+                        true /* isHistorical */, null);
+            }
+        }
+
         final int lastX = mLastX;
         final int lastY = mLastY;
         final Key oldKey = mCurrentKey;
         Key key = onMoveKey(x, y);
+
+        // Register move event on gesture tracker.
+        sGestureTracker.onMoveEvent(this, x, y, eventTime, false, key);
+        if (sGestureTracker.isInGesture()) {
+            mIgnoreModifierKey = true;
+            mTimerProxy.cancelLongPressTimer();
+            mIsInSlidingKeyInput = true;
+            mCurrentKey = null;
+            setReleasedKeyGraphics(oldKey);
+        }
+
         if (key != null) {
             if (oldKey == null) {
                 // The pointer has been slid in to the new key, but the finger was not on any keys.
@@ -607,7 +646,7 @@ public class PointerTracker {
                         if (ProductionFlag.IS_EXPERIMENTAL) {
                             ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
                         }
-                        onUpEventInternal();
+                        onUpEventInternal(x, y, eventTime);
                         onDownEventInternal(x, y, eventTime);
                     } else {
                         // HACK: If there are currently multiple touches, register the key even if
@@ -617,7 +656,7 @@ public class PointerTracker {
                         // this hack.
                         if (me != null && me.getPointerCount() > 1
                                 && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
-                            onUpEventInternal();
+                            onUpEventInternal(x, y, eventTime);
                         }
                         mKeyAlreadyProcessed = true;
                         setReleasedKeyGraphics(oldKey);
@@ -647,16 +686,18 @@ public class PointerTracker {
 
         final PointerTrackerQueue queue = sPointerTrackerQueue;
         if (queue != null) {
-            if (mCurrentKey != null && mCurrentKey.isModifier()) {
-                // Before processing an up event of modifier key, all pointers already being
-                // tracked should be released.
-                queue.releaseAllPointersExcept(this, eventTime);
-            } else {
-                queue.releaseAllPointersOlderThan(this, eventTime);
+            if (!sGestureTracker.isInGesture()) {
+                if (mCurrentKey != null && mCurrentKey.isModifier()) {
+                    // Before processing an up event of modifier key, all pointers already being
+                    // tracked should be released.
+                    queue.releaseAllPointersExcept(this, eventTime);
+                } else {
+                    queue.releaseAllPointersOlderThan(this, eventTime);
+                }
             }
             queue.remove(this);
         }
-        onUpEventInternal();
+        onUpEventInternal(x, y, eventTime);
     }
 
     // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
@@ -665,11 +706,11 @@ public class PointerTracker {
     public void onPhantomUpEvent(int x, int y, long eventTime) {
         if (DEBUG_EVENT)
             printTouchEvent("onPhntEvent:", x, y, eventTime);
-        onUpEventInternal();
+        onUpEventInternal(x, y, eventTime);
         mKeyAlreadyProcessed = true;
     }
 
-    private void onUpEventInternal() {
+    private void onUpEventInternal(int x, int y, long eventTime) {
         mTimerProxy.cancelKeyTimers();
         mIsInSlidingKeyInput = false;
         // Release the last pressed key.
@@ -678,6 +719,24 @@ public class PointerTracker {
             mDrawingProxy.dismissMoreKeysPanel();
             mIsShowingMoreKeysPanel = false;
         }
+
+        if (sGestureTracker.isInGesture()) {
+            // Register up event on gesture tracker.
+            sGestureTracker.onUpEvent(this, x, y, eventTime);
+            if (!sPointerTrackerQueue.isAnyInSlidingKeyInput()) {
+                // TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code.
+                sGestureTracker.endBatchInput();
+            }
+            if (mCurrentKey != null) {
+                callListenerOnRelease(mCurrentKey, mCurrentKey.mCode, true);
+            }
+            mCurrentKey = null;
+            return;
+        } else {
+            // TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code.
+            sGestureTracker.endBatchInput();
+        }
+
         if (mKeyAlreadyProcessed)
             return;
         if (mCurrentKey != null && !mCurrentKey.isRepeatable()) {
@@ -689,6 +748,8 @@ public class PointerTracker {
         onLongPressed();
         onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
         mIsShowingMoreKeysPanel = true;
+        // TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code.
+        sGestureTracker.abortBatchInput();
     }
 
     public void onLongPressed() {
@@ -723,7 +784,7 @@ public class PointerTracker {
     }
 
     private void startRepeatKey(Key key) {
-        if (key != null && key.isRepeatable()) {
+        if (key != null && key.isRepeatable() && !sGestureTracker.isInGesture()) {
             onRegisterKey(key);
             mTimerProxy.startKeyRepeatTimer(this);
         }
@@ -753,7 +814,7 @@ public class PointerTracker {
     }
 
     private void startLongPressTimer(Key key) {
-        if (key != null && key.isLongPressEnabled()) {
+        if (key != null && key.isLongPressEnabled() && !sGestureTracker.isInGesture()) {
             mTimerProxy.startLongPressTimer(this);
         }
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTracker.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTracker.java
new file mode 100644
index 0000000000..f989114451
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTracker.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardActionListener;
+import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.latin.InputPointers;
+import com.android.inputmethod.latin.SuggestedWords;
+
+// TODO: Remove this class by consolidating with PointerTracker
+public class GestureTracker {
+    private static final String TAG = GestureTracker.class.getSimpleName();
+    private static final boolean DEBUG_LISTENER = false;
+
+    // TODO: There should be an option to turn on/off the gesture input.
+    private static final boolean GESTURE_ON = true;
+
+    private static final GestureTracker sInstance = new GestureTracker();
+
+    private static final int MIN_RECOGNITION_TIME = 100;
+    private static final int MIN_GESTURE_DURATION = 200;
+
+    private static final float GESTURE_RECOG_SPEED_THRESHOLD = 0.4f;
+    private static final float SQUARED_GESTURE_RECOG_SPEED_THRESHOLD =
+            GESTURE_RECOG_SPEED_THRESHOLD * GESTURE_RECOG_SPEED_THRESHOLD;
+    private static final float GESTURE_RECOG_CURVATURE_THRESHOLD = (float) (Math.PI / 4);
+
+    private boolean mIsAlphabetKeyboard;
+    private boolean mIsPossibleGesture = false;
+    private boolean mInGesture = false;
+
+    private KeyboardActionListener mListener;
+    private SuggestedWords mSuggestions;
+
+    private final SparseArray<GestureStroke> mGestureStrokes = new SparseArray<GestureStroke>();
+
+    private int mLastRecognitionPointSize = 0;
+    private long mLastRecognitionTime = 0;
+
+    public static void init(KeyboardActionListener listner) {
+        sInstance.mListener = listner;
+    }
+
+    public static GestureTracker getInstance() {
+        return sInstance;
+    }
+
+    private GestureTracker() {
+    }
+
+    public void setKeyboard(Keyboard keyboard) {
+        mIsAlphabetKeyboard = keyboard.mId.isAlphabetKeyboard();
+        GestureStroke.setGestureSampleLength(keyboard.mMostCommonKeyWidth / 2,
+                keyboard.mMostCommonKeyHeight / 6);
+    }
+
+    private void startBatchInput() {
+        if (DEBUG_LISTENER) {
+            Log.d(TAG, "onStartBatchInput");
+        }
+        mInGesture = true;
+        mListener.onStartBatchInput();
+        mSuggestions = null;
+    }
+
+    // TODO: The corresponding startBatchInput() is a private method. Reorganize the code.
+    public void endBatchInput() {
+        if (isInGesture() && mSuggestions != null && mSuggestions.size() > 0) {
+            final CharSequence text = mSuggestions.getWord(0);
+            if (DEBUG_LISTENER) {
+                Log.d(TAG, "onEndBatchInput: text=" + text);
+            }
+            mListener.onEndBatchInput(text);
+        }
+        mInGesture = false;
+        clearBatchInputPoints();
+    }
+
+    public void abortBatchInput() {
+        mIsPossibleGesture = false;
+        mInGesture = false;
+    }
+
+    public boolean isInGesture() {
+        return mInGesture;
+    }
+
+    public void onDownEvent(PointerTracker tracker, int x, int y, long eventTime, Key key) {
+        mIsPossibleGesture = false;
+        if (GESTURE_ON && mIsAlphabetKeyboard && key != null && !key.isModifier()) {
+            mIsPossibleGesture = true;
+            addPointToStroke(x, y, 0, tracker.mPointerId, false);
+        }
+    }
+
+    public void onMoveEvent(PointerTracker tracker, int x, int y, long eventTime,
+            boolean isHistorical, Key key) {
+        final int gestureTime = (int)(eventTime - tracker.getDownTime());
+        if (GESTURE_ON && mIsPossibleGesture) {
+            final GestureStroke stroke = addPointToStroke(x, y, gestureTime, tracker.mPointerId,
+                    isHistorical);
+            if (!isInGesture() && stroke.isStartOfAGesture(gestureTime)) {
+                startBatchInput();
+            }
+        }
+
+        if (key != null && isInGesture()) {
+            final InputPointers batchPoints = getIncrementalBatchPoints();
+            if (updateBatchInputRecognitionState(eventTime, batchPoints.getPointerSize())) {
+                if (DEBUG_LISTENER) {
+                    Log.d(TAG, "onUpdateBatchInput: batchPoints=" + batchPoints.getPointerSize());
+                }
+                mSuggestions = mListener.onUpdateBatchInput(batchPoints);
+            }
+        }
+    }
+
+    public void onUpEvent(PointerTracker tracker, int x, int y, long eventTime) {
+        if (isInGesture()) {
+            final InputPointers batchPoints = getAllBatchPoints();
+            if (DEBUG_LISTENER) {
+                Log.d(TAG, "onUpdateBatchInput: batchPoints=" + batchPoints.getPointerSize());
+            }
+            mSuggestions = mListener.onUpdateBatchInput(batchPoints);
+        }
+    }
+
+    private GestureStroke addPointToStroke(int x, int y, int time, int pointerId,
+            boolean isHistorical) {
+        GestureStroke stroke = mGestureStrokes.get(pointerId);
+        if (stroke == null) {
+            stroke = new GestureStroke(pointerId);
+            mGestureStrokes.put(pointerId, stroke);
+        }
+        stroke.addPoint(x, y, time, isHistorical);
+        return stroke;
+    }
+
+    // The working and return object of the following methods, {@link #getIncrementalBatchPoints()}
+    // and {@link #getAllBatchPoints()}.
+    private final InputPointers mAggregatedPointers = new InputPointers();
+
+    private InputPointers getIncrementalBatchPoints() {
+        final InputPointers pointers = mAggregatedPointers;
+        pointers.reset();
+        final int strokeSize = mGestureStrokes.size();
+        for (int index = 0; index < strokeSize; index++) {
+            final GestureStroke stroke = mGestureStrokes.valueAt(index);
+            stroke.appendIncrementalBatchPoints(pointers);
+        }
+        return pointers;
+    }
+
+    private InputPointers getAllBatchPoints() {
+        final InputPointers pointers = mAggregatedPointers;
+        pointers.reset();
+        final int strokeSize = mGestureStrokes.size();
+        for (int index = 0; index < strokeSize; index++) {
+            final GestureStroke stroke = mGestureStrokes.valueAt(index);
+            stroke.appendAllBatchPoints(pointers);
+        }
+        return pointers;
+    }
+
+    private void clearBatchInputPoints() {
+        final int strokeSize = mGestureStrokes.size();
+        for (int index = 0; index < strokeSize; index++) {
+            final GestureStroke stroke = mGestureStrokes.valueAt(index);
+            stroke.reset();
+        }
+        mLastRecognitionPointSize = 0;
+        mLastRecognitionTime = 0;
+    }
+
+    private boolean updateBatchInputRecognitionState(long eventTime, int size) {
+        if (size > mLastRecognitionPointSize
+                && eventTime > mLastRecognitionTime + MIN_RECOGNITION_TIME) {
+            mLastRecognitionPointSize = size;
+            mLastRecognitionTime = eventTime;
+            return true;
+        }
+        return false;
+    }
+
+    private static class GestureStroke {
+        private final int mPointerId;
+        private final InputPointers mInputPointers = new InputPointers();
+        private float mLength;
+        private float mAngle;
+        private int mIncrementalRecognitionPoint;
+        private boolean mHasSharpCorner;
+        private long mLastPointTime;
+        private int mLastPointX;
+        private int mLastPointY;
+
+        private static int sMinGestureLength;
+        private static int sSquaredGestureSampleLength;
+
+        private static final float DOUBLE_PI = (float)(2 * Math.PI);
+
+        public static void setGestureSampleLength(final int minGestureLength,
+                final int sampleLength) {
+            sMinGestureLength = minGestureLength;
+            sSquaredGestureSampleLength = sampleLength * sampleLength;
+        }
+
+        public GestureStroke(int pointerId) {
+            mPointerId = pointerId;
+            reset();
+        }
+
+        public boolean isStartOfAGesture(int downDuration) {
+            return downDuration > MIN_GESTURE_DURATION / 2  && mLength > sMinGestureLength / 2;
+        }
+
+        public void reset() {
+            mLength = 0;
+            mAngle = 0;
+            mIncrementalRecognitionPoint = 0;
+            mHasSharpCorner = false;
+            mLastPointTime = 0;
+            mInputPointers.reset();
+        }
+
+        private void updateLastPoint(final int x, final int y, final int time) {
+            mLastPointTime = time;
+            mLastPointX = x;
+            mLastPointY = y;
+        }
+
+        public void addPoint(final int x, final int y, final int time, final boolean isHistorical) {
+            final int size = mInputPointers.getPointerSize();
+            if (size == 0) {
+                mInputPointers.addPointer(x, y, mPointerId, time);
+                if (!isHistorical) {
+                    updateLastPoint(x, y, time);
+                }
+                return;
+            }
+
+            final int[] xCoords = mInputPointers.getXCoordinates();
+            final int[] yCoords = mInputPointers.getYCoordinates();
+            final int lastX = xCoords[size - 1];
+            final int lastY = yCoords[size - 1];
+            final float dist = squaredDistance(lastX, lastY, x, y);
+            if (dist > sSquaredGestureSampleLength) {
+                mInputPointers.addPointer(x, y, mPointerId, time);
+                mLength += dist;
+                final float angle = angle(lastX, lastY, x, y);
+                if (size > 1) {
+                    float curvature = getAngleDiff(angle, mAngle);
+                    if (curvature > GESTURE_RECOG_CURVATURE_THRESHOLD) {
+                        if (size > mIncrementalRecognitionPoint) {
+                            mIncrementalRecognitionPoint = size;
+                        }
+                        mHasSharpCorner = true;
+                    }
+                    if (!mHasSharpCorner) {
+                        mIncrementalRecognitionPoint = size;
+                    }
+                }
+                mAngle = angle;
+            }
+
+            if (!isHistorical) {
+                final int duration = (int)(time - mLastPointTime);
+                if (mLastPointTime != 0 && duration > 0) {
+                    final int squaredDuration = duration * duration;
+                    final float squaredSpeed =
+                            squaredDistance(mLastPointX, mLastPointY, x, y) / squaredDuration;
+                    if (squaredSpeed < SQUARED_GESTURE_RECOG_SPEED_THRESHOLD) {
+                        mIncrementalRecognitionPoint = size;
+                    }
+                }
+                updateLastPoint(x, y, time);
+            }
+        }
+
+        private float getAngleDiff(float a1, float a2) {
+            final float diff = Math.abs(a1 - a2);
+            if (diff > Math.PI) {
+                return DOUBLE_PI - diff;
+            }
+            return diff;
+        }
+
+        public void appendAllBatchPoints(InputPointers out) {
+            out.append(mInputPointers, 0, mInputPointers.getPointerSize());
+        }
+
+        public void appendIncrementalBatchPoints(InputPointers out) {
+            out.append(mInputPointers, 0, mIncrementalRecognitionPoint);
+        }
+    }
+
+    static float squaredDistance(int p1x, int p1y, int p2x, int p2y) {
+        final float dx = p1x - p2x;
+        final float dy = p1y - p2y;
+        return dx * dx + dy * dy;
+    }
+
+    static float angle(int p1x, int p1y, int p2x, int p2y) {
+        final int dx = p1x - p2x;
+        final int dy = p1y - p2y;
+        if (dx == 0 && dy == 0) return 0;
+        return (float)Math.atan2(dy, dx);
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
index d3bb85d4be..e4a71844a2 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -31,6 +31,10 @@ public class PointerTrackerQueue {
     // TODO: Use ring buffer instead of {@link LinkedList}.
     private final LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
 
+    public int size() {
+        return mQueue.size();
+    }
+
     public synchronized void add(PointerTracker tracker) {
         mQueue.add(tracker);
     }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index f27d32150b..518bcd5ce5 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1268,13 +1268,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
                 if (SPACE_STATE_PHANTOM == spaceState) {
                     commitTyped(LastComposedWord.NOT_A_SEPARATOR);
                 }
+                final int keyX, keyY;
                 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
                 if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
-                    handleCharacter(primaryCode, x, y, spaceState);
+                    keyX = x;
+                    keyY = y;
                 } else {
-                    handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE,
-                            spaceState);
+                    keyX = NOT_A_TOUCH_COORDINATE;
+                    keyY = NOT_A_TOUCH_COORDINATE;
                 }
+                handleCharacter(primaryCode, keyX, keyY, spaceState);
             }
             mExpectingUpdateSelection = true;
             mShouldSwitchToLastSubtype = true;
@@ -1320,10 +1323,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
             mSpaceState = SPACE_STATE_PHANTOM;
         }
         mConnection.endBatchEdit();
+        // TODO: Should handle TextUtils.CAP_MODE_CHARACTER.
+        mWordComposer.setAutoCapitalized(
+                getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
+    }
+
+    @Override
+    public SuggestedWords onUpdateBatchInput(InputPointers batchPointers) {
+        mWordComposer.setBatchInputPointers(batchPointers);
+        return updateSuggestionsOrPredictions();
     }
 
     @Override
     public void onEndBatchInput(CharSequence text) {
+        mWordComposer.setBatchInputWord(text);
         mConnection.beginBatchEdit();
         if (SPACE_STATE_PHANTOM == mSpaceState) {
             sendKeyCodePoint(Keyboard.CODE_SPACE);
@@ -1669,7 +1682,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
     }
 
     // TODO: rename this method to updateSuggestionStrip or simply updateSuggestions
-    private void updateSuggestionsOrPredictions() {
+    private SuggestedWords updateSuggestionsOrPredictions() {
         mHandler.cancelUpdateSuggestionStrip();
 
         // Check if we have a suggestion engine attached.
@@ -1679,13 +1692,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
                         + "requested!");
                 mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
             }
-            return;
+            return null;
         }
 
         final String typedWord = mWordComposer.getTypedWord();
         if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) {
             setPunctuationSuggestions();
-            return;
+            return null;
         }
 
         // Get the word on which we should search the bigrams. If we are composing a word, it's
@@ -1701,6 +1714,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
         suggestedWords = maybeRetrieveOlderSuggestions(typedWord, suggestedWords);
 
         showSuggestions(suggestedWords, typedWord);
+        return suggestedWords;
     }
 
     private SuggestedWords maybeRetrieveOlderSuggestions(final CharSequence typedWord,
@@ -1761,9 +1775,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
         if (mHandler.hasPendingUpdateSuggestions()) {
             updateSuggestionsOrPredictions();
         }
-        final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
+        final CharSequence typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
+        final String typedWord = mWordComposer.getTypedWord();
+        final CharSequence autoCorrection = (typedAutoCorrection != null)
+                ? typedAutoCorrection : typedWord;
         if (autoCorrection != null) {
-            final String typedWord = mWordComposer.getTypedWord();
             if (TextUtils.isEmpty(typedWord)) {
                 throw new RuntimeException("We have an auto-correction but the typed word "
                         + "is empty? Impossible! I must commit suicide.");
@@ -1808,7 +1824,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
         }
 
         mConnection.beginBatchEdit();
-        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
+        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
+                // In the batch input mode, a manually picked suggested word should just replace
+                // the current batch input text and there is no need for a phantom space.
+                && !mWordComposer.isBatchMode()) {
             int firstChar = Character.codePointAt(suggestion, 0);
             if ((!mCurrentSettings.isWeakSpaceStripper(firstChar))
                     && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) {
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 31566bf138..598ef1de73 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -300,11 +300,27 @@ public class Suggest {
 
         final ArrayList<SuggestedWordInfo> suggestionsContainer =
                 new ArrayList<SuggestedWordInfo>(suggestionsSet);
+        final int suggestionsCount = suggestionsContainer.size();
+        final boolean isFirstCharCapitalized = wordComposer.isAutoCapitalized();
+        // TODO: Handle the manual temporary shifted mode.
+        // TODO: Should handle TextUtils.CAP_MODE_CHARACTER.
+        final boolean isAllUpperCase = false;
+        if (isFirstCharCapitalized || isAllUpperCase) {
+            for (int i = 0; i < suggestionsCount; ++i) {
+                final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+                final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
+                        wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized,
+                        0 /* trailingSingleQuotesCount */);
+                suggestionsContainer.set(i, transformedWordInfo);
+            }
+        }
 
         SuggestedWordInfo.removeDups(suggestionsContainer);
+        // In the batch input mode, the most relevant suggested word should act as a "typed word"
+        // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
         return new SuggestedWords(suggestionsContainer,
                 true /* typedWordValid */,
-                true /* willAutoCorrect */,
+                false /* willAutoCorrect */,
                 false /* isPunctuationSuggestions */,
                 false /* isObsoleteSuggestions */,
                 false /* isPrediction */);
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 25e29008e0..ca9dbaf054 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -130,8 +130,13 @@ public class WordComposer {
         if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
             mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE
                     ? Character.toLowerCase(primaryCode) : primaryCode;
-            // TODO: Set correct pointer id and time
-            mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0);
+            // In the batch input mode, the {@code mInputPointers} holds batch input points and
+            // shouldn't be overridden by the "typed key" coordinates
+            // (See {@link #setBatchInputWord}).
+            if (!mIsBatchMode) {
+                // TODO: Set correct pointer id and time
+                mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0);
+            }
         }
         mIsFirstCharCapitalized = isFirstCharCapitalized(
                 newIndex, primaryCode, mIsFirstCharCapitalized);
@@ -144,10 +149,21 @@ public class WordComposer {
         mAutoCorrection = null;
     }
 
-    // TODO: We may want to have appendBatchInputPointers() as well.
     public void setBatchInputPointers(InputPointers batchPointers) {
-        mInputPointers.copy(batchPointers);
+        mInputPointers.set(batchPointers);
+        mIsBatchMode = true;
+    }
+
+    public void setBatchInputWord(CharSequence word) {
+        reset();
         mIsBatchMode = true;
+        final int length = word.length();
+        for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
+            final int codePoint = Character.codePointAt(word, i);
+            // We don't want to override the batch input points that are held in mInputPointers
+            // (See {@link #add(int,int,int)}).
+            add(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE);
+        }
     }
 
     /**
@@ -161,7 +177,7 @@ public class WordComposer {
             add(codePoint, x, y);
             return;
         }
-        add(codePoint, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
+        add(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE);
     }
 
     /**
-- 
GitLab