UserBinaryDictionary.java revision 61fe28831d93a829f67694a342b6c94bdc7732ea
1/*
2 * Copyright (C) 2012 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;
18
19import android.content.ContentProviderClient;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.database.ContentObserver;
23import android.database.Cursor;
24import android.database.sqlite.SQLiteException;
25import android.net.Uri;
26import android.os.Build;
27import android.provider.UserDictionary.Words;
28import android.text.TextUtils;
29import android.util.Log;
30
31import com.android.inputmethod.compat.UserDictionaryCompatUtils;
32import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
33
34import java.io.File;
35import java.util.Arrays;
36import java.util.Locale;
37
38/**
39 * An expandable dictionary that stores the words in the user dictionary provider into a binary
40 * dictionary file to use it from native code.
41 */
42public class UserBinaryDictionary extends ExpandableBinaryDictionary {
43    private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName();
44
45    // The user dictionary provider uses an empty string to mean "all languages".
46    private static final String USER_DICTIONARY_ALL_LANGUAGES = "";
47    private static final int HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY = 250;
48    private static final int LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY = 160;
49    // Shortcut frequency is 0~15, with 15 = whitelist. We don't want user dictionary entries
50    // to auto-correct, so we set this to the highest frequency that won't, i.e. 14.
51    private static final int USER_DICT_SHORTCUT_FREQUENCY = 14;
52
53    private static final String[] PROJECTION_QUERY_WITH_SHORTCUT = new String[] {
54        Words.WORD,
55        Words.SHORTCUT,
56        Words.FREQUENCY,
57    };
58    private static final String[] PROJECTION_QUERY_WITHOUT_SHORTCUT = new String[] {
59        Words.WORD,
60        Words.FREQUENCY,
61    };
62
63    private static final String NAME = "userunigram";
64
65    private ContentObserver mObserver;
66    final private String mLocale;
67    final private boolean mAlsoUseMoreRestrictiveLocales;
68
69    private UserBinaryDictionary(final Context context, final Locale locale, final File dictFile) {
70        this(context, locale, false /* alsoUseMoreRestrictiveLocales */, dictFile, NAME);
71    }
72
73    protected UserBinaryDictionary(final Context context, final Locale locale,
74            final boolean alsoUseMoreRestrictiveLocales, final File dictFile, final String name) {
75        super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_USER, dictFile);
76        if (null == locale) throw new NullPointerException(); // Catch the error earlier
77        final String localeStr = locale.toString();
78        if (SubtypeLocaleUtils.NO_LANGUAGE.equals(localeStr)) {
79            // If we don't have a locale, insert into the "all locales" user dictionary.
80            mLocale = USER_DICTIONARY_ALL_LANGUAGES;
81        } else {
82            mLocale = localeStr;
83        }
84        mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
85        ContentResolver cres = context.getContentResolver();
86
87        mObserver = new ContentObserver(null) {
88            @Override
89            public void onChange(final boolean self) {
90                // This hook is deprecated as of API level 16 (Build.VERSION_CODES.JELLY_BEAN),
91                // but should still be supported for cases where the IME is running on an older
92                // version of the platform.
93                onChange(self, null);
94            }
95            // The following hook is only available as of API level 16
96            // (Build.VERSION_CODES.JELLY_BEAN), and as such it will only work on JellyBean+
97            // devices. On older versions of the platform, the hook above will be called instead.
98            @Override
99            public void onChange(final boolean self, final Uri uri) {
100                setNeedsToReload();
101            }
102        };
103        cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
104        reloadDictionaryIfRequired();
105    }
106
107    public static UserBinaryDictionary getDictionary(final Context context, final Locale locale,
108            final File dictFile) {
109        return new UserBinaryDictionary(context, locale, dictFile);
110    }
111
112    @Override
113    public synchronized void close() {
114        if (mObserver != null) {
115            mContext.getContentResolver().unregisterContentObserver(mObserver);
116            mObserver = null;
117        }
118        super.close();
119    }
120
121    @Override
122    public void loadInitialContentsLocked() {
123        // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"],
124        // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3.
125        // This is correct for locale processing.
126        // For this example, we'll look at the "en_US_POSIX" case.
127        final String[] localeElements =
128                TextUtils.isEmpty(mLocale) ? new String[] {} : mLocale.split("_", 3);
129        final int length = localeElements.length;
130
131        final StringBuilder request = new StringBuilder("(locale is NULL)");
132        String localeSoFar = "";
133        // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ;
134        // and request = "(locale is NULL)"
135        for (int i = 0; i < length; ++i) {
136            // i | localeSoFar    | localeElements
137            // 0 | ""             | ["en", "US", "POSIX"]
138            // 1 | "en_"          | ["en", "US", "POSIX"]
139            // 2 | "en_US_"       | ["en", "en_US", "POSIX"]
140            localeElements[i] = localeSoFar + localeElements[i];
141            localeSoFar = localeElements[i] + "_";
142            // i | request
143            // 0 | "(locale is NULL)"
144            // 1 | "(locale is NULL) or (locale=?)"
145            // 2 | "(locale is NULL) or (locale=?) or (locale=?)"
146            request.append(" or (locale=?)");
147        }
148        // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_"
149        // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)"
150
151        final String[] requestArguments;
152        // If length == 3, we already have all the arguments we need (common prefix is meaningless
153        // inside variants
154        if (mAlsoUseMoreRestrictiveLocales && length < 3) {
155            request.append(" or (locale like ?)");
156            // The following creates an array with one more (null) position
157            final String[] localeElementsWithMoreRestrictiveLocalesIncluded =
158                    Arrays.copyOf(localeElements, length + 1);
159            localeElementsWithMoreRestrictiveLocalesIncluded[length] =
160                    localeElements[length - 1] + "_%";
161            requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded;
162            // If for example localeElements = ["en"]
163            // then requestArguments = ["en", "en_%"]
164            // and request = (locale is NULL) or (locale=?) or (locale like ?)
165            // If localeElements = ["en", "en_US"]
166            // then requestArguments = ["en", "en_US", "en_US_%"]
167        } else {
168            requestArguments = localeElements;
169        }
170        final String requestString = request.toString();
171        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
172            try {
173                addWordsFromProjectionLocked(PROJECTION_QUERY_WITH_SHORTCUT, requestString,
174                        requestArguments);
175            } catch (IllegalArgumentException e) {
176                // This may happen on some non-compliant devices where the declared API is JB+ but
177                // the SHORTCUT column is not present for some reason.
178                addWordsFromProjectionLocked(PROJECTION_QUERY_WITHOUT_SHORTCUT, requestString,
179                        requestArguments);
180            }
181        } else {
182            addWordsFromProjectionLocked(PROJECTION_QUERY_WITHOUT_SHORTCUT, requestString,
183                    requestArguments);
184        }
185    }
186
187    private void addWordsFromProjectionLocked(final String[] query, String request,
188            final String[] requestArguments) throws IllegalArgumentException {
189        Cursor cursor = null;
190        try {
191            cursor = mContext.getContentResolver().query(
192                    Words.CONTENT_URI, query, request, requestArguments, null);
193            addWordsLocked(cursor);
194        } catch (final SQLiteException e) {
195            Log.e(TAG, "SQLiteException in the remote User dictionary process.", e);
196        } finally {
197            try {
198                if (null != cursor) cursor.close();
199            } catch (final SQLiteException e) {
200                Log.e(TAG, "SQLiteException in the remote User dictionary process.", e);
201            }
202        }
203    }
204
205    public static boolean isEnabled(final Context context) {
206        final ContentResolver cr = context.getContentResolver();
207        final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI);
208        if (client != null) {
209            client.release();
210            return true;
211        } else {
212            return false;
213        }
214    }
215
216    /**
217     * Adds a word to the user dictionary and makes it persistent.
218     *
219     * @param context the context
220     * @param locale the locale
221     * @param word the word to add. If the word is capitalized, then the dictionary will
222     * recognize it as a capitalized word when searched.
223     */
224    public static void addWordToUserDictionary(final Context context, final Locale locale,
225            final String word) {
226        // Update the user dictionary provider
227        UserDictionaryCompatUtils.addWord(context, word,
228                HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY, null, locale);
229    }
230
231    private int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) {
232        // The default frequency for the user dictionary is 250 for historical reasons.
233        // Latin IME considers a good value for the default user dictionary frequency
234        // is about 160 considering the scale we use. So we are scaling down the values.
235        if (defaultFrequency > Integer.MAX_VALUE / LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY) {
236            return (defaultFrequency / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY)
237                    * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY;
238        } else {
239            return (defaultFrequency * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY)
240                    / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY;
241        }
242    }
243
244    private void addWordsLocked(final Cursor cursor) {
245        final boolean hasShortcutColumn = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
246        if (cursor == null) return;
247        if (cursor.moveToFirst()) {
248            final int indexWord = cursor.getColumnIndex(Words.WORD);
249            final int indexShortcut = hasShortcutColumn ? cursor.getColumnIndex(Words.SHORTCUT) : 0;
250            final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
251            while (!cursor.isAfterLast()) {
252                final String word = cursor.getString(indexWord);
253                final String shortcut = hasShortcutColumn ? cursor.getString(indexShortcut) : null;
254                final int frequency = cursor.getInt(indexFrequency);
255                final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency);
256                // Safeguard against adding really long words.
257                if (word.length() < MAX_WORD_LENGTH) {
258                    runGCIfRequiredLocked(true /* mindsBlockByGC */);
259                    addWordDynamicallyLocked(word, adjustedFrequency, null /* shortcutTarget */,
260                            0 /* shortcutFreq */, false /* isNotAWord */,
261                            false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
262                    if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) {
263                        runGCIfRequiredLocked(true /* mindsBlockByGC */);
264                        addWordDynamicallyLocked(shortcut, adjustedFrequency, word,
265                                USER_DICT_SHORTCUT_FREQUENCY, true /* isNotAWord */,
266                                false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
267                    }
268                }
269                cursor.moveToNext();
270            }
271        }
272    }
273
274    @Override
275    protected boolean haveContentsChanged() {
276        return true;
277    }
278}
279