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.ContentResolver; 20 import android.content.Context; 21 import android.database.ContentObserver; 22 import android.database.Cursor; 23 import android.database.sqlite.SQLiteException; 24 import android.net.Uri; 25 import android.provider.UserDictionary.Words; 26 import android.text.TextUtils; 27 import android.util.Log; 28 29 import com.android.inputmethod.annotations.ExternallyReferenced; 30 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 31 32 import java.io.File; 33 import java.util.Arrays; 34 import java.util.Locale; 35 36 import javax.annotation.Nullable; 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 */ 42 public 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 50 private static final String[] PROJECTION_QUERY = new String[] {Words.WORD, Words.FREQUENCY}; 51 52 private static final String NAME = "userunigram"; 53 54 private ContentObserver mObserver; 55 final private String mLocaleString; 56 final private boolean mAlsoUseMoreRestrictiveLocales; 57 UserBinaryDictionary(final Context context, final Locale locale, final boolean alsoUseMoreRestrictiveLocales, final File dictFile, final String name)58 protected UserBinaryDictionary(final Context context, final Locale locale, 59 final boolean alsoUseMoreRestrictiveLocales, 60 final File dictFile, final String name) { 61 super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_USER, dictFile); 62 if (null == locale) throw new NullPointerException(); // Catch the error earlier 63 final String localeStr = locale.toString(); 64 if (SubtypeLocaleUtils.NO_LANGUAGE.equals(localeStr)) { 65 // If we don't have a locale, insert into the "all locales" user dictionary. 66 mLocaleString = USER_DICTIONARY_ALL_LANGUAGES; 67 } else { 68 mLocaleString = localeStr; 69 } 70 mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales; 71 ContentResolver cres = context.getContentResolver(); 72 73 mObserver = new ContentObserver(null) { 74 @Override 75 public void onChange(final boolean self) { 76 // This hook is deprecated as of API level 16 (Build.VERSION_CODES.JELLY_BEAN), 77 // but should still be supported for cases where the IME is running on an older 78 // version of the platform. 79 onChange(self, null); 80 } 81 // The following hook is only available as of API level 16 82 // (Build.VERSION_CODES.JELLY_BEAN), and as such it will only work on JellyBean+ 83 // devices. On older versions of the platform, the hook above will be called instead. 84 @Override 85 public void onChange(final boolean self, final Uri uri) { 86 setNeedsToRecreate(); 87 } 88 }; 89 cres.registerContentObserver(Words.CONTENT_URI, true, mObserver); 90 reloadDictionaryIfRequired(); 91 } 92 93 // Note: This method is called by {@link DictionaryFacilitator} using Java reflection. 94 @ExternallyReferenced getDictionary( final Context context, final Locale locale, final File dictFile, final String dictNamePrefix, @Nullable final String account)95 public static UserBinaryDictionary getDictionary( 96 final Context context, final Locale locale, final File dictFile, 97 final String dictNamePrefix, @Nullable final String account) { 98 return new UserBinaryDictionary( 99 context, locale, false /* alsoUseMoreRestrictiveLocales */, 100 dictFile, dictNamePrefix + NAME); 101 } 102 103 @Override close()104 public synchronized void close() { 105 if (mObserver != null) { 106 mContext.getContentResolver().unregisterContentObserver(mObserver); 107 mObserver = null; 108 } 109 super.close(); 110 } 111 112 @Override loadInitialContentsLocked()113 public void loadInitialContentsLocked() { 114 // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"], 115 // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3. 116 // This is correct for locale processing. 117 // For this example, we'll look at the "en_US_POSIX" case. 118 final String[] localeElements = 119 TextUtils.isEmpty(mLocaleString) ? new String[] {} : mLocaleString.split("_", 3); 120 final int length = localeElements.length; 121 122 final StringBuilder request = new StringBuilder("(locale is NULL)"); 123 String localeSoFar = ""; 124 // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ; 125 // and request = "(locale is NULL)" 126 for (int i = 0; i < length; ++i) { 127 // i | localeSoFar | localeElements 128 // 0 | "" | ["en", "US", "POSIX"] 129 // 1 | "en_" | ["en", "US", "POSIX"] 130 // 2 | "en_US_" | ["en", "en_US", "POSIX"] 131 localeElements[i] = localeSoFar + localeElements[i]; 132 localeSoFar = localeElements[i] + "_"; 133 // i | request 134 // 0 | "(locale is NULL)" 135 // 1 | "(locale is NULL) or (locale=?)" 136 // 2 | "(locale is NULL) or (locale=?) or (locale=?)" 137 request.append(" or (locale=?)"); 138 } 139 // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_" 140 // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)" 141 142 final String[] requestArguments; 143 // If length == 3, we already have all the arguments we need (common prefix is meaningless 144 // inside variants 145 if (mAlsoUseMoreRestrictiveLocales && length < 3) { 146 request.append(" or (locale like ?)"); 147 // The following creates an array with one more (null) position 148 final String[] localeElementsWithMoreRestrictiveLocalesIncluded = 149 Arrays.copyOf(localeElements, length + 1); 150 localeElementsWithMoreRestrictiveLocalesIncluded[length] = 151 localeElements[length - 1] + "_%"; 152 requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded; 153 // If for example localeElements = ["en"] 154 // then requestArguments = ["en", "en_%"] 155 // and request = (locale is NULL) or (locale=?) or (locale like ?) 156 // If localeElements = ["en", "en_US"] 157 // then requestArguments = ["en", "en_US", "en_US_%"] 158 } else { 159 requestArguments = localeElements; 160 } 161 final String requestString = request.toString(); 162 addWordsFromProjectionLocked(PROJECTION_QUERY, requestString, requestArguments); 163 } 164 addWordsFromProjectionLocked(final String[] query, String request, final String[] requestArguments)165 private void addWordsFromProjectionLocked(final String[] query, String request, 166 final String[] requestArguments) 167 throws IllegalArgumentException { 168 Cursor cursor = null; 169 try { 170 cursor = mContext.getContentResolver().query( 171 Words.CONTENT_URI, query, request, requestArguments, null); 172 addWordsLocked(cursor); 173 } catch (final SQLiteException e) { 174 Log.e(TAG, "SQLiteException in the remote User dictionary process.", e); 175 } finally { 176 try { 177 if (null != cursor) cursor.close(); 178 } catch (final SQLiteException e) { 179 Log.e(TAG, "SQLiteException in the remote User dictionary process.", e); 180 } 181 } 182 } 183 scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency)184 private static int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) { 185 // The default frequency for the user dictionary is 250 for historical reasons. 186 // Latin IME considers a good value for the default user dictionary frequency 187 // is about 160 considering the scale we use. So we are scaling down the values. 188 if (defaultFrequency > Integer.MAX_VALUE / LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY) { 189 return (defaultFrequency / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY) 190 * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY; 191 } 192 return (defaultFrequency * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY) 193 / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY; 194 } 195 addWordsLocked(final Cursor cursor)196 private void addWordsLocked(final Cursor cursor) { 197 if (cursor == null) return; 198 if (cursor.moveToFirst()) { 199 final int indexWord = cursor.getColumnIndex(Words.WORD); 200 final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY); 201 while (!cursor.isAfterLast()) { 202 final String word = cursor.getString(indexWord); 203 final int frequency = cursor.getInt(indexFrequency); 204 final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency); 205 // Safeguard against adding really long words. 206 if (word.length() <= MAX_WORD_LENGTH) { 207 runGCIfRequiredLocked(true /* mindsBlockByGC */); 208 addUnigramLocked(word, adjustedFrequency, false /* isNotAWord */, 209 false /* isPossiblyOffensive */, 210 BinaryDictionary.NOT_A_VALID_TIMESTAMP); 211 } 212 cursor.moveToNext(); 213 } 214 } 215 } 216 } 217