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.Context; 20 import android.content.Intent; 21 import android.content.SharedPreferences; 22 import android.preference.PreferenceManager; 23 import android.service.textservice.SpellCheckerService; 24 import android.text.InputType; 25 import android.util.Log; 26 import android.util.LruCache; 27 import android.view.inputmethod.EditorInfo; 28 import android.view.inputmethod.InputMethodSubtype; 29 import android.view.textservice.SuggestionsInfo; 30 31 import com.android.inputmethod.keyboard.Keyboard; 32 import com.android.inputmethod.keyboard.KeyboardId; 33 import com.android.inputmethod.keyboard.KeyboardLayoutSet; 34 import com.android.inputmethod.keyboard.ProximityInfo; 35 import com.android.inputmethod.latin.ContactsBinaryDictionary; 36 import com.android.inputmethod.latin.Dictionary; 37 import com.android.inputmethod.latin.DictionaryCollection; 38 import com.android.inputmethod.latin.DictionaryFacilitator; 39 import com.android.inputmethod.latin.DictionaryFactory; 40 import com.android.inputmethod.latin.PrevWordsInfo; 41 import com.android.inputmethod.latin.R; 42 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 43 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; 44 import com.android.inputmethod.latin.UserBinaryDictionary; 45 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; 46 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; 47 import com.android.inputmethod.latin.utils.CollectionUtils; 48 import com.android.inputmethod.latin.utils.LocaleUtils; 49 import com.android.inputmethod.latin.utils.ScriptUtils; 50 import com.android.inputmethod.latin.utils.StringUtils; 51 import com.android.inputmethod.latin.utils.SuggestionResults; 52 import com.android.inputmethod.latin.WordComposer; 53 54 import java.lang.ref.WeakReference; 55 import java.util.ArrayList; 56 import java.util.Arrays; 57 import java.util.Collections; 58 import java.util.HashMap; 59 import java.util.HashSet; 60 import java.util.Iterator; 61 import java.util.Locale; 62 import java.util.Map; 63 import java.util.TreeMap; 64 import java.util.concurrent.ConcurrentHashMap; 65 import java.util.concurrent.ConcurrentLinkedQueue; 66 import java.util.concurrent.Semaphore; 67 import java.util.concurrent.TimeUnit; 68 69 /** 70 * Service for spell checking, using LatinIME's dictionaries and mechanisms. 71 */ 72 public final class AndroidSpellCheckerService extends SpellCheckerService 73 implements SharedPreferences.OnSharedPreferenceChangeListener { 74 private static final String TAG = AndroidSpellCheckerService.class.getSimpleName(); 75 private static final boolean DBG = false; 76 77 public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts"; 78 79 private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480; 80 private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 368; 81 82 private static final String DICTIONARY_NAME_PREFIX = "spellcheck_"; 83 private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000; 84 private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5; 85 86 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 87 88 private final HashSet<Locale> mCachedLocales = new HashSet<>(); 89 90 private final int MAX_NUM_OF_THREADS_READ_DICTIONARY = 2; 91 private final Semaphore mSemaphore = new Semaphore(MAX_NUM_OF_THREADS_READ_DICTIONARY, 92 true /* fair */); 93 // TODO: Make each spell checker session has its own session id. 94 private final ConcurrentLinkedQueue<Integer> mSessionIdPool = new ConcurrentLinkedQueue<>(); 95 96 private static class DictionaryFacilitatorLruCache extends 97 LruCache<Locale, DictionaryFacilitator> { 98 private final HashSet<Locale> mCachedLocales; DictionaryFacilitatorLruCache(final HashSet<Locale> cachedLocales, int maxSize)99 public DictionaryFacilitatorLruCache(final HashSet<Locale> cachedLocales, int maxSize) { 100 super(maxSize); 101 mCachedLocales = cachedLocales; 102 } 103 104 @Override entryRemoved(boolean evicted, Locale key, DictionaryFacilitator oldValue, DictionaryFacilitator newValue)105 protected void entryRemoved(boolean evicted, Locale key, 106 DictionaryFacilitator oldValue, DictionaryFacilitator newValue) { 107 if (oldValue != null && oldValue != newValue) { 108 oldValue.closeDictionaries(); 109 } 110 if (key != null && newValue == null) { 111 // Remove locale from the cache when the dictionary facilitator for the locale is 112 // evicted and new facilitator is not set for the locale. 113 mCachedLocales.remove(key); 114 if (size() >= maxSize()) { 115 Log.w(TAG, "DictionaryFacilitator for " + key.toString() 116 + " has been evicted due to cache size limit." 117 + " size: " + size() + ", maxSize: " + maxSize()); 118 } 119 } 120 } 121 } 122 123 private static final int MAX_DICTIONARY_FACILITATOR_COUNT = 3; 124 private final LruCache<Locale, DictionaryFacilitator> mDictionaryFacilitatorCache = 125 new DictionaryFacilitatorLruCache(mCachedLocales, MAX_DICTIONARY_FACILITATOR_COUNT); 126 private final ConcurrentHashMap<Locale, Keyboard> mKeyboardCache = new ConcurrentHashMap<>(); 127 128 // The threshold for a suggestion to be considered "recommended". 129 private float mRecommendedThreshold; 130 // Whether to use the contacts dictionary 131 private boolean mUseContactsDictionary; 132 // TODO: make a spell checker option to block offensive words or not 133 private final SettingsValuesForSuggestion mSettingsValuesForSuggestion = 134 new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */, 135 true /* spaceAwareGestureEnabled */, 136 null /* additionalFeaturesSettingValues */); 137 private final Object mDictionaryLock = new Object(); 138 139 public static final String SINGLE_QUOTE = "\u0027"; 140 public static final String APOSTROPHE = "\u2019"; 141 AndroidSpellCheckerService()142 public AndroidSpellCheckerService() { 143 super(); 144 for (int i = 0; i < MAX_NUM_OF_THREADS_READ_DICTIONARY; i++) { 145 mSessionIdPool.add(i); 146 } 147 } 148 onCreate()149 @Override public void onCreate() { 150 super.onCreate(); 151 mRecommendedThreshold = 152 Float.parseFloat(getString(R.string.spellchecker_recommended_threshold_value)); 153 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 154 prefs.registerOnSharedPreferenceChangeListener(this); 155 onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); 156 } 157 getRecommendedThreshold()158 public float getRecommendedThreshold() { 159 return mRecommendedThreshold; 160 } 161 getKeyboardLayoutNameForScript(final int script)162 private static String getKeyboardLayoutNameForScript(final int script) { 163 switch (script) { 164 case ScriptUtils.SCRIPT_LATIN: 165 return "qwerty"; 166 case ScriptUtils.SCRIPT_CYRILLIC: 167 return "east_slavic"; 168 case ScriptUtils.SCRIPT_GREEK: 169 return "greek"; 170 default: 171 throw new RuntimeException("Wrong script supplied: " + script); 172 } 173 } 174 175 @Override onSharedPreferenceChanged(final SharedPreferences prefs, final String key)176 public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { 177 if (!PREF_USE_CONTACTS_KEY.equals(key)) return; 178 final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); 179 if (useContactsDictionary != mUseContactsDictionary) { 180 mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); 181 try { 182 mUseContactsDictionary = useContactsDictionary; 183 for (final Locale locale : mCachedLocales) { 184 final DictionaryFacilitator dictionaryFacilitator = 185 mDictionaryFacilitatorCache.get(locale); 186 resetDictionariesForLocale(this /* context */, 187 dictionaryFacilitator, locale, mUseContactsDictionary); 188 } 189 } finally { 190 mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); 191 } 192 } 193 } 194 195 @Override createSession()196 public Session createSession() { 197 // Should not refer to AndroidSpellCheckerSession directly considering 198 // that AndroidSpellCheckerSession may be overlaid. 199 return AndroidSpellCheckerSessionFactory.newInstance(this); 200 } 201 202 /** 203 * Returns an empty SuggestionsInfo with flags signaling the word is not in the dictionary. 204 * @param reportAsTypo whether this should include the flag LOOKS_LIKE_TYPO, for red underline. 205 * @return the empty SuggestionsInfo with the appropriate flags set. 206 */ getNotInDictEmptySuggestions(final boolean reportAsTypo)207 public static SuggestionsInfo getNotInDictEmptySuggestions(final boolean reportAsTypo) { 208 return new SuggestionsInfo(reportAsTypo ? SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO : 0, 209 EMPTY_STRING_ARRAY); 210 } 211 212 /** 213 * Returns an empty suggestionInfo with flags signaling the word is in the dictionary. 214 * @return the empty SuggestionsInfo with the appropriate flags set. 215 */ getInDictEmptySuggestions()216 public static SuggestionsInfo getInDictEmptySuggestions() { 217 return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY, 218 EMPTY_STRING_ARRAY); 219 } 220 isValidWord(final Locale locale, final String word)221 public boolean isValidWord(final Locale locale, final String word) { 222 mSemaphore.acquireUninterruptibly(); 223 try { 224 DictionaryFacilitator dictionaryFacilitatorForLocale = 225 getDictionaryFacilitatorForLocaleLocked(locale); 226 return dictionaryFacilitatorForLocale.isValidWord(word, false /* igroreCase */); 227 } finally { 228 mSemaphore.release(); 229 } 230 } 231 getSuggestionResults(final Locale locale, final WordComposer composer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo)232 public SuggestionResults getSuggestionResults(final Locale locale, final WordComposer composer, 233 final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo) { 234 Integer sessionId = null; 235 mSemaphore.acquireUninterruptibly(); 236 try { 237 sessionId = mSessionIdPool.poll(); 238 DictionaryFacilitator dictionaryFacilitatorForLocale = 239 getDictionaryFacilitatorForLocaleLocked(locale); 240 return dictionaryFacilitatorForLocale.getSuggestionResults(composer, prevWordsInfo, 241 proximityInfo, mSettingsValuesForSuggestion, sessionId); 242 } finally { 243 if (sessionId != null) { 244 mSessionIdPool.add(sessionId); 245 } 246 mSemaphore.release(); 247 } 248 } 249 hasMainDictionaryForLocale(final Locale locale)250 public boolean hasMainDictionaryForLocale(final Locale locale) { 251 mSemaphore.acquireUninterruptibly(); 252 try { 253 final DictionaryFacilitator dictionaryFacilitator = 254 getDictionaryFacilitatorForLocaleLocked(locale); 255 return dictionaryFacilitator.hasInitializedMainDictionary(); 256 } finally { 257 mSemaphore.release(); 258 } 259 } 260 getDictionaryFacilitatorForLocaleLocked(final Locale locale)261 private DictionaryFacilitator getDictionaryFacilitatorForLocaleLocked(final Locale locale) { 262 DictionaryFacilitator dictionaryFacilitatorForLocale = 263 mDictionaryFacilitatorCache.get(locale); 264 if (dictionaryFacilitatorForLocale == null) { 265 dictionaryFacilitatorForLocale = new DictionaryFacilitator(); 266 mDictionaryFacilitatorCache.put(locale, dictionaryFacilitatorForLocale); 267 mCachedLocales.add(locale); 268 resetDictionariesForLocale(this /* context */, dictionaryFacilitatorForLocale, 269 locale, mUseContactsDictionary); 270 } 271 return dictionaryFacilitatorForLocale; 272 } 273 resetDictionariesForLocale(final Context context, final DictionaryFacilitator dictionaryFacilitator, final Locale locale, final boolean useContactsDictionary)274 private static void resetDictionariesForLocale(final Context context, 275 final DictionaryFacilitator dictionaryFacilitator, final Locale locale, 276 final boolean useContactsDictionary) { 277 dictionaryFacilitator.resetDictionariesWithDictNamePrefix(context, locale, 278 useContactsDictionary, false /* usePersonalizedDicts */, 279 false /* forceReloadMainDictionary */, null /* listener */, 280 DICTIONARY_NAME_PREFIX); 281 for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) { 282 try { 283 dictionaryFacilitator.waitForLoadingMainDictionary( 284 WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS); 285 return; 286 } catch (final InterruptedException e) { 287 Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e); 288 if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) { 289 Log.i(TAG, "Retry", e); 290 } else { 291 Log.w(TAG, "Give up retrying. Retried " 292 + MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e); 293 } 294 } 295 } 296 } 297 298 @Override onUnbind(final Intent intent)299 public boolean onUnbind(final Intent intent) { 300 mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); 301 try { 302 mDictionaryFacilitatorCache.evictAll(); 303 mCachedLocales.clear(); 304 } finally { 305 mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); 306 } 307 mKeyboardCache.clear(); 308 return false; 309 } 310 getKeyboardForLocale(final Locale locale)311 public Keyboard getKeyboardForLocale(final Locale locale) { 312 Keyboard keyboard = mKeyboardCache.get(locale); 313 if (keyboard == null) { 314 keyboard = createKeyboardForLocale(locale); 315 if (keyboard != null) { 316 mKeyboardCache.put(locale, keyboard); 317 } 318 } 319 return keyboard; 320 } 321 createKeyboardForLocale(final Locale locale)322 private Keyboard createKeyboardForLocale(final Locale locale) { 323 final int script = ScriptUtils.getScriptFromSpellCheckerLocale(locale); 324 final String keyboardLayoutName = getKeyboardLayoutNameForScript(script); 325 final InputMethodSubtype subtype = AdditionalSubtypeUtils.createDummyAdditionalSubtype( 326 locale.toString(), keyboardLayoutName); 327 final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype); 328 return keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); 329 } 330 createKeyboardSetForSpellChecker(final InputMethodSubtype subtype)331 private KeyboardLayoutSet createKeyboardSetForSpellChecker(final InputMethodSubtype subtype) { 332 final EditorInfo editorInfo = new EditorInfo(); 333 editorInfo.inputType = InputType.TYPE_CLASS_TEXT; 334 final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(this, editorInfo); 335 builder.setKeyboardGeometry( 336 SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT); 337 builder.setSubtype(subtype); 338 builder.setIsSpellChecker(true /* isSpellChecker */); 339 builder.disableTouchPositionCorrectionData(); 340 return builder.build(); 341 } 342 } 343