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