From 0e9ee4d3bf75459560670ca5c28ff4c4f7c346fb Mon Sep 17 00:00:00 2001
From: Jean Chalard <jchalard@google.com>
Date: Wed, 10 Apr 2013 18:30:11 +0900
Subject: [PATCH] If there are no suggestion span, recompute suggestions.

Bug: 8084810
Change-Id: I1743c09c43ca6835bb2f607684b037bf17d36335
---
 .../android/inputmethod/latin/LatinIME.java   | 71 +++++++++++++++----
 .../inputmethod/latin/SuggestedWords.java     | 17 +++++
 .../inputmethod/latin/WordComposer.java       | 16 ++++-
 .../latin/SuggestedWordsTests.java            | 60 ++++++++++++++++
 4 files changed, 148 insertions(+), 16 deletions(-)
 create mode 100644 tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java

diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 47d51c5866..da1232f5e5 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1466,7 +1466,13 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
                                 "", mWordComposer.getTypedWord(), " ", mWordComposer);
                     }
                 }
-                commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+                if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+                    // If we are in the middle of a recorrection, we need to commit the recorrection
+                    // first so that we can insert the character at the current cursor position.
+                    resetEntireInputState(mLastSelectionStart);
+                } else {
+                    commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+                }
             }
             final int keyX, keyY;
             final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
@@ -1522,8 +1528,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
             }
             final int wordComposerSize = mWordComposer.size();
             // Since isComposingWord() is true, the size is at least 1.
-            final int lastChar = mWordComposer.getCodeAt(wordComposerSize - 1);
-            if (wordComposerSize <= 1) {
+            final int lastChar = mWordComposer.getCodeBeforeCursor();
+            if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+                // If we are in the middle of a recorrection, we need to commit the recorrection
+                // first so that we can insert the batch input at the current cursor position.
+                resetEntireInputState(mLastSelectionStart);
+            } else if (wordComposerSize <= 1) {
                 // We auto-correct the previous (typed, not gestured) string iff it's one character
                 // long. The reason for this is, even in the middle of gesture typing, you'll still
                 // tap one-letter words and you want them auto-corrected (typically, "i" in English
@@ -1734,8 +1744,11 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
         // during key repeat.
         mHandler.postUpdateShiftState();
 
-        if (mWordComposer.isComposingWord() && !mWordComposer.isCursorAtEndOfComposingWord()) {
+        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            // If we are in the middle of a recorrection, we need to commit the recorrection
+            // first so that we can remove the character at the current cursor position.
             resetEntireInputState(mLastSelectionStart);
+            // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
         }
         if (mWordComposer.isComposingWord()) {
             final int length = mWordComposer.size();
@@ -1870,7 +1883,9 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
             promotePhantomSpace();
         }
 
-        if (mWordComposer.isComposingWord() && !mWordComposer.isCursorAtEndOfComposingWord()) {
+        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            // If we are in the middle of a recorrection, we need to commit the recorrection
+            // first so that we can insert the character at the current cursor position.
             resetEntireInputState(mLastSelectionStart);
             isComposingWord = false;
         }
@@ -1935,7 +1950,11 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
             ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
         }
         boolean didAutoCorrect = false;
-        // Handle separator
+        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            // If we are in the middle of a recorrection, we need to commit the recorrection
+            // first so that we can insert the separator at the current cursor position.
+            resetEntireInputState(mLastSelectionStart);
+        }
         if (mWordComposer.isComposingWord()) {
             if (mSettings.getCurrent().mCorrectionEnabled) {
                 // TODO: maybe cache Strings in an <String> sparse array or something
@@ -2357,9 +2376,9 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
         final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(),
                 0 /* additionalPrecedingWordsCount */);
         final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
+        final String typedWord = range.mWord.toString();
         if (range.mWord instanceof SpannableString) {
             final SpannableString spannableString = (SpannableString)range.mWord;
-            final String typedWord = spannableString.toString();
             int i = 0;
             for (Object object : spannableString.getSpans(0, spannableString.length(),
                     SuggestionSpan.class)) {
@@ -2374,18 +2393,42 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
                 }
             }
         }
-        mWordComposer.setComposingWord(range.mWord, mKeyboardSwitcher.getKeyboard());
+        mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
         mWordComposer.setCursorPositionWithinWord(range.mCharsBefore);
         mConnection.setComposingRegion(mLastSelectionStart - range.mCharsBefore,
                 mLastSelectionEnd + range.mCharsAfter);
+        final SuggestedWords suggestedWords;
         if (suggestions.isEmpty()) {
-            suggestions.add(new SuggestedWordInfo(range.mWord.toString(), 1,
-                    SuggestedWordInfo.KIND_TYPED, Dictionary.TYPE_RESUMED));
+            // We come here if there weren't any suggestion spans on this word. We will try to
+            // compute suggestions for it instead.
+            final SuggestedWords suggestedWordsIncludingTypedWord =
+                    getSuggestedWords(Suggest.SESSION_TYPING);
+            if (suggestedWordsIncludingTypedWord.size() > 1) {
+                // We were able to compute new suggestions for this word.
+                // Remove the typed word, since we don't want to display it in this case.
+                // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false.
+                suggestedWords =
+                        suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord();
+            } else {
+                // No saved suggestions, and we were unable to compute any good one either.
+                // Rather than displaying an empty suggestion strip, we'll display the original
+                // word alone in the middle.
+                // Since there is only one word, willAutoCorrect is false.
+                suggestedWords = suggestedWordsIncludingTypedWord;
+            }
+        } else {
+            // We found suggestion spans in the word. We'll create the SuggestedWords out of
+            // them, and make willAutoCorrect false.
+            suggestedWords = new SuggestedWords(suggestions,
+                    true /* typedWordValid */, false /* willAutoCorrect */,
+                    false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
+                    false /* isPrediction */);
         }
