1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.latin.spellcheck; 18 19 import android.content.Intent; 20 import android.content.SharedPreferences; 21 import android.preference.PreferenceManager; 22 import android.service.textservice.SpellCheckerService; 23 import android.text.InputType; 24 import android.view.inputmethod.EditorInfo; 25 import android.view.inputmethod.InputMethodSubtype; 26 import android.view.textservice.SuggestionsInfo; 27 28 import com.android.inputmethod.keyboard.Keyboard; 29 import com.android.inputmethod.keyboard.KeyboardId; 30 import com.android.inputmethod.keyboard.KeyboardLayoutSet; 31 import com.android.inputmethod.latin.DictionaryFacilitator; 32 import com.android.inputmethod.latin.DictionaryFacilitatorLruCache; 33 import com.android.inputmethod.latin.NgramContext; 34 import com.android.inputmethod.latin.R; 35 import com.android.inputmethod.latin.RichInputMethodSubtype; 36 import com.android.inputmethod.latin.SuggestedWords; 37 import com.android.inputmethod.latin.common.ComposedData; 38 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; 39 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; 40 import com.android.inputmethod.latin.utils.ScriptUtils; 41 import com.android.inputmethod.latin.utils.SuggestionResults; 42 43 import java.util.Locale; 44 import java.util.concurrent.ConcurrentHashMap; 45 import java.util.concurrent.ConcurrentLinkedQueue; 46 import java.util.concurrent.Semaphore; 47 48 import javax.annotation.Nonnull; 49 50 /** 51 * Service for spell checking, using LatinIME's dictionaries and mechanisms. 52 */ 53 public final class AndroidSpellCheckerService extends SpellCheckerService 54 implements SharedPreferences.OnSharedPreferenceChangeListener { 55 private static final String TAG = AndroidSpellCheckerService.class.getSimpleName(); 56 private static final boolean DEBUG = false; 57 58 public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts"; 59 60 private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480; 61 private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 301; 62 63 private static final String DICTIONARY_NAME_PREFIX = "spellcheck_"; 64 65 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 66 67 private final int MAX_NUM_OF_THREADS_READ_DICTIONARY = 2; 68 private final Semaphore mSemaphore = new Semaphore(MAX_NUM_OF_THREADS_READ_DICTIONARY, 69 true /* fair */); 70 // TODO: Make each spell checker session has its own session id. 71 private final ConcurrentLinkedQueue<Integer> mSessionIdPool = new ConcurrentLinkedQueue<>(); 72 73 private final DictionaryFacilitatorLruCache mDictionaryFacilitatorCache = 74 new DictionaryFacilitatorLruCache(this /* context */, DICTIONARY_NAME_PREFIX); 75 private final ConcurrentHashMap<Locale, Keyboard> mKeyboardCache = new ConcurrentHashMap<>(); 76 77 // The threshold for a suggestion to be considered "recommended". 78 private float mRecommendedThreshold; 79 // TODO: make a spell checker option to block offensive words or not 80 private final SettingsValuesForSuggestion mSettingsValuesForSuggestion = 81 new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */); 82 83 public static final String SINGLE_QUOTE = "\u0027"; 84 public static final String APOSTROPHE = "\u2019"; 85 AndroidSpellCheckerService()86 public AndroidSpellCheckerService() { 87 super(); 88 for (int i = 0; i < MAX_NUM_OF_THREADS_READ_DICTIONARY; i++) { 89 mSessionIdPool.add(i); 90 } 91 } 92 93 @Override onCreate()94 public void onCreate() { 95 super.onCreate(); 96 mRecommendedThreshold = Float.parseFloat( 97 getString(R.string.spellchecker_recommended_threshold_value)); 98 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 99 prefs.registerOnSharedPreferenceChangeListener(this); 100 onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); 101 } 102 getRecommendedThreshold()103 public float getRecommendedThreshold() { 104 return mRecommendedThreshold; 105 } 106 getKeyboardLayoutNameForLocale(final Locale locale)107 private static String getKeyboardLayoutNameForLocale(final Locale locale) { 108 // See b/19963288. 109 if (locale.getLanguage().equals("sr")) { 110 return "south_slavic"; 111 } 112 final int script = ScriptUtils.getScriptFromSpellCheckerLocale(locale); 113 switch (script) { 114 case ScriptUtils.SCRIPT_LATIN: 115 return "qwerty"; 116 case ScriptUtils.SCRIPT_CYRILLIC: 117 return "east_slavic"; 118 case ScriptUtils.SCRIPT_GREEK: 119 return "greek"; 120 case ScriptUtils.SCRIPT_HEBREW: 121 return "hebrew"; 122 default: 123 throw new RuntimeException("Wrong script supplied: " + script); 124 } 125 } 126 127 @Override onSharedPreferenceChanged(final SharedPreferences prefs, final String key)128 public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { 129 if (!PREF_USE_CONTACTS_KEY.equals(key)) return; 130 final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); 131 mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary); 132 } 133 134 @Override createSession()135 public Session createSession() { 136 // Should not refer to AndroidSpellCheckerSession directly considering 137 // that AndroidSpellCheckerSession may be overlaid. 138 return AndroidSpellCheckerSessionFactory.newInstance(this); 139 } 140 141 /** 142 * Returns an empty SuggestionsInfo with flags signaling the word is not in the dictionary. 143 * @param reportAsTypo whether this should include the flag LOOKS_LIKE_TYPO, for red underline. 144 * @return the empty SuggestionsInfo with the appropriate flags set. 145 */ getNotInDictEmptySuggestions(final boolean reportAsTypo)146 public static SuggestionsInfo getNotInDictEmptySuggestions(final boolean reportAsTypo) { 147 return new SuggestionsInfo(reportAsTypo ? SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO : 0, 148 EMPTY_STRING_ARRAY); 149 } 150 151 /** 152 * Returns an empty suggestionInfo with flags signaling the word is in the dictionary. 153 * @return the empty SuggestionsInfo with the appropriate flags set. 154 */ getInDictEmptySuggestions()155 public static SuggestionsInfo getInDictEmptySuggestions() { 156 return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY, 157 EMPTY_STRING_ARRAY); 158 } 159 isValidWord(final Locale locale, final String word)160 public boolean isValidWord(final Locale locale, final String word) { 161 mSemaphore.acquireUninterruptibly(); 162 try { 163 DictionaryFacilitator dictionaryFacilitatorForLocale = 164 mDictionaryFacilitatorCache.get(locale); 165 return dictionaryFacilitatorForLocale.isValidSpellingWord(word); 166 } finally { 167 mSemaphore.release(); 168 } 169 } 170 getSuggestionResults(final Locale locale, final ComposedData composedData, final NgramContext ngramContext, @Nonnull final Keyboard keyboard)171 public SuggestionResults getSuggestionResults(final Locale locale, 172 final ComposedData composedData, final NgramContext ngramContext, 173 @Nonnull final Keyboard keyboard) { 174 Integer sessionId = null; 175 mSemaphore.acquireUninterruptibly(); 176 try { 177 sessionId = mSessionIdPool.poll(); 178 DictionaryFacilitator dictionaryFacilitatorForLocale = 179 mDictionaryFacilitatorCache.get(locale); 180 return dictionaryFacilitatorForLocale.getSuggestionResults(composedData, ngramContext, 181 keyboard, mSettingsValuesForSuggestion, 182 sessionId, SuggestedWords.INPUT_STYLE_TYPING); 183 } finally { 184 if (sessionId != null) { 185 mSessionIdPool.add(sessionId); 186 } 187 mSemaphore.release(); 188 } 189 } 190 hasMainDictionaryForLocale(final Locale locale)191 public boolean hasMainDictionaryForLocale(final Locale locale) { 192 mSemaphore.acquireUninterruptibly(); 193 try { 194 final DictionaryFacilitator dictionaryFacilitator = 195 mDictionaryFacilitatorCache.get(locale); 196 return dictionaryFacilitator.hasAtLeastOneInitializedMainDictionary(); 197 } finally { 198 mSemaphore.release(); 199 } 200 } 201 202 @Override onUnbind(final Intent intent)203 public boolean onUnbind(final Intent intent) { 204 mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); 205 try { 206 mDictionaryFacilitatorCache.closeDictionaries(); 207 } finally { 208 mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); 209 } 210 mKeyboardCache.clear(); 211 return false; 212 } 213 getKeyboardForLocale(final Locale locale)214 public Keyboard getKeyboardForLocale(final Locale locale) { 215 Keyboard keyboard = mKeyboardCache.get(locale); 216 if (keyboard == null) { 217 keyboard = createKeyboardForLocale(locale); 218 if (keyboard != null) { 219 mKeyboardCache.put(locale, keyboard); 220 } 221 } 222 return keyboard; 223 } 224 createKeyboardForLocale(final Locale locale)225 private Keyboard createKeyboardForLocale(final Locale locale) { 226 final String keyboardLayoutName = getKeyboardLayoutNameForLocale(locale); 227 final InputMethodSubtype subtype = AdditionalSubtypeUtils.createDummyAdditionalSubtype( 228 locale.toString(), keyboardLayoutName); 229 final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype); 230 return keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); 231 } 232 createKeyboardSetForSpellChecker(final InputMethodSubtype subtype)233 private KeyboardLayoutSet createKeyboardSetForSpellChecker(final InputMethodSubtype subtype) { 234 final EditorInfo editorInfo = new EditorInfo(); 235 editorInfo.inputType = InputType.TYPE_CLASS_TEXT; 236 final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(this, editorInfo); 237 builder.setKeyboardGeometry( 238 SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT); 239 builder.setSubtype(RichInputMethodSubtype.getRichInputMethodSubtype(subtype)); 240 builder.setIsSpellChecker(true /* isSpellChecker */); 241 builder.disableTouchPositionCorrectionData(); 242 return builder.build(); 243 } 244 } 245