From 89bd776cf68150202d774d62cc1c88664aea5e9f Mon Sep 17 00:00:00 2001
From: Jean Chalard <jchalard@google.com>
Date: Wed, 20 Apr 2011 11:50:05 +0900
Subject: [PATCH] Use user-history bigrams when no input if available.

This also fixes a small inconsistency upon clicking on whitespace
twice in a row.
Also add some unit tests for an introduced and an existing method.

Change-Id: I1be2fb53c9624f4d0f5299009632cb4384fdfc15
---
 .../inputmethod/latin/BinaryDictionary.java   |  5 ++
 .../inputmethod/latin/EditingUtils.java       | 63 +++++++++++---
 .../android/inputmethod/latin/LatinIME.java   | 86 +++++++++++++++++--
 .../android/inputmethod/latin/Suggest.java    | 47 ++++++----
 .../latin/UserBigramDictionary.java           |  4 +
 .../android/inputmethod/latin/UtilsTests.java | 67 +++++++++++++++
 6 files changed, 235 insertions(+), 37 deletions(-)
 create mode 100644 tests/src/com/android/inputmethod/latin/UtilsTests.java

diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 58e9099a91..7e63aacdf0 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -197,6 +197,11 @@ public class BinaryDictionary extends Dictionary {
         Arrays.fill(mBigramScores, 0);
 
         int codesSize = codes.size();
+        if (codesSize <= 0) {
+            // Do not return bigrams from BinaryDictionary when nothing was typed.
+            // Only use user-history bigrams (or whatever other bigram dictionaries decide).
+            return;
+        }
         Arrays.fill(mInputCodes, -1);
         int[] alternatives = codes.getCodesAt(0);
         System.arraycopy(alternatives, 0, mInputCodes, 0,
diff --git a/java/src/com/android/inputmethod/latin/EditingUtils.java b/java/src/com/android/inputmethod/latin/EditingUtils.java
index 80830c000e..ea281f5b85 100644
--- a/java/src/com/android/inputmethod/latin/EditingUtils.java
+++ b/java/src/com/android/inputmethod/latin/EditingUtils.java
@@ -161,23 +161,62 @@ public class EditingUtils {
 
     private static final Pattern spaceRegex = Pattern.compile("\\s+");
 
+
     public static CharSequence getPreviousWord(InputConnection connection,
             String sentenceSeperators) {
         //TODO: Should fix this. This could be slow!
         CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
-        if (prev == null) {
-            return null;
-        }
+        return getPreviousWord(prev, sentenceSeperators);
+    }
+
+    // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
+    // Also, it won't return words that end in a separator.
+    // Example :
+    // "abc def|" -> abc
+    // "abc def |" -> abc
+    // "abc def. |" -> abc
+    // "abc def . |" -> def
+    // "abc|" -> null
+    // "abc |" -> null
+    // "abc. def|" -> null
+    public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
+        if (prev == null) return null;
         String[] w = spaceRegex.split(prev);
-        if (w.length >= 2 && w[w.length-2].length() > 0) {
-            char lastChar = w[w.length-2].charAt(w[w.length-2].length() -1);
-            if (sentenceSeperators.contains(String.valueOf(lastChar))) {
-                return null;
-            }
-            return w[w.length-2];
-        } else {
-            return null;
-        }
+
+        // If we can't find two words, or we found an empty word, return null.
+        if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
+
+        // If ends in a separator, return null
+        char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
+        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+        return w[w.length - 2];
+    }
+
+    public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
+        final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+        return getThisWord(prev, sentenceSeperators);
+    }
+
+    // Get the word immediately before the cursor, even if there is whitespace between it and
+    // the cursor - but not if there is punctuation.
+    // Example :
+    // "abc def|" -> def
+    // "abc def |" -> def
+    // "abc def. |" -> null
+    // "abc def . |" -> null
+    public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
+        if (prev == null) return null;
+        String[] w = spaceRegex.split(prev);
+
+        // No word : return null
+        if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
+
+        // If ends in a separator, return null
+        char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
+        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+        return w[w.length - 1];
     }
 
     public static class SelectedWord {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 0ae675b3bc..8f44ec743b 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -266,6 +266,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
         private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 4;
         private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 5;
         private static final int MSG_SPACE_TYPED = 6;
+        private static final int MSG_SET_BIGRAM_SUGGESTIONS = 7;
 
         @Override
         public void handleMessage(Message msg) {
@@ -281,6 +282,9 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
             case MSG_UPDATE_SHIFT_STATE:
                 switcher.updateShiftState();
                 break;
+            case MSG_SET_BIGRAM_SUGGESTIONS:
+                updateBigramSuggestions();
+                break;
             case MSG_VOICE_RESULTS:
                 mVoiceProxy.handleVoiceResults(preferCapitalization()
                         || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked()));
@@ -333,6 +337,15 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
             removeMessages(MSG_UPDATE_SHIFT_STATE);
         }
 
