diff options
6 files changed, 239 insertions, 50 deletions
diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java index c33c01552..27db9b8e8 100644 --- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java +++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java @@ -23,10 +23,11 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.style.SuggestionSpan; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.define.DebugFlags; import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver; +import com.android.inputmethod.latin.define.DebugFlags; import java.lang.reflect.Field; import java.util.ArrayList; @@ -51,12 +52,14 @@ public final class SuggestionSpanUtils { // This utility class is not publicly instantiable. } + @UsedForTesting public static CharSequence getTextWithAutoCorrectionIndicatorUnderline( final Context context, final String text) { if (TextUtils.isEmpty(text) || OBJ_FLAG_AUTO_CORRECTION == null) { return text; } final Spannable spannable = new SpannableString(text); + // TODO: Set locale if it is feasible. final SuggestionSpan suggestionSpan = new SuggestionSpan(context, null /* locale */, new String[] {} /* suggestions */, OBJ_FLAG_AUTO_CORRECTION, SuggestionSpanPickedNotificationReceiver.class); @@ -65,6 +68,7 @@ public final class SuggestionSpanUtils { return spannable; } + @UsedForTesting public static CharSequence getTextWithSuggestionSpan(final Context context, final String pickedWord, final SuggestedWords suggestedWords) { if (TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() @@ -86,6 +90,7 @@ public final class SuggestionSpanUtils { suggestionsList.add(word.toString()); } } + // TODO: Set locale if it is feasible. final SuggestionSpan suggestionSpan = new SuggestionSpan(context, null /* locale */, suggestionsList.toArray(new String[suggestionsList.size()]), 0 /* flags */, SuggestionSpanPickedNotificationReceiver.class); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 86c265f86..10dea749d 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -42,6 +42,8 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import javax.annotation.Nonnull; + /** * Implements a static, compacted, binary dictionary of standard words. */ @@ -495,8 +497,7 @@ public final class BinaryDictionary extends Dictionary { } // Update entries for the word occurrence with the ngramContext. - @UsedForTesting - public boolean updateEntriesForWordWithNgramContext(final NgramContext ngramContext, + public boolean updateEntriesForWordWithNgramContext(@Nonnull final NgramContext ngramContext, final String word, final boolean isValidWord, final int count, final int timestamp) { if (TextUtils.isEmpty(word)) { return false; diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 1bdadc30b..d24f80a45 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -46,6 +46,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.Nonnull; + /** * Abstract base class for an expandable dictionary that can be created and updated dynamically * during runtime. When updated it automatically generates a new binary dictionary to handle future @@ -292,13 +294,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } } - /** - * Adds unigram information of a word to the dictionary. May overwrite an existing entry. - */ - public void addUnigramEntryWithCheckingDistracter(final String word, final int frequency, - final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, - final boolean isBlacklisted, final int timestamp, - final DistracterFilter distracterFilter) { + private void updateDictionaryWithWriteLockIfWordIsNotADistracter( + @Nonnull final Runnable updateTask, + @Nonnull final String word, @Nonnull final DistracterFilter distracterFilter) { reloadDictionaryIfRequired(); asyncPreCheckAndExecuteTaskWithWriteLock( new Callable<Boolean>() { @@ -315,12 +313,27 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return; } runGCIfRequiredLocked(true /* mindsBlockByGC */); - addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq, - isNotAWord, isBlacklisted, timestamp); + updateTask.run(); } }); } + /** + * Adds unigram information of a word to the dictionary. May overwrite an existing entry. + */ + public void addUnigramEntryWithCheckingDistracter(final String word, final int frequency, + final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, + final boolean isBlacklisted, final int timestamp, + @Nonnull final DistracterFilter distracterFilter) { + updateDictionaryWithWriteLockIfWordIsNotADistracter(new Runnable() { + @Override + public void run() { + addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq, + isNotAWord, isBlacklisted, timestamp); + } + }, word, distracterFilter); + } + protected void addUnigramLocked(final String word, final int frequency, final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, final boolean isBlacklisted, final int timestamp) { @@ -354,7 +367,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { /** * Adds n-gram information of a word to the dictionary. May overwrite an existing entry. */ - public void addNgramEntry(final NgramContext ngramContext, final String word, + public void addNgramEntry(@Nonnull final NgramContext ngramContext, final String word, final int frequency, final int timestamp) { reloadDictionaryIfRequired(); asyncExecuteTaskWithWriteLock(new Runnable() { @@ -369,7 +382,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { }); } - protected void addNgramEntryLocked(final NgramContext ngramContext, final String word, + protected void addNgramEntryLocked(@Nonnull final NgramContext ngramContext, final String word, final int frequency, final int timestamp) { if (!mBinaryDictionary.addNgramEntry(ngramContext, word, frequency, timestamp)) { if (DEBUG) { @@ -383,7 +396,8 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * Dynamically remove the n-gram entry in the dictionary. */ @UsedForTesting - public void removeNgramDynamically(final NgramContext ngramContext, final String word) { + public void removeNgramDynamically(@Nonnull final NgramContext ngramContext, + final String word) { reloadDictionaryIfRequired(); asyncExecuteTaskWithWriteLock(new Runnable() { @Override @@ -402,6 +416,26 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { }); } + /** + * Update dictionary for the word with the ngramContext if the word is not a distracter. + */ + public void updateEntriesForWordWithCheckingDistracter(@Nonnull final NgramContext ngramContext, + final String word, final boolean isValidWord, final int count, final int timestamp, + @Nonnull final DistracterFilter distracterFilter) { + updateDictionaryWithWriteLockIfWordIsNotADistracter(new Runnable() { + @Override + public void run() { + if (!mBinaryDictionary.updateEntriesForWordWithNgramContext(ngramContext, word, + isValidWord, count, timestamp)) { + if (DEBUG) { + Log.e(TAG, "Cannot update counter. word: " + word + + " context: "+ ngramContext.toString()); + } + } + } + }, word, distracterFilter); + } + public interface AddMultipleDictionaryEntriesCallback { public void onFinished(); } @@ -410,7 +444,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * Dynamically add multiple entries to the dictionary. */ public void addMultipleDictionaryEntriesDynamically( - final ArrayList<LanguageModelParam> languageModelParams, + @Nonnull final ArrayList<LanguageModelParam> languageModelParams, final AddMultipleDictionaryEntriesCallback callback) { reloadDictionaryIfRequired(); asyncExecuteTaskWithWriteLock(new Runnable() { diff --git a/java/src/com/android/inputmethod/latin/NgramContext.java b/java/src/com/android/inputmethod/latin/NgramContext.java index 6d438584f..a02531cc4 100644 --- a/java/src/com/android/inputmethod/latin/NgramContext.java +++ b/java/src/com/android/inputmethod/latin/NgramContext.java @@ -158,11 +158,6 @@ public class NgramContext { } } - public NgramContext getTrimmedNgramContext(final int maxPrevWordCount) { - final int newSize = Math.min(maxPrevWordCount, mPrevWordsCount); - return new NgramContext(this /* prevWordsInfo */, newSize); - } - public int getPrevWordCount() { return mPrevWordsCount; } diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java index d61684698..59761547d 100644 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java @@ -65,34 +65,7 @@ public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBas if (word.length() > Constants.DICTIONARY_MAX_WORD_LENGTH) { return; } - final int frequency = isValid ? - FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS; - userHistoryDictionary.addUnigramEntryWithCheckingDistracter(word, frequency, - null /* shortcutTarget */, 0 /* shortcutFreq */, false /* isNotAWord */, - false /* isBlacklisted */, timestamp, distracterFilter); - - final boolean isBeginningOfSentenceContext = ngramContext.isBeginningOfSentenceContext(); - final NgramContext ngramContextToBeSaved = - ngramContext.getTrimmedNgramContext(SUPPORTED_NGRAM - 1); - for (int i = 0; i < ngramContextToBeSaved.getPrevWordCount(); i++) { - final CharSequence prevWord = ngramContextToBeSaved.getNthPrevWord(1 /* n */); - if (prevWord == null || (prevWord.length() > Constants.DICTIONARY_MAX_WORD_LENGTH)) { - return; - } - // Do not insert a word as a bigram of itself - if (i == 0 && TextUtils.equals(word, prevWord)) { - return; - } - if (isBeginningOfSentenceContext) { - // Beginning-of-Sentence n-gram entry is added as an n-gram entry of an OOV word. - userHistoryDictionary.addNgramEntry( - ngramContextToBeSaved.getTrimmedNgramContext(i + 1), word, - FREQUENCY_FOR_WORDS_NOT_IN_DICTS, timestamp); - } else { - userHistoryDictionary.addNgramEntry( - ngramContextToBeSaved.getTrimmedNgramContext(i + 1), word, frequency, - timestamp); - } - } + userHistoryDictionary.updateEntriesForWordWithCheckingDistracter(ngramContext, word, + isValid, 1 /* count */, timestamp, distracterFilter); } } diff --git a/tests/src/com/android/inputmethod/compat/SuggestionSpanUtilsTest.java b/tests/src/com/android/inputmethod/compat/SuggestionSpanUtilsTest.java new file mode 100644 index 000000000..a6b3af4c2 --- /dev/null +++ b/tests/src/com/android/inputmethod/compat/SuggestionSpanUtilsTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.annotation.TargetApi; +import android.os.Build; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.SuggestionSpan; + +import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +@SmallTest +public class SuggestionSpanUtilsTest extends AndroidTestCase { + + /** + * Helper method to create a dummy {@link SuggestedWordInfo}. + * + * @param kindAndFlags the kind and flags to be used to create {@link SuggestedWordInfo}. + * @param word the word to be used to create {@link SuggestedWordInfo}. + * @return a new instance of {@link SuggestedWordInfo}. + */ + private static SuggestedWordInfo createWordInfo(final String word, final int kindAndFlags) { + return new SuggestedWordInfo(word, 1 /* score */, kindAndFlags, null /* sourceDict */, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); + } + + private static void assertNotSuggestionSpan(final String expectedText, + final CharSequence actualText) { + assertTrue(TextUtils.equals(expectedText, actualText)); + if (!(actualText instanceof Spanned)) { + return; + } + final Spanned spanned = (Spanned)actualText; + final SuggestionSpan[] suggestionSpans = spanned.getSpans(0, spanned.length(), + SuggestionSpan.class); + assertEquals(0, suggestionSpans.length); + } + + private static void assertSuggestionSpan(final String expectedText, + final int reuiredSuggestionSpanFlags, final int requiredSpanFlags, + final String[] expectedSuggestions, + final CharSequence actualText) { + assertTrue(TextUtils.equals(expectedText, actualText)); + assertTrue(actualText instanceof Spanned); + final Spanned spanned = (Spanned)actualText; + final SuggestionSpan[] suggestionSpans = spanned.getSpans(0, spanned.length(), + SuggestionSpan.class); + assertEquals(1, suggestionSpans.length); + final SuggestionSpan suggestionSpan = suggestionSpans[0]; + if (reuiredSuggestionSpanFlags != 0) { + assertTrue((suggestionSpan.getFlags() & reuiredSuggestionSpanFlags) != 0); + } + if (requiredSpanFlags != 0) { + assertTrue((spanned.getSpanFlags(suggestionSpan) & requiredSpanFlags) != 0); + } + if (expectedSuggestions != null) { + final String[] actualSuggestions = suggestionSpan.getSuggestions(); + assertEquals(expectedSuggestions.length, actualSuggestions.length); + for (int i = 0; i < expectedSuggestions.length; ++i) { + assertEquals(expectedSuggestions[i], actualSuggestions[i]); + } + } + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) + public void testGetTextWithAutoCorrectionIndicatorUnderline() { + final String ORIGINAL_TEXT = "Hey!"; + final CharSequence text = SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline( + getContext(), ORIGINAL_TEXT); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + assertNotSuggestionSpan(ORIGINAL_TEXT, text); + return; + } + + assertSuggestionSpan(ORIGINAL_TEXT, + SuggestionSpan.FLAG_AUTO_CORRECTION /* reuiredSuggestionSpanFlags */, + Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE /* requiredSpanFlags */, + new String[]{}, text); + } + + public void testGetTextWithSuggestionSpan() { + final SuggestedWordInfo predicition1 = + createWordInfo("Quality", SuggestedWordInfo.KIND_PREDICTION); + final SuggestedWordInfo predicition2 = + createWordInfo("Speed", SuggestedWordInfo.KIND_PREDICTION); + final SuggestedWordInfo predicition3 = + createWordInfo("Price", SuggestedWordInfo.KIND_PREDICTION); + + final SuggestedWordInfo typed = + createWordInfo("Hey", SuggestedWordInfo.KIND_TYPED); + + final SuggestedWordInfo[] corrections = + new SuggestedWordInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE * 2]; + for (int i = 0; i < corrections.length; ++i) { + corrections[i] = createWordInfo("correction" + i, SuggestedWordInfo.KIND_CORRECTION); + } + + // SuggestionSpan will not be attached when {@link SuggestedWords#INPUT_STYLE_PREDICTION} + // is specified. + { + final SuggestedWords predictedWords = new SuggestedWords( + new ArrayList<>(Arrays.asList(predicition1, predicition2, predicition3)), + null /* rawSuggestions */, + false /* typedWordValid */, + false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, + SuggestedWords.INPUT_STYLE_PREDICTION); + final String PICKED_WORD = predicition2.mWord; + assertNotSuggestionSpan( + PICKED_WORD, + SuggestionSpanUtils.getTextWithSuggestionSpan(getContext(), PICKED_WORD, + predictedWords)); + } + + final ArrayList<SuggestedWordInfo> suggestedWordList = new ArrayList<>(); + suggestedWordList.add(typed); + suggestedWordList.add(predicition1); + suggestedWordList.add(predicition2); + suggestedWordList.add(predicition3); + suggestedWordList.addAll(Arrays.asList(corrections)); + final SuggestedWords typedAndCollectedWords = new SuggestedWords( + suggestedWordList, + null /* rawSuggestions */, + false /* typedWordValid */, + false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, + SuggestedWords.INPUT_STYLE_TYPING); + + for (final SuggestedWordInfo pickedWord : suggestedWordList) { + final String PICKED_WORD = pickedWord.mWord; + + final ArrayList<String> expectedSuggestions = new ArrayList<>(); + for (SuggestedWordInfo suggestedWordInfo : suggestedWordList) { + if (expectedSuggestions.size() >= SuggestionSpan.SUGGESTIONS_MAX_SIZE) { + break; + } + if (suggestedWordInfo.isKindOf(SuggestedWordInfo.KIND_PREDICTION)) { + // Currently predictions are not filled into SuggestionSpan. + continue; + } + final String suggestedWord = suggestedWordInfo.mWord; + if (TextUtils.equals(PICKED_WORD, suggestedWord)) { + // Typed word itself is not added to SuggestionSpan. + continue; + } + expectedSuggestions.add(suggestedWord); + } + + assertSuggestionSpan( + PICKED_WORD, + 0 /* reuiredSuggestionSpanFlags */, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE /* requiredSpanFlags */, + expectedSuggestions.toArray(new String[expectedSuggestions.size()]), + SuggestionSpanUtils.getTextWithSuggestionSpan(getContext(), PICKED_WORD, + typedAndCollectedWords)); + } + } +} |