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
17package com.android.inputmethod.latin.spellcheck;
18
19import android.content.Intent;
20import android.content.SharedPreferences;
21import android.preference.PreferenceManager;
22import android.service.textservice.SpellCheckerService;
23import android.text.InputType;
24import android.view.inputmethod.EditorInfo;
25import android.view.inputmethod.InputMethodSubtype;
26import android.view.textservice.SuggestionsInfo;
27
28import com.android.inputmethod.keyboard.Keyboard;
29import com.android.inputmethod.keyboard.KeyboardId;
30import com.android.inputmethod.keyboard.KeyboardLayoutSet;
31import com.android.inputmethod.latin.DictionaryFacilitator;
32import com.android.inputmethod.latin.DictionaryFacilitatorLruCache;
33import com.android.inputmethod.latin.NgramContext;
34import com.android.inputmethod.latin.R;
35import com.android.inputmethod.latin.RichInputMethodSubtype;
36import com.android.inputmethod.latin.SuggestedWords;
37import com.android.inputmethod.latin.common.ComposedData;
38import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
39import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
40import com.android.inputmethod.latin.utils.ScriptUtils;
41import com.android.inputmethod.latin.utils.SuggestionResults;
42
43import java.util.Locale;
44import java.util.concurrent.ConcurrentHashMap;
45import java.util.concurrent.ConcurrentLinkedQueue;
46import java.util.concurrent.Semaphore;
47
48import javax.annotation.Nonnull;
49
50/**
51 * Service for spell checking, using LatinIME's dictionaries and mechanisms.
52 */
53public 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