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