1 /* 2 * Copyright (C) 2008 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.ContentValues; 22 import android.content.Context; 23 import android.database.ContentObserver; 24 import android.database.Cursor; 25 import android.net.Uri; 26 import android.os.RemoteException; 27 import android.provider.UserDictionary.Words; 28 import android.text.TextUtils; 29 30 import com.android.inputmethod.keyboard.ProximityInfo; 31 32 import java.util.Arrays; 33 34 public class UserDictionary extends ExpandableDictionary { 35 36 private static final String[] PROJECTION_QUERY = { 37 Words.WORD, 38 Words.FREQUENCY, 39 }; 40 41 private static final String[] PROJECTION_ADD = { 42 Words._ID, 43 Words.FREQUENCY, 44 Words.LOCALE, 45 }; 46 47 private ContentObserver mObserver; 48 final private String mLocale; 49 final private boolean mAlsoUseMoreRestrictiveLocales; 50 UserDictionary(final Context context, final String locale)51 public UserDictionary(final Context context, final String locale) { 52 this(context, locale, false); 53 } 54 UserDictionary(final Context context, final String locale, final boolean alsoUseMoreRestrictiveLocales)55 public UserDictionary(final Context context, final String locale, 56 final boolean alsoUseMoreRestrictiveLocales) { 57 super(context, Suggest.DIC_USER); 58 if (null == locale) throw new NullPointerException(); // Catch the error earlier 59 mLocale = locale; 60 mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales; 61 // Perform a managed query. The Activity will handle closing and re-querying the cursor 62 // when needed. 63 ContentResolver cres = context.getContentResolver(); 64 65 mObserver = new ContentObserver(null) { 66 @Override 67 public void onChange(boolean self) { 68 setRequiresReload(true); 69 } 70 }; 71 cres.registerContentObserver(Words.CONTENT_URI, true, mObserver); 72 73 loadDictionary(); 74 } 75 76 @Override close()77 public synchronized void close() { 78 if (mObserver != null) { 79 getContext().getContentResolver().unregisterContentObserver(mObserver); 80 mObserver = null; 81 } 82 super.close(); 83 } 84 85 @Override loadDictionaryAsync()86 public void loadDictionaryAsync() { 87 // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"], 88 // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3. 89 // This is correct for locale processing. 90 // For this example, we'll look at the "en_US_POSIX" case. 91 final String[] localeElements = 92 TextUtils.isEmpty(mLocale) ? new String[] {} : mLocale.split("_", 3); 93 final int length = localeElements.length; 94 95 final StringBuilder request = new StringBuilder("(locale is NULL)"); 96 String localeSoFar = ""; 97 // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ; 98 // and request = "(locale is NULL)" 99 for (int i = 0; i < length; ++i) { 100 // i | localeSoFar | localeElements 101 // 0 | "" | ["en", "US", "POSIX"] 102 // 1 | "en_" | ["en", "US", "POSIX"] 103 // 2 | "en_US_" | ["en", "en_US", "POSIX"] 104 localeElements[i] = localeSoFar + localeElements[i]; 105 localeSoFar = localeElements[i] + "_"; 106 // i | request 107 // 0 | "(locale is NULL)" 108 // 1 | "(locale is NULL) or (locale=?)" 109 // 2 | "(locale is NULL) or (locale=?) or (locale=?)" 110 request.append(" or (locale=?)"); 111 } 112 // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_" 113 // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)" 114 115 final String[] requestArguments; 116 // If length == 3, we already have all the arguments we need (common prefix is meaningless 117 // inside variants 118 if (mAlsoUseMoreRestrictiveLocales && length < 3) { 119 request.append(" or (locale like ?)"); 120 // The following creates an array with one more (null) position 121 final String[] localeElementsWithMoreRestrictiveLocalesIncluded = 122 Arrays.copyOf(localeElements, length + 1); 123 localeElementsWithMoreRestrictiveLocalesIncluded[length] = 124 localeElements[length - 1] + "_%"; 125 requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded; 126 // If for example localeElements = ["en"] 127 // then requestArguments = ["en", "en_%"] 128 // and request = (locale is NULL) or (locale=?) or (locale like ?) 129 // If localeElements = ["en", "en_US"] 130 // then requestArguments = ["en", "en_US", "en_US_%"] 131 } else { 132 requestArguments = localeElements; 133 } 134 final Cursor cursor = getContext().getContentResolver() 135 .query(Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), 136 requestArguments, null); 137 addWords(cursor); 138 } 139 isEnabled()140 public boolean isEnabled() { 141 final ContentResolver cr = getContext().getContentResolver(); 142 final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI); 143 if (client != null) { 144 client.release(); 145 return true; 146 } else { 147 return false; 148 } 149 } 150 151 /** 152 * Adds a word to the dictionary and makes it persistent. 153 * @param word the word to add. If the word is capitalized, then the dictionary will 154 * recognize it as a capitalized word when searched. 155 * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered 156 * the highest. 157 * @TODO use a higher or float range for frequency 158 */ 159 @Override addWord(final String word, final int frequency)160 public synchronized void addWord(final String word, final int frequency) { 161 // Force load the dictionary here synchronously 162 if (getRequiresReload()) loadDictionaryAsync(); 163 // Safeguard against adding long words. Can cause stack overflow. 164 if (word.length() >= getMaxWordLength()) return; 165 166 super.addWord(word, frequency); 167 168 // Update the user dictionary provider 169 final ContentValues values = new ContentValues(5); 170 values.put(Words.WORD, word); 171 values.put(Words.FREQUENCY, frequency); 172 values.put(Words.LOCALE, mLocale); 173 values.put(Words.APP_ID, 0); 174 175 final ContentResolver contentResolver = getContext().getContentResolver(); 176 final ContentProviderClient client = 177 contentResolver.acquireContentProviderClient(Words.CONTENT_URI); 178 if (null == client) return; 179 new Thread("addWord") { 180 @Override 181 public void run() { 182 try { 183 final Cursor cursor = client.query(Words.CONTENT_URI, PROJECTION_ADD, 184 "word=? and ((locale IS NULL) or (locale=?))", 185 new String[] { word, mLocale }, null); 186 if (cursor != null && cursor.moveToFirst()) { 187 final String locale = cursor.getString(cursor.getColumnIndex(Words.LOCALE)); 188 // If locale is null, we will not override the entry. 189 if (locale != null && locale.equals(mLocale.toString())) { 190 final long id = cursor.getLong(cursor.getColumnIndex(Words._ID)); 191 final Uri uri = 192 Uri.withAppendedPath(Words.CONTENT_URI, Long.toString(id)); 193 // Update the entry with new frequency value. 194 client.update(uri, values, null, null); 195 } 196 } else { 197 // Insert new entry. 198 client.insert(Words.CONTENT_URI, values); 199 } 200 } catch (RemoteException e) { 201 // If we come here, the activity is already about to be killed, and we 202 // have no means of contacting the content provider any more. 203 // See ContentResolver#insert, inside the catch(){} 204 } 205 } 206 }.start(); 207 208 // In case the above does a synchronous callback of the change observer 209 setRequiresReload(false); 210 } 211 212 @Override getWords(final WordComposer codes, final WordCallback callback, final ProximityInfo proximityInfo)213 public synchronized void getWords(final WordComposer codes, final WordCallback callback, 214 final ProximityInfo proximityInfo) { 215 super.getWords(codes, callback, proximityInfo); 216 } 217 218 @Override isValidWord(CharSequence word)219 public synchronized boolean isValidWord(CharSequence word) { 220 return super.isValidWord(word); 221 } 222 addWords(Cursor cursor)223 private void addWords(Cursor cursor) { 224 clearDictionary(); 225 if (cursor == null) return; 226 final int maxWordLength = getMaxWordLength(); 227 if (cursor.moveToFirst()) { 228 final int indexWord = cursor.getColumnIndex(Words.WORD); 229 final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY); 230 while (!cursor.isAfterLast()) { 231 String word = cursor.getString(indexWord); 232 int frequency = cursor.getInt(indexFrequency); 233 // Safeguard against adding really long words. Stack may overflow due 234 // to recursion 235 if (word.length() < maxWordLength) { 236 super.addWord(word, frequency); 237 } 238 cursor.moveToNext(); 239 } 240 } 241 cursor.close(); 242 } 243 } 244