Make spell checker use dictionary facilitator.

Bug: 13630847
Change-Id: I07d17ccf5ce0755f63a0b8d236d77600baaf62b6
This commit is contained in:
Keisuke Kuroyanagi 2014-07-10 12:51:37 +09:00
parent 3b8f2ece40
commit 9e76304d60
6 changed files with 236 additions and 537 deletions

View file

@ -16,22 +16,31 @@
package com.android.inputmethod.latin.spellcheck; package com.android.inputmethod.latin.spellcheck;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.service.textservice.SpellCheckerService; import android.service.textservice.SpellCheckerService;
import android.text.InputType; import android.text.InputType;
import android.util.Log; import android.util.Log;
import android.util.LruCache;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype;
import android.view.textservice.SuggestionsInfo; 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.KeyboardLayoutSet;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.ContactsBinaryDictionary; import com.android.inputmethod.latin.ContactsBinaryDictionary;
import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.DictionaryCollection; import com.android.inputmethod.latin.DictionaryCollection;
import com.android.inputmethod.latin.DictionaryFacilitator;
import com.android.inputmethod.latin.DictionaryFactory; import com.android.inputmethod.latin.DictionaryFactory;
import com.android.inputmethod.latin.PrevWordsInfo;
import com.android.inputmethod.latin.R; 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.UserBinaryDictionary;
import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; 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.LocaleUtils;
import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.ScriptUtils;
import com.android.inputmethod.latin.utils.StringUtils; 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.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Locale; import java.util.Locale;
import java.util.Map; 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. * Service for spell checking, using LatinIME's dictionaries and mechanisms.
@ -56,31 +73,79 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
implements SharedPreferences.OnSharedPreferenceChangeListener { implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = AndroidSpellCheckerService.class.getSimpleName(); private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
private static final boolean DBG = false; 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"; 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_WIDTH = 480;
private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 368; private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 368;
private final static String[] EMPTY_STRING_ARRAY = new String[0]; private static final String DICTIONARY_NAME_PREFIX = "spellcheck_";
private Map<String, DictionaryPool> mDictionaryPools = CollectionUtils.newSynchronizedTreeMap(); private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000;
private Map<String, UserBinaryDictionary> mUserDictionaries = private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5;
CollectionUtils.newSynchronizedTreeMap();
private ContactsBinaryDictionary mContactsDictionary; private static final String[] EMPTY_STRING_ARRAY = new String[0];
private final HashSet<Locale> 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<Integer> mSessionIdPool = new ConcurrentLinkedQueue<>();
private static class DictionaryFacilitatorLruCache extends
LruCache<Locale, DictionaryFacilitator> {
private final HashSet<Locale> mCachedLocales;
public DictionaryFacilitatorLruCache(final HashSet<Locale> 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<Locale, DictionaryFacilitator> mDictionaryFacilitatorCache =
new DictionaryFacilitatorLruCache(mCachedLocales, MAX_DICTIONARY_FACILITATOR_COUNT);
private final ConcurrentHashMap<Locale, Keyboard> mKeyboardCache = new ConcurrentHashMap<>();
// The threshold for a suggestion to be considered "recommended". // The threshold for a suggestion to be considered "recommended".
private float mRecommendedThreshold; private float mRecommendedThreshold;
// Whether to use the contacts dictionary // Whether to use the contacts dictionary
private boolean mUseContactsDictionary; private boolean mUseContactsDictionary;
private final Object mUseContactsLock = new Object(); // TODO: make a spell checker option to block offensive words or not
private final SettingsValuesForSuggestion mSettingsValuesForSuggestion =
private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList = new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */,
new HashSet<>(); true /* spaceAwareGestureEnabled */,
null /* additionalFeaturesSettingValues */);
private final Object mDictionaryLock = new Object();
public static final String SINGLE_QUOTE = "\u0027"; public static final String SINGLE_QUOTE = "\u0027";
public static final String APOSTROPHE = "\u2019"; 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() { @Override public void onCreate() {
super.onCreate(); super.onCreate();
mRecommendedThreshold = mRecommendedThreshold =
@ -106,54 +171,23 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
@Override @Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (!PREF_USE_CONTACTS_KEY.equals(key)) return; if (!PREF_USE_CONTACTS_KEY.equals(key)) return;
synchronized(mUseContactsLock) { final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true);
mUseContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); if (useContactsDictionary != mUseContactsDictionary) {
if (mUseContactsDictionary) { mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY);
startUsingContactsDictionaryLocked(); try {
} else { mUseContactsDictionary = useContactsDictionary;
stopUsingContactsDictionaryLocked(); 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<WeakReference<DictionaryCollection>> iterator =
mDictionaryCollectionsList.iterator();
while (iterator.hasNext()) {
final WeakReference<DictionaryCollection> 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<WeakReference<DictionaryCollection>> iterator =
mDictionaryCollectionsList.iterator();
while (iterator.hasNext()) {
final WeakReference<DictionaryCollection> dictRef = iterator.next();
final DictionaryCollection dict = dictRef.get();
if (null == dict) {
iterator.remove();
} else {
dict.removeDictionary(contactsDict);
}
}
contactsDict.close();
}
@Override @Override
public Session createSession() { public Session createSession() {
// Should not refer to AndroidSpellCheckerSession directly considering // Should not refer to AndroidSpellCheckerSession directly considering
@ -203,11 +237,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
private final int mMaxLength; private final int mMaxLength;
private int mLength = 0; 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, SuggestionsGatherer(final String originalText, final float recommendedThreshold,
final int maxLength) { final int maxLength) {
mOriginalText = originalText; mOriginalText = originalText;
@ -226,20 +255,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
// Weak <- insertIndex == 0, ..., insertIndex == mLength -> Strong // Weak <- insertIndex == 0, ..., insertIndex == mLength -> Strong
if (insertIndex == 0 && mLength >= mMaxLength) { 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; return true;
} }
@ -264,20 +279,8 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
final String[] gatheredSuggestions; final String[] gatheredSuggestions;
final boolean hasRecommendedSuggestions; final boolean hasRecommendedSuggestions;
if (0 == mLength) { 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; gatheredSuggestions = null;
hasRecommendedSuggestions = false; hasRecommendedSuggestions = false;
} else {
gatheredSuggestions = EMPTY_STRING_ARRAY;
final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
mOriginalText, mBestSuggestion, mBestScore);
hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
}
} else { } else {
if (DBG) { if (DBG) {
if (mLength != mSuggestions.size()) { 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 @Override
public boolean onUnbind(final Intent intent) { 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; return false;
} }
private void closeAllDictionaries() { public Keyboard getKeyboardForLocale(final Locale locale) {
final Map<String, DictionaryPool> oldPools = mDictionaryPools; Keyboard keyboard = mKeyboardCache.get(locale);
mDictionaryPools = CollectionUtils.newSynchronizedTreeMap(); if (keyboard == null) {
final Map<String, UserBinaryDictionary> oldUserDictionaries = mUserDictionaries; keyboard = createKeyboardForLocale(locale);
mUserDictionaries = CollectionUtils.newSynchronizedTreeMap(); if (keyboard != null) {
new Thread("spellchecker_close_dicts") { mKeyboardCache.put(locale, keyboard);
@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();
} }
} }
} return keyboard;
}.start();
} }
public DictionaryPool getDictionaryPool(final String locale) { private Keyboard createKeyboardForLocale(final Locale 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;
}
public DictAndKeyboard createDictAndKeyboard(final Locale locale) {
final int script = ScriptUtils.getScriptFromSpellCheckerLocale(locale); final int script = ScriptUtils.getScriptFromSpellCheckerLocale(locale);
final String keyboardLayoutName = getKeyboardLayoutNameForScript(script); final String keyboardLayoutName = getKeyboardLayoutNameForScript(script);
final InputMethodSubtype subtype = AdditionalSubtypeUtils.createDummyAdditionalSubtype( final InputMethodSubtype subtype = AdditionalSubtypeUtils.createDummyAdditionalSubtype(
locale.toString(), keyboardLayoutName); locale.toString(), keyboardLayoutName);
final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype); final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype);
return keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
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);
} }
private KeyboardLayoutSet createKeyboardSetForSpellChecker(final InputMethodSubtype subtype) { private KeyboardLayoutSet createKeyboardSetForSpellChecker(final InputMethodSubtype subtype) {

View file

@ -28,8 +28,9 @@ import android.view.textservice.SuggestionsInfo;
import android.view.textservice.TextInfo; import android.view.textservice.TextInfo;
import com.android.inputmethod.compat.SuggestionsInfoCompatUtils; 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.Constants;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.PrevWordsInfo; import com.android.inputmethod.latin.PrevWordsInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.WordComposer; 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.LocaleUtils;
import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.ScriptUtils;
import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.StringUtils;
import com.android.inputmethod.latin.utils.SuggestionResults;
import java.util.ArrayList;
import java.util.Locale; import java.util.Locale;
public abstract class AndroidWordLevelSpellCheckerSession extends Session { public abstract class AndroidWordLevelSpellCheckerSession extends Session {
private static final String TAG = AndroidWordLevelSpellCheckerSession.class.getSimpleName(); private static final String TAG = AndroidWordLevelSpellCheckerSession.class.getSimpleName();
private static final boolean DBG = false; private static final boolean DBG = false;
// Immutable, but need the locale which is not available in the constructor yet // Immutable, but not available in the constructor.
private DictionaryPool mDictionaryPool;
// Likewise
private Locale mLocale; private Locale mLocale;
// Cache this for performance // Cache this for performance
private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now. private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
@ -116,7 +115,6 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
@Override @Override
public void onCreate() { public void onCreate() {
final String localeString = getLocale(); final String localeString = getLocale();
mDictionaryPool = mService.getDictionaryPool(localeString);
mLocale = LocaleUtils.constructLocaleFromString(localeString); mLocale = LocaleUtils.constructLocaleFromString(localeString);
mScript = ScriptUtils.getScriptFromSpellCheckerLocale(mLocale); 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 * 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". * version of it "text" and the capitalized version of it "Text".
*/ */
private boolean isInDictForAnyCapitalization(final Dictionary dict, final String text, private boolean isInDictForAnyCapitalization(final String text, final int capitalizeType) {
final int capitalizeType) {
// If the word is in there as is, then it's in the dictionary. If not, we'll test lower // 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. // 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 (StringUtils.CAPITALIZE_NONE == capitalizeType) return false;
// If we come here, we have a capitalized word (either First- or All-). // 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 // 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. // tested all possibilities, so if it's still negative we can return false.
final String lowerCaseText = text.toLowerCase(mLocale); 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 (StringUtils.CAPITALIZE_FIRST == capitalizeType) return false;
// If the lower case version is not in the dictionary, it's still possible // 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 // 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". // 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 // Note : this must be reentrant
@ -236,21 +234,13 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
return new SuggestionsInfo( return new SuggestionsInfo(
cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions); cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
} }
final int checkability = getCheckabilityInScript(inText, mScript); final int checkability = getCheckabilityInScript(inText, mScript);
if (CHECKABILITY_CHECKABLE != checkability) { 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) { if (CHECKABILITY_CONTAINS_PERIOD == checkability) {
final String[] splitText = inText.split(Constants.REGEXP_PERIOD); final String[] splitText = inText.split(Constants.REGEXP_PERIOD);
boolean allWordsAreValid = true; boolean allWordsAreValid = true;
for (final String word : splitText) { for (final String word : splitText) {
if (!dictInfo.mDictionary.isValidWord(word)) { if (!mService.isValidWord(mLocale, word)) {
allWordsAreValid = false; allWordsAreValid = false;
break; break;
} }
@ -259,23 +249,13 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO
| SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS, | SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS,
new String[] { new String[] {
TextUtils.join(Constants.STRING_SPACE, splitText), TextUtils.join(Constants.STRING_SPACE, splitText) });
TextUtils.join(Constants.STRING_PERIOD_AND_SPACE,
splitText) });
}
}
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");
}
} }
} }
return mService.isValidWord(mLocale, inText) ?
AndroidSpellCheckerService.getInDictEmptySuggestions() :
AndroidSpellCheckerService.getNotInDictEmptySuggestions(
CHECKABILITY_CONTAINS_PERIOD == checkability /* reportAsTypo */);
} }
final String text = inText.replaceAll( final String text = inText.replaceAll(
AndroidSpellCheckerService.APOSTROPHE, AndroidSpellCheckerService.SINGLE_QUOTE); AndroidSpellCheckerService.APOSTROPHE, AndroidSpellCheckerService.SINGLE_QUOTE);
@ -289,48 +269,34 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
final int capitalizeType = StringUtils.getCapitalizationType(text); final int capitalizeType = StringUtils.getCapitalizationType(text);
boolean isInDict = true; boolean isInDict = true;
DictAndKeyboard dictInfo = null; if (!mService.hasMainDictionaryForLocale(mLocale)) {
try {
dictInfo = mDictionaryPool.pollWithDefaultTimeout();
if (!DictionaryPool.isAValidDictionary(dictInfo)) {
return AndroidSpellCheckerService.getNotInDictEmptySuggestions( return AndroidSpellCheckerService.getNotInDictEmptySuggestions(
false /* reportAsTypo */); false /* reportAsTypo */);
} }
final Keyboard keyboard = mService.getKeyboardForLocale(mLocale);
final WordComposer composer = new WordComposer(); final WordComposer composer = new WordComposer();
final int[] codePoints = StringUtils.toCodePointArray(text); final int[] codePoints = StringUtils.toCodePointArray(text);
final int[] coordinates; final int[] coordinates;
if (null == dictInfo.mKeyboard) { final ProximityInfo proximityInfo;
if (null == keyboard) {
coordinates = CoordinateUtils.newCoordinateArray(codePoints.length, coordinates = CoordinateUtils.newCoordinateArray(codePoints.length,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
proximityInfo = null;
} else { } else {
coordinates = dictInfo.mKeyboard.getCoordinates(codePoints); coordinates = keyboard.getCoordinates(codePoints);
proximityInfo = keyboard.getProximityInfo();
} }
composer.setComposingWord(codePoints, coordinates); composer.setComposingWord(codePoints, coordinates);
// TODO: make a spell checker option to block offensive words or not final SuggestionResults suggestionResults = mService.getSuggestionResults(
final ArrayList<SuggestedWordInfo> suggestions = mLocale, composer, prevWordsInfo, proximityInfo);
dictInfo.mDictionary.getSuggestions(composer, prevWordsInfo, if (suggestionResults != null) {
dictInfo.getProximityInfo(), for (final SuggestedWordInfo suggestion : suggestionResults) {
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; final String suggestionStr = suggestion.mWord;
suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0, suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
suggestionStr.length(), suggestion.mScore); suggestionStr.length(), suggestion.mScore);
} }
} }
isInDict = isInDictForAnyCapitalization(dictInfo.mDictionary, text, capitalizeType); isInDict = isInDictForAnyCapitalization(text, capitalizeType);
} finally {
if (null != dictInfo) {
if (!mDictionaryPool.offer(dictInfo)) {
Log.e(TAG, "Can't re-insert a dictionary into its pool");
}
}
}
final SuggestionsGatherer.Result result = suggestionsGatherer.getResults( final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
capitalizeType, mLocale); capitalizeType, mLocale);

View file

@ -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();
}
}

View file

@ -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<DictAndKeyboard> {
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<SuggestedWordInfo> 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<SuggestedWordInfo> 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);
}
}
}

View file

@ -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<SuggestedWordInfo> 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);
}
}
}

View file

@ -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<SuggestedWordInfo> 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);
}
}
}