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 51 public UserDictionary(final Context context, final String locale) { 52 this(context, locale, false); 53 } 54 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 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 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 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 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 Cursor cursor = null; 183 try { 184 cursor = client.query(Words.CONTENT_URI, PROJECTION_ADD, 185 "word=? and ((locale IS NULL) or (locale=?))", 186 new String[] { word, mLocale }, null); 187 if (cursor != null && cursor.moveToFirst()) { 188 final String locale = cursor.getString(cursor.getColumnIndex(Words.LOCALE)); 189 // If locale is null, we will not override the entry. 190 if (locale != null && locale.equals(mLocale.toString())) { 191 final long id = cursor.getLong(cursor.getColumnIndex(Words._ID)); 192 final Uri uri = 193 Uri.withAppendedPath(Words.CONTENT_URI, Long.toString(id)); 194 // Update the entry with new frequency value. 195 client.update(uri, values, null, null); 196 } 197 } else { 198 // Insert new entry. 199 client.insert(Words.CONTENT_URI, values); 200 } 201 } catch (RemoteException e) { 202 // If we come here, the activity is already about to be killed, and we 203 // have no means of contacting the content provider any more. 204 // See ContentResolver#insert, inside the catch(){} 205 } finally { 206 if (null != cursor) cursor.close(); 207 client.release(); 208 } 209 } 210 }.start(); 211 212 // In case the above does a synchronous callback of the change observer 213 setRequiresReload(false); 214 } 215 216 @Override 217 public synchronized void getWords(final WordComposer codes, final WordCallback callback, 218 final ProximityInfo proximityInfo) { 219 super.getWords(codes, callback, proximityInfo); 220 } 221 222 @Override 223 public synchronized boolean isValidWord(CharSequence word) { 224 return super.isValidWord(word); 225 } 226 227 private void addWords(Cursor cursor) { 228 clearDictionary(); 229 if (cursor == null) return; 230 final int maxWordLength = getMaxWordLength(); 231 if (cursor.moveToFirst()) { 232 final int indexWord = cursor.getColumnIndex(Words.WORD); 233 final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY); 234 while (!cursor.isAfterLast()) { 235 String word = cursor.getString(indexWord); 236 int frequency = cursor.getInt(indexFrequency); 237 // Safeguard against adding really long words. Stack may overflow due 238 // to recursion 239 if (word.length() < maxWordLength) { 240 super.addWord(word, frequency); 241 } 242 cursor.moveToNext(); 243 } 244 } 245 cursor.close(); 246 } 247 } 248