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