diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/spellcheck')
7 files changed, 588 insertions, 652 deletions
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 503b18b1b..90398deb2 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -16,40 +16,55 @@ 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.latin.BinaryDictionary; +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.SynchronouslyLoadedContactsBinaryDictionary; -import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary; +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; 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. @@ -58,61 +73,77 @@ 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<String, DictionaryPool> mDictionaryPools = CollectionUtils.newSynchronizedTreeMap(); - private Map<String, UserBinaryDictionary> 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<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". private float mRecommendedThreshold; // Whether to use the contacts dictionary private boolean mUseContactsDictionary; - private final Object mUseContactsLock = new Object(); - - private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList = - CollectionUtils.newHashSet(); + // 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 int SCRIPT_LATIN = 0; - public static final int SCRIPT_CYRILLIC = 1; - public static final int SCRIPT_GREEK = 2; public static final String SINGLE_QUOTE = "\u0027"; public static final String APOSTROPHE = "\u2019"; - private static final TreeMap<String, Integer> mLanguageToScript; - static { - // List of the supported languages and their associated script. We won't check - // words written in another script than the selected script, because we know we - // don't have those in our dictionary so we will underline everything and we - // will never have any suggestions, so it makes no sense checking them, and this - // is done in {@link #shouldFilterOut}. Also, the script is used to choose which - // proximity to pass to the dictionary descent algorithm. - // IMPORTANT: this only contains languages - do not write countries in there. - // Only the language is searched from the map. - mLanguageToScript = CollectionUtils.newTreeMap(); - mLanguageToScript.put("cs", SCRIPT_LATIN); - mLanguageToScript.put("da", SCRIPT_LATIN); - mLanguageToScript.put("de", SCRIPT_LATIN); - mLanguageToScript.put("el", SCRIPT_GREEK); - mLanguageToScript.put("en", SCRIPT_LATIN); - mLanguageToScript.put("es", SCRIPT_LATIN); - mLanguageToScript.put("fi", SCRIPT_LATIN); - mLanguageToScript.put("fr", SCRIPT_LATIN); - mLanguageToScript.put("hr", SCRIPT_LATIN); - mLanguageToScript.put("it", SCRIPT_LATIN); - mLanguageToScript.put("lt", SCRIPT_LATIN); - mLanguageToScript.put("lv", SCRIPT_LATIN); - mLanguageToScript.put("nb", SCRIPT_LATIN); - mLanguageToScript.put("nl", SCRIPT_LATIN); - mLanguageToScript.put("pt", SCRIPT_LATIN); - mLanguageToScript.put("sl", SCRIPT_LATIN); - mLanguageToScript.put("ru", SCRIPT_CYRILLIC); + + public AndroidSpellCheckerService() { + super(); + for (int i = 0; i < MAX_NUM_OF_THREADS_READ_DICTIONARY; i++) { + mSessionIdPool.add(i); + } } @Override public void onCreate() { @@ -124,22 +155,17 @@ public final class AndroidSpellCheckerService extends SpellCheckerService onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); } - public static int getScriptFromLocale(final Locale locale) { - final Integer script = mLanguageToScript.get(locale.getLanguage()); - if (null == script) { - throw new RuntimeException("We have been called with an unsupported language: \"" - + locale.getLanguage() + "\". Framework bug?"); - } - return script; + public float getRecommendedThreshold() { + return mRecommendedThreshold; } private static String getKeyboardLayoutNameForScript(final int script) { switch (script) { - case AndroidSpellCheckerService.SCRIPT_LATIN: + case ScriptUtils.SCRIPT_LATIN: return "qwerty"; - case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + case ScriptUtils.SCRIPT_CYRILLIC: return "east_slavic"; - case AndroidSpellCheckerService.SCRIPT_GREEK: + case ScriptUtils.SCRIPT_GREEK: return "greek"; default: throw new RuntimeException("Wrong script supplied: " + script); @@ -149,52 +175,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(); - } - } - } - - 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); + 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); + } } - } - contactsDict.close(); } @Override @@ -223,230 +218,114 @@ public final class AndroidSpellCheckerService extends SpellCheckerService EMPTY_STRING_ARRAY); } - public SuggestionsGatherer newSuggestionsGatherer(final String text, int maxLength) { - return new SuggestionsGatherer(text, mRecommendedThreshold, maxLength); + 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(); + } } - // TODO: remove this class and replace it by storage local to the session. - public static final class SuggestionsGatherer { - public static final class Result { - public final String[] mSuggestions; - public final boolean mHasRecommendedSuggestions; - public Result(final String[] gatheredSuggestions, - final boolean hasRecommendedSuggestions) { - mSuggestions = gatheredSuggestions; - mHasRecommendedSuggestions = hasRecommendedSuggestions; + 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(); } + } - private final ArrayList<String> mSuggestions; - private final int[] mScores; - private final String mOriginalText; - private final float mRecommendedThreshold; - 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; - mRecommendedThreshold = recommendedThreshold; - mMaxLength = maxLength; - mSuggestions = CollectionUtils.newArrayList(maxLength + 1); - mScores = new int[mMaxLength]; + public boolean hasMainDictionaryForLocale(final Locale locale) { + mSemaphore.acquireUninterruptibly(); + try { + final DictionaryFacilitator dictionaryFacilitator = + getDictionaryFacilitatorForLocaleLocked(locale); + return dictionaryFacilitator.hasInitializedMainDictionary(); + } finally { + mSemaphore.release(); } + } - synchronized public boolean addWord(char[] word, int[] spaceIndices, int wordOffset, - int wordLength, int score) { - final int positionIndex = Arrays.binarySearch(mScores, 0, mLength, score); - // binarySearch returns the index if the element exists, and -<insertion index> - 1 - // if it doesn't. See documentation for binarySearch. - final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1; - - 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; - } - if (insertIndex >= mMaxLength) { - // We found a suggestion, but its score is too weak to be kept considering - // the suggestion limit. - return true; - } - - final String wordString = new String(word, wordOffset, wordLength); - if (mLength < mMaxLength) { - final int copyLen = mLength - insertIndex; - ++mLength; - System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen); - mSuggestions.add(insertIndex, wordString); - } else { - System.arraycopy(mScores, 1, mScores, 0, insertIndex); - mSuggestions.add(insertIndex, wordString); - mSuggestions.remove(0); - } - mScores[insertIndex] = score; - - return true; + 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; + } - public Result getResults(final int capitalizeType, final Locale locale) { - 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; + 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 { - gatheredSuggestions = EMPTY_STRING_ARRAY; - final float normalizedScore = BinaryDictionary.calcNormalizedScore( - mOriginalText, mBestSuggestion, mBestScore); - hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); - } - } else { - if (DBG) { - if (mLength != mSuggestions.size()) { - Log.e(TAG, "Suggestion size is not the same as stored mLength"); - } - for (int i = mLength - 1; i >= 0; --i) { - Log.i(TAG, "" + mScores[i] + " " + mSuggestions.get(i)); - } - } - Collections.reverse(mSuggestions); - StringUtils.removeDupes(mSuggestions); - if (StringUtils.CAPITALIZE_ALL == capitalizeType) { - for (int i = 0; i < mSuggestions.size(); ++i) { - // get(i) returns a CharSequence which is actually a String so .toString() - // should return the same object. - mSuggestions.set(i, mSuggestions.get(i).toString().toUpperCase(locale)); - } - } else if (StringUtils.CAPITALIZE_FIRST == capitalizeType) { - for (int i = 0; i < mSuggestions.size(); ++i) { - // Likewise - mSuggestions.set(i, StringUtils.capitalizeFirstCodePoint( - mSuggestions.get(i).toString(), locale)); - } - } - // This returns a String[], while toArray() returns an Object[] which cannot be cast - // into a String[]. - gatheredSuggestions = mSuggestions.toArray(EMPTY_STRING_ARRAY); - - final int bestScore = mScores[mLength - 1]; - final String bestSuggestion = mSuggestions.get(0); - final float normalizedScore = - BinaryDictionary.calcNormalizedScore( - mOriginalText, bestSuggestion.toString(), bestScore); - hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); - if (DBG) { - Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore); - Log.i(TAG, "Normalized score = " + normalizedScore - + " (threshold " + mRecommendedThreshold - + ") => hasRecommendedSuggestions = " + hasRecommendedSuggestions); + Log.w(TAG, "Give up retrying. Retried " + + MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e); } } - return new Result(gatheredSuggestions, hasRecommendedSuggestions); } } @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<String, DictionaryPool> oldPools = mDictionaryPools; - mDictionaryPools = CollectionUtils.newSynchronizedTreeMap(); - final Map<String, UserBinaryDictionary> oldUserDictionaries = mUserDictionaries; - mUserDictionaries = CollectionUtils.newSynchronizedTreeMap(); - new Thread("spellchecker_close_dicts") { - @Override - public void run() { - 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) { - final int script = getScriptFromLocale(locale); + private Keyboard createKeyboardForLocale(final Locale locale) { + final int script = ScriptUtils.getScriptFromSpellCheckerLocale(locale); final String keyboardLayoutName = getKeyboardLayoutNameForScript(script); - final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype( - locale.toString(), keyboardLayoutName, null); + 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, localeStr, 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>(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/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java index ddda52d71..14ab2dbbf 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin.spellcheck; +import android.content.res.Resources; import android.os.Binder; import android.text.TextUtils; import android.util.Log; @@ -23,31 +24,35 @@ import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.compat.TextInfoCompatUtils; +import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.utils.StringUtils; import java.util.ArrayList; +import java.util.Locale; public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheckerSession { private static final String TAG = AndroidSpellCheckerSession.class.getSimpleName(); private static final boolean DBG = false; - private final static String[] EMPTY_STRING_ARRAY = new String[0]; + private final Resources mResources; + private SentenceLevelAdapter mSentenceLevelAdapter; public AndroidSpellCheckerSession(AndroidSpellCheckerService service) { super(service); + mResources = service.getResources(); } private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti, SentenceSuggestionsInfo ssi) { - final String typedText = ti.getText(); - if (!typedText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { + final CharSequence typedText = TextInfoCompatUtils.getCharSequenceOrString(ti); + if (!typedText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { return null; } final int N = ssi.getSuggestionsCount(); - final ArrayList<Integer> additionalOffsets = CollectionUtils.newArrayList(); - final ArrayList<Integer> additionalLengths = CollectionUtils.newArrayList(); - final ArrayList<SuggestionsInfo> additionalSuggestionsInfos = - CollectionUtils.newArrayList(); - String currentWord = null; + final ArrayList<Integer> additionalOffsets = new ArrayList<>(); + final ArrayList<Integer> additionalLengths = new ArrayList<>(); + final ArrayList<SuggestionsInfo> additionalSuggestionsInfos = new ArrayList<>(); + CharSequence currentWord = null; for (int i = 0; i < N; ++i) { final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i); final int flags = si.getSuggestionsAttributes(); @@ -56,31 +61,33 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck } final int offset = ssi.getOffsetAt(i); final int length = ssi.getLengthAt(i); - final String subText = typedText.substring(offset, offset + length); - final String prevWord = currentWord; + final CharSequence subText = typedText.subSequence(offset, offset + length); + final PrevWordsInfo prevWordsInfo = + new PrevWordsInfo(new PrevWordsInfo.WordInfo(currentWord)); currentWord = subText; - if (!subText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { + if (!subText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { continue; } - final String[] splitTexts = - subText.split(AndroidSpellCheckerService.SINGLE_QUOTE, -1); + final CharSequence[] splitTexts = StringUtils.split(subText, + AndroidSpellCheckerService.SINGLE_QUOTE, + true /* preserveTrailingEmptySegments */ ); if (splitTexts == null || splitTexts.length <= 1) { continue; } final int splitNum = splitTexts.length; for (int j = 0; j < splitNum; ++j) { - final String splitText = splitTexts[j]; + final CharSequence splitText = splitTexts[j]; if (TextUtils.isEmpty(splitText)) { continue; } - if (mSuggestionsCache.getSuggestionsFromCache(splitText, prevWord) == null) { + if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString(), prevWordsInfo) + == null) { continue; } final int newLength = splitText.length(); // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO final int newFlags = 0; - final SuggestionsInfo newSi = - new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY); + final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY); newSi.setCookieAndSequence(si.getCookie(), si.getSequence()); if (DBG) { Log.d(TAG, "Override and remove old span over: " + splitText + ", " @@ -116,8 +123,7 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck @Override public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { - final SentenceSuggestionsInfo[] retval = - super.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit); + final SentenceSuggestionsInfo[] retval = splitAndSuggest(textInfos, suggestionsLimit); if (retval == null || retval.length != textInfos.length) { return retval; } @@ -131,6 +137,58 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck return retval; } + /** + * Get sentence suggestions for specified texts in an array of TextInfo. This is taken from + * SpellCheckerService#onGetSentenceSuggestionsMultiple that we can't use because it's + * using private variables. + * The default implementation splits the input text to words and returns + * {@link SentenceSuggestionsInfo} which contains suggestions for each word. + * This function will run on the incoming IPC thread. + * So, this is not called on the main thread, + * but will be called in series on another thread. + * @param textInfos an array of the text metadata + * @param suggestionsLimit the maximum number of suggestions to be returned + * @return an array of {@link SentenceSuggestionsInfo} returned by + * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} + */ + private SentenceSuggestionsInfo[] splitAndSuggest(TextInfo[] textInfos, int suggestionsLimit) { + if (textInfos == null || textInfos.length == 0) { + return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; + } + SentenceLevelAdapter sentenceLevelAdapter; + synchronized(this) { + sentenceLevelAdapter = mSentenceLevelAdapter; + if (sentenceLevelAdapter == null) { + final String localeStr = getLocale(); + if (!TextUtils.isEmpty(localeStr)) { + sentenceLevelAdapter = new SentenceLevelAdapter(mResources, + new Locale(localeStr)); + mSentenceLevelAdapter = sentenceLevelAdapter; + } + } + } + if (sentenceLevelAdapter == null) { + return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; + } + final int infosSize = textInfos.length; + final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[infosSize]; + for (int i = 0; i < infosSize; ++i) { + final SentenceLevelAdapter.SentenceTextInfoParams textInfoParams = + sentenceLevelAdapter.getSplitWords(textInfos[i]); + final ArrayList<SentenceLevelAdapter.SentenceWordItem> mItems = + textInfoParams.mItems; + final int itemsSize = mItems.size(); + final TextInfo[] splitTextInfos = new TextInfo[itemsSize]; + for (int j = 0; j < itemsSize; ++j) { + splitTextInfos[j] = mItems.get(j).mTextInfo; + } + retval[i] = SentenceLevelAdapter.reconstructSuggestions( + textInfoParams, onGetSuggestionsMultiple( + splitTextInfos, suggestionsLimit, true)); + } + return retval; + } + @Override public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { @@ -139,18 +197,22 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck final int length = textInfos.length; final SuggestionsInfo[] retval = new SuggestionsInfo[length]; for (int i = 0; i < length; ++i) { - final String prevWord; + final CharSequence prevWord; if (sequentialWords && i > 0) { - final String prevWordCandidate = textInfos[i - 1].getText(); - // Note that an empty string would be used to indicate the initial word - // in the future. - prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate; + final TextInfo prevTextInfo = textInfos[i - 1]; + final CharSequence prevWordCandidate = + TextInfoCompatUtils.getCharSequenceOrString(prevTextInfo); + // Note that an empty string would be used to indicate the initial word + // in the future. + prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate; } else { prevWord = null; } - retval[i] = onGetSuggestionsInternal(textInfos[i], prevWord, suggestionsLimit); - retval[i].setCookieAndSequence(textInfos[i].getCookie(), - textInfos[i].getSequence()); + final PrevWordsInfo prevWordsInfo = + new PrevWordsInfo(new PrevWordsInfo.WordInfo(prevWord)); + final TextInfo textInfo = textInfos[i]; + retval[i] = onGetSuggestionsInternal(textInfo, prevWordsInfo, suggestionsLimit); + retval[i].setCookieAndSequence(textInfo.getCookie(), textInfo.getSequence()); } return retval; } finally { diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index d6e5b75ad..d668672aa 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -28,13 +28,18 @@ 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; -import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService.SuggestionsGatherer; +import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; +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; @@ -43,9 +48,9 @@ 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 + public final static String[] EMPTY_STRING_ARRAY = new String[0]; + + // 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. @@ -66,29 +71,29 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { private static final char CHAR_DELIMITER = '\uFFFC'; private static final int MAX_CACHE_SIZE = 50; private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache = - new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE); + new LruCache<>(MAX_CACHE_SIZE); // TODO: Support n-gram input - private static String generateKey(String query, String prevWord) { - if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWord)) { + private static String generateKey(final String query, final PrevWordsInfo prevWordsInfo) { + if (TextUtils.isEmpty(query) || !prevWordsInfo.isValid()) { return query; } - return query + CHAR_DELIMITER + prevWord; + return query + CHAR_DELIMITER + prevWordsInfo; } - // TODO: Support n-gram input - public SuggestionsParams getSuggestionsFromCache(String query, String prevWord) { - return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWord)); + public SuggestionsParams getSuggestionsFromCache(String query, + final PrevWordsInfo prevWordsInfo) { + return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWordsInfo)); } - // TODO: Support n-gram input public void putSuggestionsToCache( - String query, String prevWord, String[] suggestions, int flags) { + final String query, final PrevWordsInfo prevWordsInfo, + final String[] suggestions, final int flags) { if (suggestions == null || TextUtils.isEmpty(query)) { return; } mUnigramSuggestionsInfoCache.put( - generateKey(query, prevWord), new SuggestionsParams(suggestions, flags)); + generateKey(query, prevWordsInfo), new SuggestionsParams(suggestions, flags)); } public void clearCache() { @@ -112,9 +117,8 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { @Override public void onCreate() { final String localeString = getLocale(); - mDictionaryPool = mService.getDictionaryPool(localeString); mLocale = LocaleUtils.constructLocaleFromString(localeString); - mScript = AndroidSpellCheckerService.getScriptFromLocale(mLocale); + mScript = ScriptUtils.getScriptFromSpellCheckerLocale(mLocale); } @Override @@ -123,44 +127,6 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { cres.unregisterContentObserver(mObserver); } - /* - * Returns whether the code point is a letter that makes sense for the specified - * locale for this spell checker. - * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml - * and is limited to EFIGS languages and Russian. - * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters - * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters. - */ - private static boolean isLetterCheckableByLanguage(final int codePoint, - final int script) { - switch (script) { - case AndroidSpellCheckerService.SCRIPT_LATIN: - // Our supported latin script dictionaries (EFIGS) at the moment only include - // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode - // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF, - // so the below is a very efficient way to test for it. As for the 0-0x3F, it's - // excluded from isLetter anyway. - return codePoint <= 0x2AF && Character.isLetter(codePoint); - case AndroidSpellCheckerService.SCRIPT_CYRILLIC: - // All Cyrillic characters are in the 400~52F block. There are some in the upper - // Unicode range, but they are archaic characters that are not used in modern - // Russian and are not used by our dictionary. - return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint); - case AndroidSpellCheckerService.SCRIPT_GREEK: - // Greek letters are either in the 370~3FF range (Greek & Coptic), or in the - // 1F00~1FFF range (Greek extended). Our dictionary contains both sort of characters. - // Our dictionary also contains a few words with 0xF2; it would be best to check - // if that's correct, but a web search does return results for these words so - // they are probably okay. - return (codePoint >= 0x370 && codePoint <= 0x3FF) - || (codePoint >= 0x1F00 && codePoint <= 0x1FFF) - || codePoint == 0xF2; - default: - // Should never come here - throw new RuntimeException("Impossible value of script: " + script); - } - } - private static final int CHECKABILITY_CHECKABLE = 0; private static final int CHECKABILITY_TOO_MANY_NON_LETTERS = 1; private static final int CHECKABILITY_CONTAINS_PERIOD = 2; @@ -187,7 +153,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { // Filter by first letter final int firstCodePoint = text.codePointAt(0); // Filter out words that don't start with a letter or an apostrophe - if (!isLetterCheckableByLanguage(firstCodePoint, script) + if (!ScriptUtils.isLetterPartOfScript(firstCodePoint, script) && '\'' != firstCodePoint) return CHECKABILITY_FIRST_LETTER_UNCHECKABLE; // Filter contents @@ -208,7 +174,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { if (Constants.CODE_PERIOD == codePoint) { return CHECKABILITY_CONTAINS_PERIOD; } - if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount; + if (ScriptUtils.isLetterPartOfScript(codePoint, script)) ++letterCount; } // Guestimate heuristic: perform spell checking if at least 3/4 of the characters // in this word are letters @@ -225,24 +191,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 @@ -257,11 +223,12 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { } protected SuggestionsInfo onGetSuggestionsInternal( - final TextInfo textInfo, final String prevWord, final int suggestionsLimit) { + final TextInfo textInfo, final PrevWordsInfo prevWordsInfo, + final int suggestionsLimit) { try { final String inText = textInfo.getText(); final SuggestionsParams cachedSuggestionsParams = - mSuggestionsCache.getSuggestionsFromCache(inText, prevWord); + mSuggestionsCache.getSuggestionsFromCache(inText, prevWordsInfo); if (cachedSuggestionsParams != null) { if (DBG) { Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags); @@ -269,78 +236,57 @@ 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 */); - } - 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 (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; } } + 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); - - // TODO: Don't gather suggestions if the limit is <= 0 unless necessary - //final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text, - //mService.mSuggestionThreshold, mService.mRecommendedThreshold, - //suggestionsLimit); - final SuggestionsGatherer suggestionsGatherer = mService.newSuggestionsGatherer( - text, suggestionsLimit); - 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 length = text.length(); - for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { - final int codePoint = text.codePointAt(i); - composer.addKeyInfo(codePoint, dictInfo.getKeyboard(codePoint)); - } - // TODO: make a spell checker option to block offensive words or not - final ArrayList<SuggestedWordInfo> suggestions = - dictInfo.mDictionary.getSuggestions(composer, prevWord, - dictInfo.getProximityInfo(), true /* blockOffensiveWords */, - null /* additionalFeaturesOptions */); - 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 SuggestionsGatherer.Result result = suggestionsGatherer.getResults( - capitalizeType, mLocale); - + 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); + // TODO: Don't gather suggestions if the limit is <= 0 unless necessary + final SuggestionResults suggestionResults = mService.getSuggestionResults( + mLocale, composer, prevWordsInfo, proximityInfo); + final Result result = getResult(capitalizeType, mLocale, suggestionsLimit, + mService.getRecommendedThreshold(), text, suggestionResults); + isInDict = isInDictForAnyCapitalization(text, capitalizeType); if (DBG) { Log.i(TAG, "Spell checking results for " + text + " with suggestion limit " + suggestionsLimit); @@ -362,7 +308,8 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() : 0); final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions); - mSuggestionsCache.putSuggestionsToCache(text, prevWord, result.mSuggestions, flags); + mSuggestionsCache.putSuggestionsToCache(text, prevWordsInfo, result.mSuggestions, + flags); return retval; } catch (RuntimeException e) { // Don't kill the keyboard if there is a bug in the spell checker @@ -376,6 +323,62 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { } } + private static final class Result { + public final String[] mSuggestions; + public final boolean mHasRecommendedSuggestions; + public Result(final String[] gatheredSuggestions, + final boolean hasRecommendedSuggestions) { + mSuggestions = gatheredSuggestions; + mHasRecommendedSuggestions = hasRecommendedSuggestions; + } + } + + private static Result getResult(final int capitalizeType, final Locale locale, + final int suggestionsLimit, final float recommendedThreshold, final String originalText, + final SuggestionResults suggestionResults) { + if (suggestionResults.isEmpty() || suggestionsLimit <= 0) { + return new Result(null /* gatheredSuggestions */, + false /* hasRecommendedSuggestions */); + } + if (DBG) { + for (final SuggestedWordInfo suggestedWordInfo : suggestionResults) { + Log.i(TAG, "" + suggestedWordInfo.mScore + " " + suggestedWordInfo.mWord); + } + } + final ArrayList<String> suggestions = new ArrayList<>(); + for (final SuggestedWordInfo suggestedWordInfo : suggestionResults) { + final String suggestion; + if (StringUtils.CAPITALIZE_ALL == capitalizeType) { + suggestion = suggestedWordInfo.mWord.toUpperCase(locale); + } else if (StringUtils.CAPITALIZE_FIRST == capitalizeType) { + suggestion = StringUtils.capitalizeFirstCodePoint( + suggestedWordInfo.mWord, locale); + } else { + suggestion = suggestedWordInfo.mWord; + } + suggestions.add(suggestion); + } + StringUtils.removeDupes(suggestions); + // This returns a String[], while toArray() returns an Object[] which cannot be cast + // into a String[]. + final String[] gatheredSuggestions = + suggestions.subList(0, Math.min(suggestions.size(), suggestionsLimit)) + .toArray(EMPTY_STRING_ARRAY); + + final int bestScore = suggestionResults.first().mScore; + final String bestSuggestion = suggestions.get(0); + final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( + originalText, bestSuggestion.toString(), bestScore); + final boolean hasRecommendedSuggestions = (normalizedScore > recommendedThreshold); + if (DBG) { + Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore); + Log.i(TAG, "Normalized score = " + normalizedScore + + " (threshold " + recommendedThreshold + + ") => hasRecommendedSuggestions = " + hasRecommendedSuggestions); + } + return new Result(gatheredSuggestions, hasRecommendedSuggestions); + } + /* * The spell checker acts on its own behalf. That is needed, in particular, to be able to * access the dictionary files, which the provider restricts to the identity of Latin IME. 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 b77f3e2c5..000000000 --- a/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java +++ /dev/null @@ -1,56 +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.latin.Dictionary; -import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.keyboard.KeyboardId; -import com.android.inputmethod.keyboard.KeyboardLayoutSet; -import com.android.inputmethod.keyboard.ProximityInfo; - -/** - * A container for a Dictionary and a Keyboard. - */ -public final class DictAndKeyboard { - public final Dictionary mDictionary; - private final Keyboard mKeyboard; - private final Keyboard mManualShiftedKeyboard; - - public DictAndKeyboard( - final Dictionary dictionary, final KeyboardLayoutSet keyboardLayoutSet) { - mDictionary = dictionary; - if (keyboardLayoutSet == null) { - mKeyboard = null; - mManualShiftedKeyboard = null; - return; - } - mKeyboard = keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); - mManualShiftedKeyboard = - keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED); - } - - public Keyboard getKeyboard(final int codePoint) { - if (mKeyboard == null) { - return null; - } - return mKeyboard.getKey(codePoint) != null ? mKeyboard : mManualShiftedKeyboard; - } - - 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 a0aed2829..000000000 --- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java +++ /dev/null @@ -1,134 +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.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.WordComposer; -import com.android.inputmethod.latin.utils.CollectionUtils; - -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 = CollectionUtils.newArrayList(); - private final static DictAndKeyboard dummyDict = new DictAndKeyboard( - new Dictionary(Dictionary.TYPE_MAIN) { - @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - return noSuggestions; - } - @Override - public boolean isValidWord(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/SentenceLevelAdapter.java b/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java new file mode 100644 index 000000000..ae582ea25 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java @@ -0,0 +1,188 @@ +/* + * 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.latin.spellcheck; + +import android.content.res.Resources; +import android.view.textservice.SentenceSuggestionsInfo; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; + +import com.android.inputmethod.compat.TextInfoCompatUtils; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.utils.RunInLocale; + +import java.util.ArrayList; +import java.util.Locale; + +/** + * This code is mostly lifted directly from android.service.textservice.SpellCheckerService in + * the framework; maybe that should be protected instead, so that implementers don't have to + * rewrite everything for any small change. + */ +public class SentenceLevelAdapter { + public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = + new SentenceSuggestionsInfo[] {}; + private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); + /** + * Container for split TextInfo parameters + */ + public static class SentenceWordItem { + public final TextInfo mTextInfo; + public final int mStart; + public final int mLength; + public SentenceWordItem(TextInfo ti, int start, int end) { + mTextInfo = ti; + mStart = start; + mLength = end - start; + } + } + + /** + * Container for originally queried TextInfo and parameters + */ + public static class SentenceTextInfoParams { + final TextInfo mOriginalTextInfo; + final ArrayList<SentenceWordItem> mItems; + final int mSize; + public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) { + mOriginalTextInfo = ti; + mItems = items; + mSize = items.size(); + } + } + + private static class WordIterator { + private final SpacingAndPunctuations mSpacingAndPunctuations; + public WordIterator(final Resources res, final Locale locale) { + final RunInLocale<SpacingAndPunctuations> job + = new RunInLocale<SpacingAndPunctuations>() { + @Override + protected SpacingAndPunctuations job(final Resources res) { + return new SpacingAndPunctuations(res); + } + }; + mSpacingAndPunctuations = job.runInLocale(res, locale); + } + + public int getEndOfWord(final CharSequence sequence, int index) { + final int length = sequence.length(); + index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1); + while (index < length) { + final int codePoint = Character.codePointAt(sequence, index); + if (mSpacingAndPunctuations.isWordSeparator(codePoint)) { + // If it's a period, we want to stop here only if it's followed by another + // word separator. In all other cases we stop here. + if (Constants.CODE_PERIOD == codePoint) { + final int indexOfNextCodePoint = + index + Character.charCount(Constants.CODE_PERIOD); + if (indexOfNextCodePoint < length + && mSpacingAndPunctuations.isWordSeparator( + Character.codePointAt(sequence, indexOfNextCodePoint))) { + return index; + } + } else { + return index; + } + } + index += Character.charCount(codePoint); + } + return index; + } + + public int getBeginningOfNextWord(final CharSequence sequence, int index) { + final int length = sequence.length(); + if (index >= length) { + return -1; + } + index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1); + while (index < length) { + final int codePoint = Character.codePointAt(sequence, index); + if (!mSpacingAndPunctuations.isWordSeparator(codePoint)) { + return index; + } + index += Character.charCount(codePoint); + } + return -1; + } + } + + private final WordIterator mWordIterator; + public SentenceLevelAdapter(final Resources res, final Locale locale) { + mWordIterator = new WordIterator(res, locale); + } + + public SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { + final WordIterator wordIterator = mWordIterator; + final CharSequence originalText = + TextInfoCompatUtils.getCharSequenceOrString(originalTextInfo); + final int cookie = originalTextInfo.getCookie(); + final int start = -1; + final int end = originalText.length(); + final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>(); + int wordStart = wordIterator.getBeginningOfNextWord(originalText, start); + int wordEnd = wordIterator.getEndOfWord(originalText, wordStart); + while (wordStart <= end && wordEnd != -1 && wordStart != -1) { + if (wordEnd >= start && wordEnd > wordStart) { + CharSequence subSequence = originalText.subSequence(wordStart, wordEnd).toString(); + final TextInfo ti = TextInfoCompatUtils.newInstance(subSequence, 0, + subSequence.length(), cookie, subSequence.hashCode()); + wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); + } + wordStart = wordIterator.getBeginningOfNextWord(originalText, wordEnd); + if (wordStart == -1) { + break; + } + wordEnd = wordIterator.getEndOfWord(originalText, wordStart); + } + return new SentenceTextInfoParams(originalTextInfo, wordItems); + } + + public static SentenceSuggestionsInfo reconstructSuggestions( + SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { + if (results == null || results.length == 0) { + return null; + } + if (originalTextInfoParams == null) { + return null; + } + final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); + final int originalSequence = + originalTextInfoParams.mOriginalTextInfo.getSequence(); + + final int querySize = originalTextInfoParams.mSize; + final int[] offsets = new int[querySize]; + final int[] lengths = new int[querySize]; + final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; + for (int i = 0; i < querySize; ++i) { + final SentenceWordItem item = originalTextInfoParams.mItems.get(i); + SuggestionsInfo result = null; + for (int j = 0; j < results.length; ++j) { + final SuggestionsInfo cur = results[j]; + if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { + result = cur; + result.setCookieAndSequence(originalCookie, originalSequence); + break; + } + } + offsets[i] = item.mStart; + lengths[i] = item.mLength; + reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; + } + return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); + } +} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java index 999ca775b..6850e9b58 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java @@ -21,26 +21,20 @@ import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.settings.TwoStatePreferenceHelper; import com.android.inputmethod.latin.utils.ApplicationUtils; /** * Preference screen. */ public final class SpellCheckerSettingsFragment extends PreferenceFragment { - /** - * Empty constructor for fragment generation. - */ - public SpellCheckerSettingsFragment() { - } - @Override - public void onActivityCreated(Bundle savedInstanceState) { + public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); addPreferencesFromResource(R.xml.spell_checker_settings); final PreferenceScreen preferenceScreen = getPreferenceScreen(); - if (preferenceScreen != null) { - preferenceScreen.setTitle(ApplicationUtils.getAcitivityTitleResId( - getActivity(), SpellCheckerSettingsActivity.class)); - } + preferenceScreen.setTitle(ApplicationUtils.getActivityTitleResId( + getActivity(), SpellCheckerSettingsActivity.class)); + TwoStatePreferenceHelper.replaceCheckBoxPreferencesBySwitchPreferences(preferenceScreen); } } |