Home | History | Annotate | Download | only in spellcheck
      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 
     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
     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 
    103     public float getRecommendedThreshold() {
    104         return mRecommendedThreshold;
    105     }
    106 
    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
    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
    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      */
    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      */
    155     public static SuggestionsInfo getInDictEmptySuggestions() {
    156         return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY,
    157                 EMPTY_STRING_ARRAY);
    158     }
    159 
    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 
    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 
    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
    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 
    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 
    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 
    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