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