+        public void postSetBigramSuggestions() {
+            removeMessages(MSG_SET_BIGRAM_SUGGESTIONS);
+            sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_SUGGESTIONS), DELAY_UPDATE_SUGGESTIONS);
+        }
+
+        public void cancelSetBigramSuggestions() {
+            removeMessages(MSG_SET_BIGRAM_SUGGESTIONS);
+        }
+
         public void updateVoiceResults() {
             sendMessage(obtainMessage(MSG_VOICE_RESULTS));
         }
@@ -548,7 +561,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
         LatinKeyboardView inputView = switcher.getInputView();
 
-        if(DEBUG) {
+        if (DEBUG) {
             Log.d(TAG, "onStartInputView: " + inputView);
         }
         // In landscape mode, this method gets called without the input view being created.
@@ -754,7 +767,12 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
             }
             mComposing.setLength(0);
             mHasValidSuggestions = false;
-            mHandler.postUpdateSuggestions();
+            if (isCursorTouchingWord()) {
+                mHandler.cancelSetBigramSuggestions();
+                mHandler.postUpdateSuggestions();
+            } else {
+                setPunctuationSuggestions();
+            }
             TextEntryState.reset();
             InputConnection ic = getCurrentInputConnection();
             if (ic != null) {
@@ -784,14 +802,20 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
                                 || TextEntryState.isRecorrecting())
                                 && (newSelStart < newSelEnd - 1 || !mHasValidSuggestions)) {
                     if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) {
+                        mHandler.cancelSetBigramSuggestions();
                         mHandler.postUpdateOldSuggestions();
                     } else {
                         abortRecorrection(false);
-                        // Show the punctuation suggestions list if the current one is not
-                        // and if not showing "Touch again to save".
-                        if (mCandidateView != null && !isShowingPunctuationList()
+                        // If showing the "touch again to save" hint, do not replace it. Else,
+                        // show the bigrams if we are at the end of the text, punctuation otherwise.
+                        if (mCandidateView != null
                                 && !mCandidateView.isShowingAddToDictionaryHint()) {
-                            setPunctuationSuggestions();
+                            InputConnection ic = getCurrentInputConnection();
+                            if (null == ic || !TextUtils.isEmpty(ic.getTextAfterCursor(1, 0))) {
+                                if (!isShowingPunctuationList()) setPunctuationSuggestions();
+                            } else {
+                                mHandler.postSetBigramSuggestions();
+                            }
                         }
                     }
                 }
@@ -1231,7 +1255,14 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
                 if (mComposing.length() == 0) {
                     mHasValidSuggestions = false;
                 }
-                mHandler.postUpdateSuggestions();
+                if (1 == length) {
+                    // 1 == length means we are about to erase the last character of the word,
+                    // so we can show bigrams.
+                    mHandler.postSetBigramSuggestions();
+                } else {
+                    // length > 1, so we still have letters to deduce a suggestion from.
+                    mHandler.postUpdateSuggestions();
+                }
             } else {
                 ic.deleteSurroundingText(1, 0);
             }
@@ -1367,6 +1398,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
 
         // Should dismiss the "Touch again to save" message when handling separator
         if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
+            mHandler.cancelSetBigramSuggestions();
             mHandler.postUpdateSuggestions();
         }
 
@@ -1406,6 +1438,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
         }
 
         TextEntryState.typedCharacter((char) primaryCode, true, x, y);
+
         if (TextEntryState.isPunctuationAfterAccepted() && primaryCode != Keyboard.CODE_ENTER) {
             swapPunctuationAndSpace();
         } else if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) {
@@ -1416,10 +1449,20 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
             TextEntryState.backToAcceptedDefault(typedWord);
             if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) {
                 InputConnectionCompatUtils.commitCorrection(
-                        ic,  mLastSelectionEnd - typedWord.length(), typedWord, mBestWord);
+                        ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord);
                 if (mCandidateView != null)
                     mCandidateView.onAutoCorrectionInverted(mBestWord);
             }
