From 9e76304d6004c43c3149bc2df460af2a00b18a4f Mon Sep 17 00:00:00 2001 From: Keisuke Kuroyanagi Date: Thu, 10 Jul 2014 12:51:37 +0900 Subject: [PATCH] Make spell checker use dictionary facilitator. Bug: 13630847 Change-Id: I07d17ccf5ce0755f63a0b8d236d77600baaf62b6 --- .../AndroidSpellCheckerService.java | 338 ++++++++++-------- .../AndroidWordLevelSpellCheckerSession.java | 136 +++---- .../latin/spellcheck/DictAndKeyboard.java | 45 --- .../latin/spellcheck/DictionaryPool.java | 137 ------- ...onouslyLoadedContactsBinaryDictionary.java | 56 --- ...nchronouslyLoadedUserBinaryDictionary.java | 61 ---- 6 files changed, 236 insertions(+), 537 deletions(-) delete mode 100644 java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java delete mode 100644 java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java delete mode 100644 java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedContactsBinaryDictionary.java delete mode 100644 java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedUserBinaryDictionary.java diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 90c8f618f..72b503138 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -16,22 +16,31 @@ package com.android.inputmethod.latin.spellcheck; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.service.textservice.SpellCheckerService; import android.text.InputType; import android.util.Log; +import android.util.LruCache; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.textservice.SuggestionsInfo; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardLayoutSet; +import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.ContactsBinaryDictionary; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryCollection; +import com.android.inputmethod.latin.DictionaryFacilitator; import com.android.inputmethod.latin.DictionaryFactory; +import com.android.inputmethod.latin.PrevWordsInfo; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.UserBinaryDictionary; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; @@ -39,15 +48,23 @@ import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.SuggestionResults; +import com.android.inputmethod.latin.WordComposer; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; /** * Service for spell checking, using LatinIME's dictionaries and mechanisms. @@ -56,31 +73,79 @@ public final class AndroidSpellCheckerService extends SpellCheckerService implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = AndroidSpellCheckerService.class.getSimpleName(); private static final boolean DBG = false; - private static final int POOL_SIZE = 2; public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts"; private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480; private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 368; - private final static String[] EMPTY_STRING_ARRAY = new String[0]; - private Map mDictionaryPools = CollectionUtils.newSynchronizedTreeMap(); - private Map mUserDictionaries = - CollectionUtils.newSynchronizedTreeMap(); - private ContactsBinaryDictionary mContactsDictionary; + private static final String DICTIONARY_NAME_PREFIX = "spellcheck_"; + private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000; + private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5; + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private final HashSet mCachedLocales = new HashSet<>(); + + private final int MAX_NUM_OF_THREADS_READ_DICTIONARY = 2; + private final Semaphore mSemaphore = new Semaphore(MAX_NUM_OF_THREADS_READ_DICTIONARY, + true /* fair */); + // TODO: Make each spell checker session has its own session id. + private final ConcurrentLinkedQueue mSessionIdPool = new ConcurrentLinkedQueue<>(); + + private static class DictionaryFacilitatorLruCache extends + LruCache { + private final HashSet mCachedLocales; + public DictionaryFacilitatorLruCache(final HashSet cachedLocales, int maxSize) { + super(maxSize); + mCachedLocales = cachedLocales; + } + + @Override + protected void entryRemoved(boolean evicted, Locale key, + DictionaryFacilitator oldValue, DictionaryFacilitator newValue) { + if (oldValue != null && oldValue != newValue) { + oldValue.closeDictionaries(); + } + if (key != null && newValue == null) { + // Remove locale from the cache when the dictionary facilitator for the locale is + // evicted and new facilitator is not set for the locale. + mCachedLocales.remove(key); + if (size() >= maxSize()) { + Log.w(TAG, "DictionaryFacilitator for " + key.toString() + + " has been evicted due to cache size limit." + + " size: " + size() + ", maxSize: " + maxSize()); + } + } + } + } + + private static final int MAX_DICTIONARY_FACILITATOR_COUNT = 3; + private final LruCache mDictionaryFacilitatorCache = + new DictionaryFacilitatorLruCache(mCachedLocales, MAX_DICTIONARY_FACILITATOR_COUNT); + private final ConcurrentHashMap mKeyboardCache = new ConcurrentHashMap<>(); // The threshold for a suggestion to be considered "recommended". private float mRecommendedThreshold; // Whether to use the contacts dictionary private boolean mUseContactsDictionary; - private final Object mUseContactsLock = new Object(); - - private final HashSet> mDictionaryCollectionsList = - new HashSet<>(); + // TODO: make a spell checker option to block offensive words or not + private final SettingsValuesForSuggestion mSettingsValuesForSuggestion = + new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */, + true /* spaceAwareGestureEnabled */, + null /* additionalFeaturesSettingValues */); + private final Object mDictionaryLock = new Object(); public static final String SINGLE_QUOTE = "\u0027"; public static final String APOSTROPHE = "\u2019"; + public AndroidSpellCheckerService() { + super(); + for (int i = 0; i < MAX_NUM_OF_THREADS_READ_DICTIONARY; i++) { + mSessionIdPool.add(i); + } + } + @Override public void onCreate() { super.onCreate(); mRecommendedThreshold = @@ -106,52 +171,21 @@ public final class AndroidSpellCheckerService extends SpellCheckerService @Override public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { if (!PREF_USE_CONTACTS_KEY.equals(key)) return; - synchronized(mUseContactsLock) { - mUseContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); - if (mUseContactsDictionary) { - startUsingContactsDictionaryLocked(); - } else { - stopUsingContactsDictionaryLocked(); + final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); + if (useContactsDictionary != mUseContactsDictionary) { + mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); + try { + mUseContactsDictionary = useContactsDictionary; + for (final Locale locale : mCachedLocales) { + final DictionaryFacilitator dictionaryFacilitator = + mDictionaryFacilitatorCache.get(locale); + resetDictionariesForLocale(this /* context */, + dictionaryFacilitator, locale, mUseContactsDictionary); + } + } finally { + mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); + } } - } - } - - private void startUsingContactsDictionaryLocked() { - if (null == mContactsDictionary) { - // TODO: use the right locale for each session - mContactsDictionary = - new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault()); - } - final Iterator> iterator = - mDictionaryCollectionsList.iterator(); - while (iterator.hasNext()) { - final WeakReference dictRef = iterator.next(); - final DictionaryCollection dict = dictRef.get(); - if (null == dict) { - iterator.remove(); - } else { - dict.addDictionary(mContactsDictionary); - } - } - } - - private void stopUsingContactsDictionaryLocked() { - if (null == mContactsDictionary) return; - final Dictionary contactsDict = mContactsDictionary; - // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no longer needed - mContactsDictionary = null; - final Iterator> iterator = - mDictionaryCollectionsList.iterator(); - while (iterator.hasNext()) { - final WeakReference dictRef = iterator.next(); - final DictionaryCollection dict = dictRef.get(); - if (null == dict) { - iterator.remove(); - } else { - dict.removeDictionary(contactsDict); - } - } - contactsDict.close(); } @Override @@ -203,11 +237,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService private final int mMaxLength; private int mLength = 0; - // The two following attributes are only ever filled if the requested max length - // is 0 (or less, which is treated the same). - private String mBestSuggestion = null; - private int mBestScore = Integer.MIN_VALUE; // As small as possible - SuggestionsGatherer(final String originalText, final float recommendedThreshold, final int maxLength) { mOriginalText = originalText; @@ -226,20 +255,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService // Weak <- insertIndex == 0, ..., insertIndex == mLength -> Strong if (insertIndex == 0 && mLength >= mMaxLength) { - // In the future, we may want to keep track of the best suggestion score even if - // we are asked for 0 suggestions. In this case, we can use the following - // (tested) code to keep it: - // If the maxLength is 0 (should never be less, but if it is, it's treated as 0) - // then we need to keep track of the best suggestion in mBestScore and - // mBestSuggestion. This is so that we know whether the best suggestion makes - // the score cutoff, since we need to know that to return a meaningful - // looksLikeTypo. - // if (0 >= mMaxLength) { - // if (score > mBestScore) { - // mBestScore = score; - // mBestSuggestion = new String(word, wordOffset, wordLength); - // } - // } return true; } @@ -264,20 +279,8 @@ public final class AndroidSpellCheckerService extends SpellCheckerService final String[] gatheredSuggestions; final boolean hasRecommendedSuggestions; if (0 == mLength) { - // TODO: the comment below describes what is intended, but in the practice - // mBestSuggestion is only ever set to null so it doesn't work. Fix this. - // Either we found no suggestions, or we found some BUT the max length was 0. - // If we found some mBestSuggestion will not be null. If it is null, then - // we found none, regardless of the max length. - if (null == mBestSuggestion) { - gatheredSuggestions = null; - hasRecommendedSuggestions = false; - } else { - gatheredSuggestions = EMPTY_STRING_ARRAY; - final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( - mOriginalText, mBestSuggestion, mBestScore); - hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); - } + gatheredSuggestions = null; + hasRecommendedSuggestions = false; } else { if (DBG) { if (mLength != mSuggestions.size()) { @@ -323,85 +326,114 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } } + public boolean isValidWord(final Locale locale, final String word) { + mSemaphore.acquireUninterruptibly(); + try { + DictionaryFacilitator dictionaryFacilitatorForLocale = + getDictionaryFacilitatorForLocaleLocked(locale); + return dictionaryFacilitatorForLocale.isValidWord(word, false /* igroreCase */); + } finally { + mSemaphore.release(); + } + } + + public SuggestionResults getSuggestionResults(final Locale locale, final WordComposer composer, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo) { + Integer sessionId = null; + mSemaphore.acquireUninterruptibly(); + try { + sessionId = mSessionIdPool.poll(); + DictionaryFacilitator dictionaryFacilitatorForLocale = + getDictionaryFacilitatorForLocaleLocked(locale); + return dictionaryFacilitatorForLocale.getSuggestionResults(composer, prevWordsInfo, + proximityInfo, mSettingsValuesForSuggestion, sessionId); + } finally { + if (sessionId != null) { + mSessionIdPool.add(sessionId); + } + mSemaphore.release(); + } + } + + public boolean hasMainDictionaryForLocale(final Locale locale) { + mSemaphore.acquireUninterruptibly(); + try { + final DictionaryFacilitator dictionaryFacilitator = + getDictionaryFacilitatorForLocaleLocked(locale); + return dictionaryFacilitator.hasInitializedMainDictionary(); + } finally { + mSemaphore.release(); + } + } + + private DictionaryFacilitator getDictionaryFacilitatorForLocaleLocked(final Locale locale) { + DictionaryFacilitator dictionaryFacilitatorForLocale = + mDictionaryFacilitatorCache.get(locale); + if (dictionaryFacilitatorForLocale == null) { + dictionaryFacilitatorForLocale = new DictionaryFacilitator(); + mDictionaryFacilitatorCache.put(locale, dictionaryFacilitatorForLocale); + mCachedLocales.add(locale); + resetDictionariesForLocale(this /* context */, dictionaryFacilitatorForLocale, + locale, mUseContactsDictionary); + } + return dictionaryFacilitatorForLocale; + } + + private static void resetDictionariesForLocale(final Context context, + final DictionaryFacilitator dictionaryFacilitator, final Locale locale, + final boolean useContactsDictionary) { + dictionaryFacilitator.resetDictionariesWithDictNamePrefix(context, locale, + useContactsDictionary, false /* usePersonalizedDicts */, + false /* forceReloadMainDictionary */, null /* listener */, + DICTIONARY_NAME_PREFIX); + for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) { + try { + dictionaryFacilitator.waitForLoadingMainDictionary( + WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS); + return; + } catch (final InterruptedException e) { + Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e); + if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) { + Log.i(TAG, "Retry", e); + } else { + Log.w(TAG, "Give up retrying. Retried " + + MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e); + } + } + } + } + @Override public boolean onUnbind(final Intent intent) { - closeAllDictionaries(); + mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); + try { + mDictionaryFacilitatorCache.evictAll(); + mCachedLocales.clear(); + } finally { + mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); + } + mKeyboardCache.clear(); return false; } - private void closeAllDictionaries() { - final Map oldPools = mDictionaryPools; - mDictionaryPools = CollectionUtils.newSynchronizedTreeMap(); - final Map oldUserDictionaries = mUserDictionaries; - mUserDictionaries = CollectionUtils.newSynchronizedTreeMap(); - new Thread("spellchecker_close_dicts") { - @Override - public void run() { - // Contacts dictionary can be closed multiple times here. If the dictionary is - // already closed, extra closings are no-ops, so it's safe. - for (DictionaryPool pool : oldPools.values()) { - pool.close(); - } - for (Dictionary dict : oldUserDictionaries.values()) { - dict.close(); - } - synchronized (mUseContactsLock) { - if (null != mContactsDictionary) { - // The synchronously loaded contacts dictionary should have been in one - // or several pools, but it is shielded against multiple closing and it's - // safe to call it several times. - final ContactsBinaryDictionary dictToClose = mContactsDictionary; - // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY - // is no longer needed - mContactsDictionary = null; - dictToClose.close(); - } - } + public Keyboard getKeyboardForLocale(final Locale locale) { + Keyboard keyboard = mKeyboardCache.get(locale); + if (keyboard == null) { + keyboard = createKeyboardForLocale(locale); + if (keyboard != null) { + mKeyboardCache.put(locale, keyboard); } - }.start(); - } - - public DictionaryPool getDictionaryPool(final String locale) { - DictionaryPool pool = mDictionaryPools.get(locale); - if (null == pool) { - final Locale localeObject = LocaleUtils.constructLocaleFromString(locale); - pool = new DictionaryPool(POOL_SIZE, this, localeObject); - mDictionaryPools.put(locale, pool); } - return pool; + return keyboard; } - public DictAndKeyboard createDictAndKeyboard(final Locale locale) { + private Keyboard createKeyboardForLocale(final Locale locale) { final int script = ScriptUtils.getScriptFromSpellCheckerLocale(locale); final String keyboardLayoutName = getKeyboardLayoutNameForScript(script); final InputMethodSubtype subtype = AdditionalSubtypeUtils.createDummyAdditionalSubtype( locale.toString(), keyboardLayoutName); final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype); - - final DictionaryCollection dictionaryCollection = - DictionaryFactory.createMainDictionaryFromManager(this, locale, - true /* useFullEditDistance */); - final String localeStr = locale.toString(); - UserBinaryDictionary userDictionary = mUserDictionaries.get(localeStr); - if (null == userDictionary) { - userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, locale, true); - mUserDictionaries.put(localeStr, userDictionary); - } - dictionaryCollection.addDictionary(userDictionary); - synchronized (mUseContactsLock) { - if (mUseContactsDictionary) { - if (null == mContactsDictionary) { - // TODO: use the right locale. We can't do it right now because the - // spell checker is reusing the contacts dictionary across sessions - // without regard for their locale, so we need to fix that first. - mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this, - Locale.getDefault()); - } - } - dictionaryCollection.addDictionary(mContactsDictionary); - mDictionaryCollectionsList.add(new WeakReference<>(dictionaryCollection)); - } - return new DictAndKeyboard(dictionaryCollection, keyboardLayoutSet); + return keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); } private KeyboardLayoutSet createKeyboardSetForSpellChecker(final InputMethodSubtype subtype) { diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index 08adaf844..19c1dd0a5 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -28,8 +28,9 @@ import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; import com.android.inputmethod.compat.SuggestionsInfoCompatUtils; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.PrevWordsInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; @@ -39,17 +40,15 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.SuggestionResults; -import java.util.ArrayList; import java.util.Locale; public abstract class AndroidWordLevelSpellCheckerSession extends Session { private static final String TAG = AndroidWordLevelSpellCheckerSession.class.getSimpleName(); private static final boolean DBG = false; - // Immutable, but need the locale which is not available in the constructor yet - private DictionaryPool mDictionaryPool; - // Likewise + // Immutable, but not available in the constructor. private Locale mLocale; // Cache this for performance private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now. @@ -116,7 +115,6 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { @Override public void onCreate() { final String localeString = getLocale(); - mDictionaryPool = mService.getDictionaryPool(localeString); mLocale = LocaleUtils.constructLocaleFromString(localeString); mScript = ScriptUtils.getScriptFromSpellCheckerLocale(mLocale); } @@ -191,24 +189,24 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { * If the "TEXT" is fully upper case, we test the exact string "TEXT", the lower-cased * version of it "text" and the capitalized version of it "Text". */ - private boolean isInDictForAnyCapitalization(final Dictionary dict, final String text, - final int capitalizeType) { + private boolean isInDictForAnyCapitalization(final String text, final int capitalizeType) { // If the word is in there as is, then it's in the dictionary. If not, we'll test lower // case versions, but only if the word is not already all-lower case or mixed case. - if (dict.isValidWord(text)) return true; + if (mService.isValidWord(mLocale, text)) return true; if (StringUtils.CAPITALIZE_NONE == capitalizeType) return false; // If we come here, we have a capitalized word (either First- or All-). // Downcase the word and look it up again. If the word is only capitalized, we // tested all possibilities, so if it's still negative we can return false. final String lowerCaseText = text.toLowerCase(mLocale); - if (dict.isValidWord(lowerCaseText)) return true; + if (mService.isValidWord(mLocale, lowerCaseText)) return true; if (StringUtils.CAPITALIZE_FIRST == capitalizeType) return false; // If the lower case version is not in the dictionary, it's still possible // that we have an all-caps version of a word that needs to be capitalized // according to the dictionary. E.g. "GERMANS" only exists in the dictionary as "Germans". - return dict.isValidWord(StringUtils.capitalizeFirstAndDowncaseRest(lowerCaseText, mLocale)); + return mService.isValidWord(mLocale, + StringUtils.capitalizeFirstAndDowncaseRest(lowerCaseText, mLocale)); } // Note : this must be reentrant @@ -236,46 +234,28 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { return new SuggestionsInfo( cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions); } - final int checkability = getCheckabilityInScript(inText, mScript); if (CHECKABILITY_CHECKABLE != checkability) { - DictAndKeyboard dictInfo = null; - try { - dictInfo = mDictionaryPool.pollWithDefaultTimeout(); - if (!DictionaryPool.isAValidDictionary(dictInfo)) { - return AndroidSpellCheckerService.getNotInDictEmptySuggestions( - false /* reportAsTypo */); - } - if (CHECKABILITY_CONTAINS_PERIOD == checkability) { - final String[] splitText = inText.split(Constants.REGEXP_PERIOD); - boolean allWordsAreValid = true; - for (final String word : splitText) { - if (!dictInfo.mDictionary.isValidWord(word)) { - allWordsAreValid = false; - break; - } - } - if (allWordsAreValid) { - return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO - | SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS, - new String[] { - TextUtils.join(Constants.STRING_SPACE, splitText), - TextUtils.join(Constants.STRING_PERIOD_AND_SPACE, - splitText) }); + if (CHECKABILITY_CONTAINS_PERIOD == checkability) { + final String[] splitText = inText.split(Constants.REGEXP_PERIOD); + boolean allWordsAreValid = true; + for (final String word : splitText) { + if (!mService.isValidWord(mLocale, word)) { + allWordsAreValid = false; + break; } } - return dictInfo.mDictionary.isValidWord(inText) - ? AndroidSpellCheckerService.getInDictEmptySuggestions() - : AndroidSpellCheckerService.getNotInDictEmptySuggestions( - CHECKABILITY_CONTAINS_PERIOD == checkability - /* reportAsTypo */); - } finally { - if (null != dictInfo) { - if (!mDictionaryPool.offer(dictInfo)) { - Log.e(TAG, "Can't re-insert a dictionary into its pool"); - } + if (allWordsAreValid) { + return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO + | SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS, + new String[] { + TextUtils.join(Constants.STRING_SPACE, splitText) }); } } + return mService.isValidWord(mLocale, inText) ? + AndroidSpellCheckerService.getInDictEmptySuggestions() : + AndroidSpellCheckerService.getNotInDictEmptySuggestions( + CHECKABILITY_CONTAINS_PERIOD == checkability /* reportAsTypo */); } final String text = inText.replaceAll( AndroidSpellCheckerService.APOSTROPHE, AndroidSpellCheckerService.SINGLE_QUOTE); @@ -289,48 +269,34 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { final int capitalizeType = StringUtils.getCapitalizationType(text); boolean isInDict = true; - DictAndKeyboard dictInfo = null; - try { - dictInfo = mDictionaryPool.pollWithDefaultTimeout(); - if (!DictionaryPool.isAValidDictionary(dictInfo)) { - return AndroidSpellCheckerService.getNotInDictEmptySuggestions( - false /* reportAsTypo */); - } - final WordComposer composer = new WordComposer(); - final int[] codePoints = StringUtils.toCodePointArray(text); - final int[] coordinates; - if (null == dictInfo.mKeyboard) { - coordinates = CoordinateUtils.newCoordinateArray(codePoints.length, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - } else { - coordinates = dictInfo.mKeyboard.getCoordinates(codePoints); - } - composer.setComposingWord(codePoints, coordinates); - // TODO: make a spell checker option to block offensive words or not - final ArrayList suggestions = - dictInfo.mDictionary.getSuggestions(composer, prevWordsInfo, - dictInfo.getProximityInfo(), - new SettingsValuesForSuggestion( - true /* blockPotentiallyOffensive */, - true /* spaceAwareGestureEnabled */, - null /* additionalFeaturesSettingValues */), - 0 /* sessionId */, - null /* inOutLanguageWeight */); - if (suggestions != null) { - for (final SuggestedWordInfo suggestion : suggestions) { - final String suggestionStr = suggestion.mWord; - suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0, - suggestionStr.length(), suggestion.mScore); - } - } - isInDict = isInDictForAnyCapitalization(dictInfo.mDictionary, text, capitalizeType); - } finally { - if (null != dictInfo) { - if (!mDictionaryPool.offer(dictInfo)) { - Log.e(TAG, "Can't re-insert a dictionary into its pool"); - } + if (!mService.hasMainDictionaryForLocale(mLocale)) { + return AndroidSpellCheckerService.getNotInDictEmptySuggestions( + false /* reportAsTypo */); + } + final Keyboard keyboard = mService.getKeyboardForLocale(mLocale); + final WordComposer composer = new WordComposer(); + final int[] codePoints = StringUtils.toCodePointArray(text); + final int[] coordinates; + final ProximityInfo proximityInfo; + if (null == keyboard) { + coordinates = CoordinateUtils.newCoordinateArray(codePoints.length, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + proximityInfo = null; + } else { + coordinates = keyboard.getCoordinates(codePoints); + proximityInfo = keyboard.getProximityInfo(); + } + composer.setComposingWord(codePoints, coordinates); + final SuggestionResults suggestionResults = mService.getSuggestionResults( + mLocale, composer, prevWordsInfo, proximityInfo); + if (suggestionResults != null) { + for (final SuggestedWordInfo suggestion : suggestionResults) { + final String suggestionStr = suggestion.mWord; + suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0, + suggestionStr.length(), suggestion.mScore); } } + isInDict = isInDictForAnyCapitalization(text, capitalizeType); final SuggestionsGatherer.Result result = suggestionsGatherer.getResults( capitalizeType, mLocale); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java b/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java deleted file mode 100644 index b33739fc1..000000000 --- a/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 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.spellcheck; - -import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.keyboard.KeyboardId; -import com.android.inputmethod.keyboard.KeyboardLayoutSet; -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.Dictionary; - -/** - * A container for a Dictionary and a Keyboard. - */ -public final class DictAndKeyboard { - public final Dictionary mDictionary; - public final Keyboard mKeyboard; - - public DictAndKeyboard( - final Dictionary dictionary, final KeyboardLayoutSet keyboardLayoutSet) { - mDictionary = dictionary; - if (keyboardLayoutSet == null) { - mKeyboard = null; - return; - } - mKeyboard = keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); - } - - public ProximityInfo getProximityInfo() { - return mKeyboard == null ? null : mKeyboard.getProximityInfo(); - } -} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java deleted file mode 100644 index eb85d4969..000000000 --- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 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.spellcheck; - -import android.util.Log; - -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.PrevWordsInfo; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; -import com.android.inputmethod.latin.WordComposer; - -import java.util.ArrayList; -import java.util.Locale; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - -/** - * A blocking queue that creates dictionaries up to a certain limit as necessary. - * As a deadlock-detecting device, if waiting for more than TIMEOUT = 3 seconds, we - * will clear the queue and generate its contents again. This is transparent for - * the client code, but may help with sloppy clients. - */ -@SuppressWarnings("serial") -public final class DictionaryPool extends LinkedBlockingQueue { - private final static String TAG = DictionaryPool.class.getSimpleName(); - // How many seconds we wait for a dictionary to become available. Past this delay, we give up in - // fear some bug caused a deadlock, and reset the whole pool. - private final static int TIMEOUT = 3; - private final AndroidSpellCheckerService mService; - private final int mMaxSize; - private final Locale mLocale; - private int mSize; - private volatile boolean mClosed; - final static ArrayList noSuggestions = new ArrayList<>(); - private final static DictAndKeyboard dummyDict = new DictAndKeyboard( - new Dictionary(Dictionary.TYPE_MAIN) { - // TODO: this dummy dictionary should be a singleton in the Dictionary class. - @Override - public ArrayList getSuggestions(final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, - final SettingsValuesForSuggestion settingsValuesForSuggestion, - final int sessionId, final float[] inOutLanguageWeight) { - return noSuggestions; - } - @Override - public boolean isInDictionary(final String word) { - // This is never called. However if for some strange reason it ever gets - // called, returning true is less destructive (it will not underline the - // word in red). - return true; - } - }, null); - - static public boolean isAValidDictionary(final DictAndKeyboard dictInfo) { - return null != dictInfo && dummyDict != dictInfo; - } - - public DictionaryPool(final int maxSize, final AndroidSpellCheckerService service, - final Locale locale) { - super(); - mMaxSize = maxSize; - mService = service; - mLocale = locale; - mSize = 0; - mClosed = false; - } - - @Override - public DictAndKeyboard poll(final long timeout, final TimeUnit unit) - throws InterruptedException { - final DictAndKeyboard dict = poll(); - if (null != dict) return dict; - synchronized(this) { - if (mSize >= mMaxSize) { - // Our pool is already full. Wait until some dictionary is ready, or TIMEOUT - // expires to avoid a deadlock. - final DictAndKeyboard result = super.poll(timeout, unit); - if (null == result) { - Log.e(TAG, "Deadlock detected ! Resetting dictionary pool"); - clear(); - mSize = 1; - return mService.createDictAndKeyboard(mLocale); - } else { - return result; - } - } else { - ++mSize; - return mService.createDictAndKeyboard(mLocale); - } - } - } - - // Convenience method - public DictAndKeyboard pollWithDefaultTimeout() { - try { - return poll(TIMEOUT, TimeUnit.SECONDS); - } catch (InterruptedException e) { - return null; - } - } - - public void close() { - synchronized(this) { - mClosed = true; - for (DictAndKeyboard dict : this) { - dict.mDictionary.close(); - } - clear(); - } - } - - @Override - public boolean offer(final DictAndKeyboard dict) { - if (mClosed) { - dict.mDictionary.close(); - return super.offer(dummyDict); - } else { - return super.offer(dict); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedContactsBinaryDictionary.java deleted file mode 100644 index 688b184cb..000000000 --- a/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedContactsBinaryDictionary.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.latin.spellcheck; - -import android.content.Context; - -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.ContactsBinaryDictionary; -import com.android.inputmethod.latin.PrevWordsInfo; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; -import com.android.inputmethod.latin.WordComposer; - -import java.util.ArrayList; -import java.util.Locale; - -public final class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryDictionary { - private static final String NAME = "spellcheck_contacts"; - private final Object mLock = new Object(); - - public SynchronouslyLoadedContactsBinaryDictionary(final Context context, final Locale locale) { - super(context, locale, null /* dictFile */, NAME); - } - - @Override - public ArrayList getSuggestions(final WordComposer codes, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, - final SettingsValuesForSuggestion settingsValuesForSuggestion, - final int sessionId, final float[] inOutLanguageWeight) { - synchronized (mLock) { - return super.getSuggestions(codes, prevWordsInfo, proximityInfo, - settingsValuesForSuggestion, sessionId, inOutLanguageWeight); - } - } - - @Override - public boolean isInDictionary(final String word) { - synchronized (mLock) { - return super.isInDictionary(word); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedUserBinaryDictionary.java deleted file mode 100644 index ff71f59f8..000000000 --- a/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedUserBinaryDictionary.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.latin.spellcheck; - -import android.content.Context; - -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.PrevWordsInfo; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; -import com.android.inputmethod.latin.UserBinaryDictionary; -import com.android.inputmethod.latin.WordComposer; - -import java.util.ArrayList; -import java.util.Locale; - -public final class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionary { - private static final String NAME = "spellcheck_user"; - private final Object mLock = new Object(); - - public SynchronouslyLoadedUserBinaryDictionary(final Context context, final Locale locale) { - this(context, locale, false /* alsoUseMoreRestrictiveLocales */); - } - - public SynchronouslyLoadedUserBinaryDictionary(final Context context, final Locale locale, - final boolean alsoUseMoreRestrictiveLocales) { - super(context, locale, alsoUseMoreRestrictiveLocales, null /* dictFile */, NAME); - } - - @Override - public ArrayList getSuggestions(final WordComposer codes, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, - final SettingsValuesForSuggestion settingsValuesForSuggestion, - final int sessionId, final float[] inOutLanguageWeight) { - synchronized (mLock) { - return super.getSuggestions(codes, prevWordsInfo, proximityInfo, - settingsValuesForSuggestion, sessionId, inOutLanguageWeight); - } - } - - @Override - public boolean isInDictionary(final String word) { - synchronized (mLock) { - return super.isInDictionary(word); - } - } -}