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