+        }
+        if (Keyboard.CODE_SPACE == primaryCode) {
+            if (!isCursorTouchingWord()) {
+                mHandler.cancelUpdateSuggestions();
+                mHandler.cancelUpdateOldSuggestions();
+                mHandler.postSetBigramSuggestions();
+            }
+        } else {
+            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
+            // already displayed or not, so it's okay.
             setPunctuationSuggestions();
         }
         mKeyboardSwitcher.updateShiftState();
@@ -1654,6 +1697,11 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
             }
             return;
         }
+        if (!mHasValidSuggestions) {
+            // If we are not composing a word, then it was a suggestion inferred from
+            // context - no user input. We should reset the word composer.
+            mWord.reset();
+        }
         mJustAccepted = true;
         pickSuggestion(suggestion);
         // Add the word to the auto dictionary if it's not a known word
@@ -1692,7 +1740,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
             // TextEntryState.State.PICKED_SUGGESTION state.
             TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true,
                     WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
-            setPunctuationSuggestions();
+            // From there on onUpdateSelection() will fire so suggestions will be updated
         } else if (!showingAddToDictionaryHint) {
             // If we're not showing the "Touch again to save", then show corrections again.
             // In case the cursor position doesn't change, make sure we show the suggestions again.
@@ -1807,6 +1855,25 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
         }
     }
 
+    private static final WordComposer sEmptyWordComposer = new WordComposer();
+    private void updateBigramSuggestions() {
+        if (mSuggest == null || !isSuggestionsRequested())
+            return;
+
+        final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
+                mWordSeparators);
+        SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
+                mKeyboardSwitcher.getInputView(), sEmptyWordComposer, prevWord);
+
+        if (builder.size() > 0) {
+            // Explicitly supply an empty typed word (the no-second-arg version of
+            // showSuggestions will retrieve the word near the cursor, we don't want that here)
+            showSuggestions(builder.build(), "");
+        } else {
+            if (!isShowingPunctuationList()) setPunctuationSuggestions();
+        }
+    }
+
     private void setPunctuationSuggestions() {
         setSuggestions(mSuggestPuncList);
         setCandidatesViewShown(isCandidateStripVisible());
@@ -1907,6 +1974,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
                 ic.setComposingText(mComposing, 1);
                 TextEntryState.backspace();
             }