-        showSuggestionStrip(new SuggestedWords(suggestions,
-                true /* typedWordValid */, false /* willAutoCorrect */,
-                false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
-                false /* isPrediction */), range.mWord.toString());
+
+        // Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
+        // We never want to auto-correct on a resumed suggestion. Please refer to the three
+        // places above where suggestedWords is affected.
+        showSuggestionStrip(suggestedWords, typedWord);
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 158cc11557..616e1911bf 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -195,4 +195,21 @@ public final class SuggestedWords {
             }
         }
     }
+
+    // SuggestedWords is an immutable object, as much as possible. We must not just remove
+    // words from the member ArrayList as some other parties may expect the object to never change.
+    public SuggestedWords getSuggestedWordsExcludingTypedWord() {
+        final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList();
+        for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) {
+            final SuggestedWordInfo info = mSuggestedWordInfoList.get(i);
+            if (SuggestedWordInfo.KIND_TYPED != info.mKind) {
+                newSuggestions.add(info);
+            }
+        }
+        // We should never autocorrect, so we say the typed word is valid. Also, in this case,
+        // no auto-correction should take place hence willAutoCorrect = false.
+        return new SuggestedWords(newSuggestions, true /* typedWordValid */,
+                false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions,
+                mIsPrediction);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 098e8ac7bd..51bd901fb1 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -27,6 +27,7 @@ import java.util.Arrays;
  */
 public final class WordComposer {
     private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
+    private static final boolean DBG = LatinImeLogger.sDBG;
 
     public static final int CAPS_MODE_OFF = 0;
     // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
@@ -132,6 +133,13 @@ public final class WordComposer {
         return mPrimaryKeyCodes[index];
     }
 
+    public int getCodeBeforeCursor() {
+        if (mCursorPositionWithinWord < 1 || mCursorPositionWithinWord > mPrimaryKeyCodes.length) {
+            return Constants.NOT_A_CODE;
+        }
+        return mPrimaryKeyCodes[mCursorPositionWithinWord - 1];
+    }
+
     public InputPointers getInputPointers() {
         return mInputPointers;
     }
@@ -177,8 +185,12 @@ public final class WordComposer {
         mCursorPositionWithinWord = posWithinWord;
     }
 
-    public boolean isCursorAtEndOfComposingWord() {
-        return mCursorPositionWithinWord == mCodePointSize;
+    public boolean isCursorFrontOrMiddleOfComposingWord() {
+        if (DBG && mCursorPositionWithinWord > mCodePointSize) {
+            throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
+                    + "in a word of size " + mCodePointSize);
+        }
+        return mCursorPositionWithinWord != mCodePointSize;
     }
 
     public void setBatchInputPointers(final InputPointers batchPointers) {
diff --git a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
new file mode 100644
index 0000000000..9162522924
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 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.latin;
+
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+@SmallTest
+public class SuggestedWordsTests extends AndroidTestCase {
+    public void testGetSuggestedWordsExcludingTypedWord() {
+        final String TYPED_WORD = "typed";
+        final int TYPED_WORD_FREQ = 5;
+        final int NUMBER_OF_ADDED_SUGGESTIONS = 5;
+        final ArrayList<SuggestedWordInfo> list = CollectionUtils.newArrayList();
+        list.add(new SuggestedWordInfo(TYPED_WORD, TYPED_WORD_FREQ,
+                SuggestedWordInfo.KIND_TYPED, ""));
+        for (int i = 0; i < NUMBER_OF_ADDED_SUGGESTIONS; ++i) {
+            list.add(new SuggestedWordInfo("" + i, 1, SuggestedWordInfo.KIND_CORRECTION, ""));
+        }
+
+        final SuggestedWords words = new SuggestedWords(
+                list,
+                false /* typedWordValid */,
+                false /* willAutoCorrect */,
+                false /* isPunctuationSuggestions */,
+                false /* isObsoleteSuggestions */,
+                false /* isPrediction*/);
+        assertEquals(NUMBER_OF_ADDED_SUGGESTIONS + 1, words.size());
+        assertEquals("typed", words.getWord(0));
+        assertEquals(SuggestedWordInfo.KIND_TYPED, words.getInfo(0).mKind);
+        assertEquals("0", words.getWord(1));
+        assertEquals(SuggestedWordInfo.KIND_CORRECTION, words.getInfo(1).mKind);
+        assertEquals("4", words.getWord(5));
+        assertEquals(SuggestedWordInfo.KIND_CORRECTION, words.getInfo(5).mKind);
+
+        final SuggestedWords wordsWithoutTyped = words.getSuggestedWordsExcludingTypedWord();
+        assertEquals(words.size() - 1, wordsWithoutTyped.size());
+        assertEquals("0", wordsWithoutTyped.getWord(0));
+        assertEquals(SuggestedWordInfo.KIND_CORRECTION, wordsWithoutTyped.getInfo(0).mKind);
+    }
+}
-- 
GitLab