diff options
author | 2024-12-16 21:45:41 -0500 | |
---|---|---|
committer | 2025-01-11 14:17:35 -0500 | |
commit | e9a0e66716dab4dd3184d009d8920de1961efdfa (patch) | |
tree | 02dcc096643d74645bf28459c2834c3d4a2ad7f2 /java/src/org/kelar/inputmethod/latin/spellcheck | |
parent | fb3b9360d70596d7e921de8bf7d3ca99564a077e (diff) | |
download | latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.gz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.xz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.zip |
Rename to Kelar Keyboard (org.kelar.inputmethod.latin)
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/spellcheck')
7 files changed, 1232 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java new file mode 100644 index 000000000..fb53b92d7 --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -0,0 +1,244 @@ +/* + * 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 org.kelar.inputmethod.latin.spellcheck; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.service.textservice.SpellCheckerService; +import android.text.InputType; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodSubtype; +import android.view.textservice.SuggestionsInfo; + +import org.kelar.inputmethod.keyboard.Keyboard; +import org.kelar.inputmethod.keyboard.KeyboardId; +import org.kelar.inputmethod.keyboard.KeyboardLayoutSet; +import org.kelar.inputmethod.latin.DictionaryFacilitator; +import org.kelar.inputmethod.latin.DictionaryFacilitatorLruCache; +import org.kelar.inputmethod.latin.NgramContext; +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.RichInputMethodSubtype; +import org.kelar.inputmethod.latin.SuggestedWords; +import org.kelar.inputmethod.latin.common.ComposedData; +import org.kelar.inputmethod.latin.settings.SettingsValuesForSuggestion; +import org.kelar.inputmethod.latin.utils.AdditionalSubtypeUtils; +import org.kelar.inputmethod.latin.utils.ScriptUtils; +import org.kelar.inputmethod.latin.utils.SuggestionResults; + +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Semaphore; + +import javax.annotation.Nonnull; + +/** + * Service for spell checking, using LatinIME's dictionaries and mechanisms. + */ +public final class AndroidSpellCheckerService extends SpellCheckerService + implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = AndroidSpellCheckerService.class.getSimpleName(); + private static final boolean DEBUG = false; + + 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 = 301; + + private static final String DICTIONARY_NAME_PREFIX = "spellcheck_"; + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + 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 final DictionaryFacilitatorLruCache mDictionaryFacilitatorCache = + new DictionaryFacilitatorLruCache(this /* context */, DICTIONARY_NAME_PREFIX); + private final ConcurrentHashMap<Locale, Keyboard> mKeyboardCache = new ConcurrentHashMap<>(); + + // The threshold for a suggestion to be considered "recommended". + private float mRecommendedThreshold; + // TODO: make a spell checker option to block offensive words or not + private final SettingsValuesForSuggestion mSettingsValuesForSuggestion = + new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */); + + public static final String SINGLE_QUOTE = "\u0027"; + public static final String APOSTROPHE = "\u2019"; + + public AndroidSpellCheckerService() { + super(); + for (int i = 0; i < MAX_NUM_OF_THREADS_READ_DICTIONARY; i++) { + mSessionIdPool.add(i); + } + } + + @Override + public void onCreate() { + super.onCreate(); + mRecommendedThreshold = Float.parseFloat( + getString(R.string.spellchecker_recommended_threshold_value)); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); + } + + public float getRecommendedThreshold() { + return mRecommendedThreshold; + } + + private static String getKeyboardLayoutNameForLocale(final Locale locale) { + // See b/19963288. + if (locale.getLanguage().equals("sr")) { + return "south_slavic"; + } + final int script = ScriptUtils.getScriptFromSpellCheckerLocale(locale); + switch (script) { + case ScriptUtils.SCRIPT_LATIN: + return "qwerty"; + case ScriptUtils.SCRIPT_CYRILLIC: + return "east_slavic"; + case ScriptUtils.SCRIPT_GREEK: + return "greek"; + case ScriptUtils.SCRIPT_HEBREW: + return "hebrew"; + default: + throw new RuntimeException("Wrong script supplied: " + script); + } + } + + @Override + public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { + if (!PREF_USE_CONTACTS_KEY.equals(key)) return; + final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); + mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary); + } + + @Override + public Session createSession() { + // Should not refer to AndroidSpellCheckerSession directly considering + // that AndroidSpellCheckerSession may be overlaid. + return AndroidSpellCheckerSessionFactory.newInstance(this); + } + + /** + * Returns an empty SuggestionsInfo with flags signaling the word is not in the dictionary. + * @param reportAsTypo whether this should include the flag LOOKS_LIKE_TYPO, for red underline. + * @return the empty SuggestionsInfo with the appropriate flags set. + */ + public static SuggestionsInfo getNotInDictEmptySuggestions(final boolean reportAsTypo) { + return new SuggestionsInfo(reportAsTypo ? SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO : 0, + EMPTY_STRING_ARRAY); + } + + /** + * Returns an empty suggestionInfo with flags signaling the word is in the dictionary. + * @return the empty SuggestionsInfo with the appropriate flags set. + */ + public static SuggestionsInfo getInDictEmptySuggestions() { + return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY, + EMPTY_STRING_ARRAY); + } + + public boolean isValidWord(final Locale locale, final String word) { + mSemaphore.acquireUninterruptibly(); + try { + DictionaryFacilitator dictionaryFacilitatorForLocale = + mDictionaryFacilitatorCache.get(locale); + return dictionaryFacilitatorForLocale.isValidSpellingWord(word); + } finally { + mSemaphore.release(); + } + } + + public SuggestionResults getSuggestionResults(final Locale locale, + final ComposedData composedData, final NgramContext ngramContext, + @Nonnull final Keyboard keyboard) { + Integer sessionId = null; + mSemaphore.acquireUninterruptibly(); + try { + sessionId = mSessionIdPool.poll(); + DictionaryFacilitator dictionaryFacilitatorForLocale = + mDictionaryFacilitatorCache.get(locale); + return dictionaryFacilitatorForLocale.getSuggestionResults(composedData, ngramContext, + keyboard, mSettingsValuesForSuggestion, + sessionId, SuggestedWords.INPUT_STYLE_TYPING); + } finally { + if (sessionId != null) { + mSessionIdPool.add(sessionId); + } + mSemaphore.release(); + } + } + + public boolean hasMainDictionaryForLocale(final Locale locale) { + mSemaphore.acquireUninterruptibly(); + try { + final DictionaryFacilitator dictionaryFacilitator = + mDictionaryFacilitatorCache.get(locale); + return dictionaryFacilitator.hasAtLeastOneInitializedMainDictionary(); + } finally { + mSemaphore.release(); + } + } + + @Override + public boolean onUnbind(final Intent intent) { + mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); + try { + mDictionaryFacilitatorCache.closeDictionaries(); + } finally { + mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); + } + mKeyboardCache.clear(); + return false; + } + + public Keyboard getKeyboardForLocale(final Locale locale) { + Keyboard keyboard = mKeyboardCache.get(locale); + if (keyboard == null) { + keyboard = createKeyboardForLocale(locale); + if (keyboard != null) { + mKeyboardCache.put(locale, keyboard); + } + } + return keyboard; + } + + private Keyboard createKeyboardForLocale(final Locale locale) { + final String keyboardLayoutName = getKeyboardLayoutNameForLocale(locale); + final InputMethodSubtype subtype = AdditionalSubtypeUtils.createDummyAdditionalSubtype( + locale.toString(), keyboardLayoutName); + final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype); + return keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); + } + + private KeyboardLayoutSet createKeyboardSetForSpellChecker(final InputMethodSubtype subtype) { + final EditorInfo editorInfo = new EditorInfo(); + editorInfo.inputType = InputType.TYPE_CLASS_TEXT; + final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(this, editorInfo); + builder.setKeyboardGeometry( + SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT); + builder.setSubtype(RichInputMethodSubtype.getRichInputMethodSubtype(subtype)); + builder.setIsSpellChecker(true /* isSpellChecker */); + builder.disableTouchPositionCorrectionData(); + return builder.build(); + } +} diff --git a/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java new file mode 100644 index 000000000..3ab5138bf --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java @@ -0,0 +1,225 @@ +/* + * 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 org.kelar.inputmethod.latin.spellcheck; + +import android.annotation.TargetApi; +import android.content.res.Resources; +import android.os.Binder; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; +import android.view.textservice.SentenceSuggestionsInfo; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; + +import org.kelar.inputmethod.compat.TextInfoCompatUtils; +import org.kelar.inputmethod.latin.NgramContext; +import org.kelar.inputmethod.latin.utils.SpannableStringUtils; + +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 Resources mResources; + private SentenceLevelAdapter mSentenceLevelAdapter; + + public AndroidSpellCheckerSession(AndroidSpellCheckerService service) { + super(service); + mResources = service.getResources(); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti, + SentenceSuggestionsInfo ssi) { + final CharSequence typedText = TextInfoCompatUtils.getCharSequenceOrString(ti); + if (!typedText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { + return null; + } + final int N = ssi.getSuggestionsCount(); + 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(); + if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) { + continue; + } + final int offset = ssi.getOffsetAt(i); + final int length = ssi.getLengthAt(i); + final CharSequence subText = typedText.subSequence(offset, offset + length); + final NgramContext ngramContext = + new NgramContext(new NgramContext.WordInfo(currentWord)); + currentWord = subText; + if (!subText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { + continue; + } + // Split preserving spans. + final CharSequence[] splitTexts = SpannableStringUtils.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 CharSequence splitText = splitTexts[j]; + if (TextUtils.isEmpty(splitText)) { + continue; + } + if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString()) == 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); + newSi.setCookieAndSequence(si.getCookie(), si.getSequence()); + if (DBG) { + Log.d(TAG, "Override and remove old span over: " + splitText + ", " + + offset + "," + newLength); + } + additionalOffsets.add(offset); + additionalLengths.add(newLength); + additionalSuggestionsInfos.add(newSi); + } + } + final int additionalSize = additionalOffsets.size(); + if (additionalSize <= 0) { + return null; + } + final int suggestionsSize = N + additionalSize; + final int[] newOffsets = new int[suggestionsSize]; + final int[] newLengths = new int[suggestionsSize]; + final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize]; + int i; + for (i = 0; i < N; ++i) { + newOffsets[i] = ssi.getOffsetAt(i); + newLengths[i] = ssi.getLengthAt(i); + newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i); + } + for (; i < suggestionsSize; ++i) { + newOffsets[i] = additionalOffsets.get(i - N); + newLengths[i] = additionalLengths.get(i - N); + newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N); + } + return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths); + } + + @Override + public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, + int suggestionsLimit) { + final SentenceSuggestionsInfo[] retval = splitAndSuggest(textInfos, suggestionsLimit); + if (retval == null || retval.length != textInfos.length) { + return retval; + } + for (int i = 0; i < retval.length; ++i) { + final SentenceSuggestionsInfo tempSsi = + fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]); + if (tempSsi != null) { + retval[i] = tempSsi; + } + } + 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 android.service.textservice.SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} + */ + private SentenceSuggestionsInfo[] splitAndSuggest(TextInfo[] textInfos, int suggestionsLimit) { + if (textInfos == null || textInfos.length == 0) { + return SentenceLevelAdapter.getEmptySentenceSuggestionsInfo(); + } + 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.getEmptySentenceSuggestionsInfo(); + } + 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) { + long ident = Binder.clearCallingIdentity(); + try { + final int length = textInfos.length; + final SuggestionsInfo[] retval = new SuggestionsInfo[length]; + for (int i = 0; i < length; ++i) { + final CharSequence prevWord; + if (sequentialWords && i > 0) { + 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; + } + final NgramContext ngramContext = + new NgramContext(new NgramContext.WordInfo(prevWord)); + final TextInfo textInfo = textInfos[i]; + retval[i] = onGetSuggestionsInternal(textInfo, ngramContext, suggestionsLimit); + retval[i].setCookieAndSequence(textInfo.getCookie(), textInfo.getSequence()); + } + return retval; + } finally { + Binder.restoreCallingIdentity(ident); + } + } +} diff --git a/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java b/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java new file mode 100644 index 000000000..9463a8fad --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java @@ -0,0 +1,25 @@ +/* + * 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 org.kelar.inputmethod.latin.spellcheck; + +import android.service.textservice.SpellCheckerService.Session; + +public abstract class AndroidSpellCheckerSessionFactory { + public static Session newInstance(AndroidSpellCheckerService service) { + return new AndroidSpellCheckerSession(service); + } +} diff --git a/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java new file mode 100644 index 000000000..2f1fc868b --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -0,0 +1,390 @@ +/* + * 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 org.kelar.inputmethod.latin.spellcheck; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.os.Binder; +import android.provider.UserDictionary.Words; +import android.service.textservice.SpellCheckerService.Session; +import android.text.TextUtils; +import android.util.Log; +import android.util.LruCache; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; + +import org.kelar.inputmethod.compat.SuggestionsInfoCompatUtils; +import org.kelar.inputmethod.keyboard.Keyboard; +import org.kelar.inputmethod.latin.NgramContext; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.WordComposer; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.common.LocaleUtils; +import org.kelar.inputmethod.latin.common.StringUtils; +import org.kelar.inputmethod.latin.define.DebugFlags; +import org.kelar.inputmethod.latin.utils.BinaryDictionaryUtils; +import org.kelar.inputmethod.latin.utils.ScriptUtils; +import org.kelar.inputmethod.latin.utils.StatsUtils; +import org.kelar.inputmethod.latin.utils.SuggestionResults; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public abstract class AndroidWordLevelSpellCheckerSession extends Session { + private static final String TAG = AndroidWordLevelSpellCheckerSession.class.getSimpleName(); + + 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. + private final AndroidSpellCheckerService mService; + protected final SuggestionsCache mSuggestionsCache = new SuggestionsCache(); + private final ContentObserver mObserver; + + private static final String quotesRegexp = + "(\\u0022|\\u0027|\\u0060|\\u00B4|\\u2018|\\u2018|\\u201C|\\u201D)"; + + private static final class SuggestionsParams { + public final String[] mSuggestions; + public final int mFlags; + public SuggestionsParams(String[] suggestions, int flags) { + mSuggestions = suggestions; + mFlags = flags; + } + } + + protected static final class SuggestionsCache { + private static final int MAX_CACHE_SIZE = 50; + private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache = + new LruCache<>(MAX_CACHE_SIZE); + + private static String generateKey(final String query) { + return query + ""; + } + + public SuggestionsParams getSuggestionsFromCache(final String query) { + return mUnigramSuggestionsInfoCache.get(query); + } + + public void putSuggestionsToCache( + final String query, final String[] suggestions, final int flags) { + if (suggestions == null || TextUtils.isEmpty(query)) { + return; + } + mUnigramSuggestionsInfoCache.put( + generateKey(query), + new SuggestionsParams(suggestions, flags)); + } + + public void clearCache() { + mUnigramSuggestionsInfoCache.evictAll(); + } + } + + AndroidWordLevelSpellCheckerSession(final AndroidSpellCheckerService service) { + mService = service; + final ContentResolver cres = service.getContentResolver(); + + mObserver = new ContentObserver(null) { + @Override + public void onChange(boolean self) { + mSuggestionsCache.clearCache(); + } + }; + cres.registerContentObserver(Words.CONTENT_URI, true, mObserver); + } + + @Override + public void onCreate() { + final String localeString = getLocale(); + mLocale = (null == localeString) ? null + : LocaleUtils.constructLocaleFromString(localeString); + mScript = ScriptUtils.getScriptFromSpellCheckerLocale(mLocale); + } + + @Override + public void onClose() { + final ContentResolver cres = mService.getContentResolver(); + cres.unregisterContentObserver(mObserver); + } + + 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; + private static final int CHECKABILITY_EMAIL_OR_URL = 3; + private static final int CHECKABILITY_FIRST_LETTER_UNCHECKABLE = 4; + private static final int CHECKABILITY_TOO_SHORT = 5; + /** + * Finds out whether a particular string should be filtered out of spell checking. + * + * This will loosely match URLs, numbers, symbols. To avoid always underlining words that + * we know we will never recognize, this accepts a script identifier that should be one + * of the SCRIPT_* constants defined above, to rule out quickly characters from very + * different languages. + * + * @param text the string to evaluate. + * @param script the identifier for the script this spell checker recognizes + * @return one of the FILTER_OUT_* constants above. + */ + private static int getCheckabilityInScript(final String text, final int script) { + if (TextUtils.isEmpty(text) || text.length() <= 1) return CHECKABILITY_TOO_SHORT; + + // TODO: check if an equivalent processing can't be done more quickly with a + // compiled regexp. + // Filter by first letter + final int firstCodePoint = text.codePointAt(0); + // Filter out words that don't start with a letter or an apostrophe + if (!ScriptUtils.isLetterPartOfScript(firstCodePoint, script) + && '\'' != firstCodePoint) return CHECKABILITY_FIRST_LETTER_UNCHECKABLE; + + // Filter contents + final int length = text.length(); + int letterCount = 0; + for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + // Any word containing a COMMERCIAL_AT is probably an e-mail address + // Any word containing a SLASH is probably either an ad-hoc combination of two + // words or a URI - in either case we don't want to spell check that + if (Constants.CODE_COMMERCIAL_AT == codePoint || Constants.CODE_SLASH == codePoint) { + return CHECKABILITY_EMAIL_OR_URL; + } + // If the string contains a period, native returns strange suggestions (it seems + // to return suggestions for everything up to the period only and to ignore the + // rest), so we suppress lookup if there is a period. + // TODO: investigate why native returns these suggestions and remove this code. + if (Constants.CODE_PERIOD == codePoint) { + return CHECKABILITY_CONTAINS_PERIOD; + } + if (ScriptUtils.isLetterPartOfScript(codePoint, script)) ++letterCount; + } + // Guestimate heuristic: perform spell checking if at least 3/4 of the characters + // in this word are letters + return (letterCount * 4 < length * 3) + ? CHECKABILITY_TOO_MANY_NON_LETTERS : CHECKABILITY_CHECKABLE; + } + + /** + * Helper method to test valid capitalizations of a word. + * + * If the "text" is lower-case, we test only the exact string. + * If the "Text" is capitalized, we test the exact string "Text" and the lower-cased + * version of it "text". + * 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 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 (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 (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 mService.isValidWord(mLocale, + StringUtils.capitalizeFirstAndDowncaseRest(lowerCaseText, mLocale)); + } + + // Note : this must be reentrant + /** + * Gets a list of suggestions for a specific string. This returns a list of possible + * corrections for the text passed as an argument. It may split or group words, and + * even perform grammatical analysis. + */ + private SuggestionsInfo onGetSuggestionsInternal(final TextInfo textInfo, + final int suggestionsLimit) { + return onGetSuggestionsInternal(textInfo, null, suggestionsLimit); + } + + protected SuggestionsInfo onGetSuggestionsInternal( + final TextInfo textInfo, final NgramContext ngramContext, final int suggestionsLimit) { + try { + final String text = textInfo.getText(). + replaceAll(AndroidSpellCheckerService.APOSTROPHE, + AndroidSpellCheckerService.SINGLE_QUOTE). + replaceAll("^" + quotesRegexp, ""). + replaceAll(quotesRegexp + "$", ""); + + if (!mService.hasMainDictionaryForLocale(mLocale)) { + return AndroidSpellCheckerService.getNotInDictEmptySuggestions( + false /* reportAsTypo */); + } + + // Handle special patterns like email, URI, telephone number. + final int checkability = getCheckabilityInScript(text, mScript); + if (CHECKABILITY_CHECKABLE != checkability) { + if (CHECKABILITY_CONTAINS_PERIOD == checkability) { + final String[] splitText = text.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, text) ? + AndroidSpellCheckerService.getInDictEmptySuggestions() : + AndroidSpellCheckerService.getNotInDictEmptySuggestions( + CHECKABILITY_CONTAINS_PERIOD == checkability /* reportAsTypo */); + } + + // Handle normal words. + final int capitalizeType = StringUtils.getCapitalizationType(text); + + if (isInDictForAnyCapitalization(text, capitalizeType)) { + if (DebugFlags.DEBUG_ENABLED) { + Log.i(TAG, "onGetSuggestionsInternal() : [" + text + "] is a valid word"); + } + return AndroidSpellCheckerService.getInDictEmptySuggestions(); + } + if (DebugFlags.DEBUG_ENABLED) { + Log.i(TAG, "onGetSuggestionsInternal() : [" + text + "] is NOT a valid word"); + } + + final Keyboard keyboard = mService.getKeyboardForLocale(mLocale); + if (null == keyboard) { + Log.w(TAG, "onGetSuggestionsInternal() : No keyboard for locale: " + mLocale); + // If there is no keyboard for this locale, don't do any spell-checking. + return AndroidSpellCheckerService.getNotInDictEmptySuggestions( + false /* reportAsTypo */); + } + + final WordComposer composer = new WordComposer(); + final int[] codePoints = StringUtils.toCodePointArray(text); + final int[] coordinates; + coordinates = keyboard.getCoordinates(codePoints); + composer.setComposingWord(codePoints, coordinates); + // TODO: Don't gather suggestions if the limit is <= 0 unless necessary + final SuggestionResults suggestionResults = mService.getSuggestionResults( + mLocale, composer.getComposedDataSnapshot(), ngramContext, keyboard); + final Result result = getResult(capitalizeType, mLocale, suggestionsLimit, + mService.getRecommendedThreshold(), text, suggestionResults); + if (DebugFlags.DEBUG_ENABLED) { + if (result.mSuggestions != null && result.mSuggestions.length > 0) { + final StringBuilder builder = new StringBuilder(); + for (String suggestion : result.mSuggestions) { + builder.append(" ["); + builder.append(suggestion); + builder.append("]"); + } + Log.i(TAG, "onGetSuggestionsInternal() : Suggestions =" + builder); + } + } + // Handle word not in dictionary. + // This is called only once per unique word, so entering multiple + // instances of the same word does not result in more than one call + // to this method. + // Also, upon changing the orientation of the device, this is called + // again for every unique invalid word in the text box. + StatsUtils.onInvalidWordIdentification(text); + + final int flags = + SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO + | (result.mHasRecommendedSuggestions + ? SuggestionsInfoCompatUtils + .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() + : 0); + final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions); + mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags); + return retval; + } catch (RuntimeException e) { + // Don't kill the keyboard if there is a bug in the spell checker + Log.e(TAG, "Exception while spellchecking", e); + return AndroidSpellCheckerService.getNotInDictEmptySuggestions( + false /* reportAsTypo */); + } + } + + 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 */); + } + 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 List<String> gatheredSuggestionsList = + suggestions.subList(0, Math.min(suggestions.size(), suggestionsLimit)); + final String[] gatheredSuggestions = + gatheredSuggestionsList.toArray(new String[gatheredSuggestionsList.size()]); + + final int bestScore = suggestionResults.first().mScore; + final String bestSuggestion = suggestions.get(0); + final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( + originalText, bestSuggestion, bestScore); + final boolean hasRecommendedSuggestions = (normalizedScore > recommendedThreshold); + 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. + * Since it's called externally by the application, the spell checker is using the identity + * of the application by default unless we clearCallingIdentity. + * That's what the following method does. + */ + @Override + public SuggestionsInfo onGetSuggestions(final TextInfo textInfo, final int suggestionsLimit) { + long ident = Binder.clearCallingIdentity(); + try { + return onGetSuggestionsInternal(textInfo, suggestionsLimit); + } finally { + Binder.restoreCallingIdentity(ident); + } + } +} diff --git a/java/src/org/kelar/inputmethod/latin/spellcheck/SentenceLevelAdapter.java b/java/src/org/kelar/inputmethod/latin/spellcheck/SentenceLevelAdapter.java new file mode 100644 index 000000000..4dbcd092e --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/spellcheck/SentenceLevelAdapter.java @@ -0,0 +1,197 @@ +/* + * 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 org.kelar.inputmethod.latin.spellcheck; + +import android.annotation.TargetApi; +import android.content.res.Resources; +import android.os.Build; +import android.view.textservice.SentenceSuggestionsInfo; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; + +import org.kelar.inputmethod.compat.TextInfoCompatUtils; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.settings.SpacingAndPunctuations; +import org.kelar.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 { + private static class EmptySentenceSuggestionsInfosInitializationHolder { + public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = + new SentenceSuggestionsInfo[]{}; + } + private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); + + public static SentenceSuggestionsInfo[] getEmptySentenceSuggestionsInfo() { + return EmptySentenceSuggestionsInfosInitializationHolder.EMPTY_SENTENCE_SUGGESTIONS_INFOS; + } + + /** + * 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 r) { + return new SpacingAndPunctuations(r); + } + }; + mSpacingAndPunctuations = job.runInLocale(res, locale); + } + + public int getEndOfWord(final CharSequence sequence, final int fromIndex) { + final int length = sequence.length(); + int index = fromIndex < 0 ? 0 : Character.offsetByCodePoints(sequence, fromIndex, 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, final int fromIndex) { + final int length = sequence.length(); + if (fromIndex >= length) { + return -1; + } + int index = fromIndex < 0 ? 0 : Character.offsetByCodePoints(sequence, fromIndex, 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<>(); + int wordStart = wordIterator.getBeginningOfNextWord(originalText, start); + int wordEnd = wordIterator.getEndOfWord(originalText, wordStart); + while (wordStart <= end && wordEnd != -1 && wordStart != -1) { + if (wordEnd >= start && wordEnd > wordStart) { + final TextInfo ti = TextInfoCompatUtils.newInstance(originalText, wordStart, + wordEnd, cookie, originalText.subSequence(wordStart, wordEnd).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); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + 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/org/kelar/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java b/java/src/org/kelar/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java new file mode 100644 index 000000000..acbfa8666 --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java @@ -0,0 +1,61 @@ +/* + * 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 org.kelar.inputmethod.latin.spellcheck; + +import org.kelar.inputmethod.latin.permissions.PermissionsManager; +import org.kelar.inputmethod.latin.utils.FragmentUtils; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import androidx.core.app.ActivityCompat; + +/** + * Spell checker preference screen. + */ +public final class SpellCheckerSettingsActivity extends PreferenceActivity + implements ActivityCompat.OnRequestPermissionsResultCallback { + private static final String DEFAULT_FRAGMENT = SpellCheckerSettingsFragment.class.getName(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public Intent getIntent() { + final Intent modIntent = new Intent(super.getIntent()); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT); + modIntent.putExtra(EXTRA_NO_HEADERS, true); + return modIntent; + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + @Override + public boolean isValidFragment(String fragmentName) { + return FragmentUtils.isValidFragment(fragmentName); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + PermissionsManager.get(this).onRequestPermissionsResult( + requestCode, permissions, grantResults); + } +} diff --git a/java/src/org/kelar/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java new file mode 100644 index 000000000..e60173932 --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java @@ -0,0 +1,90 @@ +/* + * 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 org.kelar.inputmethod.latin.spellcheck; + +import android.Manifest; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.text.TextUtils; + +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.permissions.PermissionsManager; +import org.kelar.inputmethod.latin.permissions.PermissionsUtil; +import org.kelar.inputmethod.latin.settings.SubScreenFragment; +import org.kelar.inputmethod.latin.settings.TwoStatePreferenceHelper; +import org.kelar.inputmethod.latin.utils.ApplicationUtils; + +import static org.kelar.inputmethod.latin.permissions.PermissionsManager.get; + +/** + * Preference screen. + */ +public final class SpellCheckerSettingsFragment extends SubScreenFragment + implements SharedPreferences.OnSharedPreferenceChangeListener, + PermissionsManager.PermissionsResultCallback { + + private SwitchPreference mLookupContactsPreference; + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + addPreferencesFromResource(R.xml.spell_checker_settings); + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + preferenceScreen.setTitle(ApplicationUtils.getActivityTitleResId( + getActivity(), SpellCheckerSettingsActivity.class)); + TwoStatePreferenceHelper.replaceCheckBoxPreferencesBySwitchPreferences(preferenceScreen); + + mLookupContactsPreference = (SwitchPreference) findPreference( + AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY); + turnOffLookupContactsIfNoPermission(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (!TextUtils.equals(key, AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY)) { + return; + } + + if (!sharedPreferences.getBoolean(key, false)) { + // don't care if the preference is turned off. + return; + } + + // Check for permissions. + if (PermissionsUtil.checkAllPermissionsGranted( + getActivity() /* context */, Manifest.permission.READ_CONTACTS)) { + return; // all permissions granted, no need to request permissions. + } + + get(getActivity() /* context */).requestPermissions(this /* PermissionsResultCallback */, + getActivity() /* activity */, Manifest.permission.READ_CONTACTS); + } + + @Override + public void onRequestPermissionsResult(boolean allGranted) { + turnOffLookupContactsIfNoPermission(); + } + + private void turnOffLookupContactsIfNoPermission() { + if (!PermissionsUtil.checkAllPermissionsGranted( + getActivity(), Manifest.permission.READ_CONTACTS)) { + mLookupContactsPreference.setChecked(false); + } + } +} |