+            mHandler.cancelSetBigramSuggestions();
             mHandler.postUpdateSuggestions();
         } else {
             sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index f372062233..15743ee2d8 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -265,6 +265,16 @@ public class Suggest implements Dictionary.WordCallback {
         return sb;
     }
 
+    protected void addBigramToSuggestions(CharSequence bigram) {
+        final int poolSize = mStringPool.size();
+        final StringBuilder sb = poolSize > 0 ?
+                (StringBuilder) mStringPool.remove(poolSize - 1)
+                        : new StringBuilder(getApproxMaxWordLength());
+        sb.setLength(0);
+        sb.append(bigram);
+        mSuggestions.add(sb);
+    }
+
     // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
     public SuggestedWords.Builder getSuggestedWordBuilder(View view, WordComposer wordComposer,
             CharSequence prevWordForBigram) {
@@ -286,7 +296,7 @@ public class Suggest implements Dictionary.WordCallback {
         }
         mTypedWord = typedWord;
 
-        if (wordComposer.size() == 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM
+        if (wordComposer.size() <= 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM
                 || mCorrectionMode == CORRECTION_BASIC)) {
             // At first character typed, search only the bigrams
             Arrays.fill(mBigramScores, 0);
@@ -300,21 +310,26 @@ public class Suggest implements Dictionary.WordCallback {
                 for (final Dictionary dictionary : mBigramDictionaries.values()) {
                     dictionary.getBigrams(wordComposer, prevWordForBigram, this);
                 }
-                char currentChar = wordComposer.getTypedWord().charAt(0);
-                char currentCharUpper = Character.toUpperCase(currentChar);
-                int count = 0;
-                int bigramSuggestionSize = mBigramSuggestions.size();
-                for (int i = 0; i < bigramSuggestionSize; i++) {
-                    if (mBigramSuggestions.get(i).charAt(0) == currentChar
-                            || mBigramSuggestions.get(i).charAt(0) == currentCharUpper) {
-                        int poolSize = mStringPool.size();
-                        StringBuilder sb = poolSize > 0 ?
-                                (StringBuilder) mStringPool.remove(poolSize - 1)
-                                : new StringBuilder(getApproxMaxWordLength());
-                        sb.setLength(0);
-                        sb.append(mBigramSuggestions.get(i));
-                        mSuggestions.add(count++, sb);
-                        if (count > mPrefMaxSuggestions) break;
+                if (TextUtils.isEmpty(typedWord)) {
+                    // Nothing entered: return all bigrams for the previous word
+                    int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
+                    for (int i = 0; i < insertCount; ++i) {
+                        addBigramToSuggestions(mBigramSuggestions.get(i));
+                    }
+                } else {
+                    // Word entered: return only bigrams that match the first char of the typed word
+                    final char currentChar = typedWord.charAt(0);
+                    final char currentCharUpper = Character.toUpperCase(currentChar);
+                    int count = 0;
+                    final int bigramSuggestionSize = mBigramSuggestions.size();
+                    for (int i = 0; i < bigramSuggestionSize; i++) {
+                        final CharSequence bigramSuggestion = mBigramSuggestions.get(i);
+                        final char bigramSuggestionFirstChar = bigramSuggestion.charAt(0);
+                        if (bigramSuggestionFirstChar == currentChar
+                                || bigramSuggestionFirstChar == currentCharUpper) {
+                            addBigramToSuggestions(bigramSuggestion);
+                            if (++count > mPrefMaxSuggestions) break;
+                        }
                     }
                 }
             }
diff --git a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java
index bb6642cd9b..a32a6461af 100644
--- a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java
@@ -162,6 +162,10 @@ public class UserBigramDictionary extends ExpandableDictionary {
         if (mIme != null && mIme.getCurrentWord().isAutoCapitalized()) {
             word2 = Character.toLowerCase(word2.charAt(0)) + word2.substring(1);
         }
+        // Do not insert a word as a bigram of itself
+        if (word1.equals(word2)) {
+            return 0;
+        }
 
         int freq = super.addBigram(word1, word2, FREQUENCY_FOR_TYPED);
         if (freq > FREQUENCY_MAX) freq = FREQUENCY_MAX;
diff --git a/tests/src/com/android/inputmethod/latin/UtilsTests.java b/tests/src/com/android/inputmethod/latin/UtilsTests.java
new file mode 100644
index 0000000000..5c0b03a0a6
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/UtilsTests.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010,2011 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 android.test.AndroidTestCase;
+
+import com.android.inputmethod.latin.tests.R;
+
+public class UtilsTests extends AndroidTestCase {
+
+    // The following is meant to be a reasonable default for
+    // the "word_separators" resource.
+    private static final String sSeparators = ".,:;!?-";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+    }
+
+    /************************** Tests ************************/
+
+    /**
+     * Test for getting previous word (for bigram suggestions)
+     */
+    public void testGetPreviousWord() {
+        // If one of the following cases breaks, the bigram suggestions won't work.
+        assertEquals(EditingUtils.getPreviousWord("abc def", sSeparators), "abc");
+        assertNull(EditingUtils.getPreviousWord("abc", sSeparators));
+        assertNull(EditingUtils.getPreviousWord("abc. def", sSeparators));
+
+        // The following tests reflect the current behavior of the function
+        // EditingUtils#getPreviousWord.
+        // TODO: However at this time, the code does never go
+        // into such a path, so it should be safe to change the behavior of
+        // this function if needed - especially since it does not seem very
+        // logical. These tests are just there to catch any unintentional
+        // changes in the behavior of the EditingUtils#getPreviousWord method.
+        assertEquals(EditingUtils.getPreviousWord("abc def ", sSeparators), "abc");
+        assertEquals(EditingUtils.getPreviousWord("abc def.", sSeparators), "abc");
+        assertEquals(EditingUtils.getPreviousWord("abc def .", sSeparators), "def");
+        assertNull(EditingUtils.getPreviousWord("abc ", sSeparators));
+    }
+
+    /**
+     * Test for getting the word before the cursor (for bigram)
+     */
+    public void testGetThisWord() {
+        assertEquals(EditingUtils.getThisWord("abc def", sSeparators), "def");
+        assertEquals(EditingUtils.getThisWord("abc def ", sSeparators), "def");
+        assertNull(EditingUtils.getThisWord("abc def.", sSeparators));
+        assertNull(EditingUtils.getThisWord("abc def .", sSeparators));
+    }
+}
-- 
GitLab