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 com.android.inputmethod.latin.personalization.AccountUtils; 20 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.database.ContentObserver; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteException; 26 import android.net.Uri; 27 import android.os.SystemClock; 28 import android.provider.BaseColumns; 29 import android.provider.ContactsContract; 30 import android.provider.ContactsContract.Contacts; 31 import android.text.TextUtils; 32 import android.util.Log; 33 34 import com.android.inputmethod.latin.utils.StringUtils; 35 36 import java.util.List; 37 import java.util.Locale; 38 39 public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { 40 41 private static final String[] PROJECTION = {BaseColumns._ID, Contacts.DISPLAY_NAME}; 42 private static final String[] PROJECTION_ID_ONLY = {BaseColumns._ID}; 43 44 private static final String TAG = ContactsBinaryDictionary.class.getSimpleName(); 45 private static final String NAME = "contacts"; 46 47 private static boolean DEBUG = false; 48 49 /** 50 * Frequency for contacts information into the dictionary 51 */ 52 private static final int FREQUENCY_FOR_CONTACTS = 40; 53 private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90; 54 55 /** The maximum number of contacts that this dictionary supports. */ 56 private static final int MAX_CONTACT_COUNT = 10000; 57 58 private static final int INDEX_NAME = 1; 59 60 /** The number of contacts in the most recent dictionary rebuild. */ 61 static private int sContactCountAtLastRebuild = 0; 62 63 /** The locale for this contacts dictionary. Controls name bigram predictions. */ 64 public final Locale mLocale; 65 66 private ContentObserver mObserver; 67 68 /** 69 * Whether to use "firstname lastname" in bigram predictions. 70 */ 71 private final boolean mUseFirstLastBigrams; 72 73 public ContactsBinaryDictionary(final Context context, final Locale locale) { 74 super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS, 75 false /* isUpdatable */); 76 mLocale = locale; 77 mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale); 78 registerObserver(context); 79 80 // Load the current binary dictionary from internal storage. If no binary dictionary exists, 81 // loadDictionary will start a new thread to generate one asynchronously. 82 loadDictionary(); 83 } 84 85 private synchronized void registerObserver(final Context context) { 86 // Perform a managed query. The Activity will handle closing and requerying the cursor 87 // when needed. 88 if (mObserver != null) return; 89 ContentResolver cres = context.getContentResolver(); 90 cres.registerContentObserver(Contacts.CONTENT_URI, true, mObserver = 91 new ContentObserver(null) { 92 @Override 93 public void onChange(boolean self) { 94 setRequiresReload(true); 95 } 96 }); 97 } 98 99 public void reopen(final Context context) { 100 registerObserver(context); 101 } 102 103 @Override 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 113 public void loadDictionaryAsync() { 114 loadDeviceAccountsEmailAddresses(); 115 loadDictionaryAsyncForUri(ContactsContract.Profile.CONTENT_URI); 116 // TODO: Switch this URL to the newer ContactsContract too 117 loadDictionaryAsyncForUri(Contacts.CONTENT_URI); 118 } 119 120 private void loadDeviceAccountsEmailAddresses() { 121 final List<String> accountVocabulary = 122 AccountUtils.getDeviceAccountsEmailAddresses(mContext); 123 if (accountVocabulary == null || accountVocabulary.isEmpty()) { 124 return; 125 } 126 for (String word : accountVocabulary) { 127 if (DEBUG) { 128 Log.d(TAG, "loadAccountVocabulary: " + word); 129 } 130 super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, 0 /* shortcutFreq */, 131 false /* isNotAWord */); 132 } 133 } 134 135 private void loadDictionaryAsyncForUri(final Uri uri) { 136 try { 137 Cursor cursor = mContext.getContentResolver() 138 .query(uri, PROJECTION, null, null, null); 139 if (cursor != null) { 140 try { 141 if (cursor.moveToFirst()) { 142 sContactCountAtLastRebuild = getContactCount(); 143 addWords(cursor); 144 } 145 } finally { 146 cursor.close(); 147 } 148 } 149 } catch (final SQLiteException e) { 150 Log.e(TAG, "SQLiteException in the remote Contacts process.", e); 151 } catch (final IllegalStateException e) { 152 Log.e(TAG, "Contacts DB is having problems", e); 153 } 154 } 155 156 private boolean useFirstLastBigramsForLocale(final Locale locale) { 157 // TODO: Add firstname/lastname bigram rules for other languages. 158 if (locale != null && locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) { 159 return true; 160 } 161 return false; 162 } 163 164 private void addWords(final Cursor cursor) { 165 int count = 0; 166 while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) { 167 String name = cursor.getString(INDEX_NAME); 168 if (isValidName(name)) { 169 addName(name); 170 ++count; 171 } 172 cursor.moveToNext(); 173 } 174 } 175 176 private int getContactCount() { 177 // TODO: consider switching to a rawQuery("select count(*)...") on the database if 178 // performance is a bottleneck. 179 try { 180 final Cursor cursor = mContext.getContentResolver().query( 181 Contacts.CONTENT_URI, PROJECTION_ID_ONLY, null, null, null); 182 if (cursor != null) { 183 try { 184 return cursor.getCount(); 185 } finally { 186 cursor.close(); 187 } 188 } 189 } catch (final SQLiteException e) { 190 Log.e(TAG, "SQLiteException in the remote Contacts process.", e); 191 } 192 return 0; 193 } 194 195 /** 196 * Adds the words in a name (e.g., firstname/lastname) to the binary dictionary along with their 197 * bigrams depending on locale. 198 */ 199 private void addName(final String name) { 200 int len = StringUtils.codePointCount(name); 201 String prevWord = null; 202 // TODO: Better tokenization for non-Latin writing systems 203 for (int i = 0; i < len; i++) { 204 if (Character.isLetter(name.codePointAt(i))) { 205 int end = getWordEndPosition(name, len, i); 206 String word = name.substring(i, end); 207 i = end - 1; 208 // Don't add single letter words, possibly confuses 209 // capitalization of i. 210 final int wordLen = StringUtils.codePointCount(word); 211 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) { 212 if (DEBUG) { 213 Log.d(TAG, "addName " + name + ", " + word + ", " + prevWord); 214 } 215 super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, 216 0 /* shortcutFreq */, false /* isNotAWord */); 217 if (!TextUtils.isEmpty(prevWord)) { 218 if (mUseFirstLastBigrams) { 219 super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM, 220 0 /* lastModifiedTime */); 221 } 222 } 223 prevWord = word; 224 } 225 } 226 } 227 } 228 229 /** 230 * Returns the index of the last letter in the word, starting from position startIndex. 231 */ 232 private static int getWordEndPosition(final String string, final int len, 233 final int startIndex) { 234 int end; 235 int cp = 0; 236 for (end = startIndex + 1; end < len; end += Character.charCount(cp)) { 237 cp = string.codePointAt(end); 238 if (!(cp == Constants.CODE_DASH || cp == Constants.CODE_SINGLE_QUOTE 239 || Character.isLetter(cp))) { 240 break; 241 } 242 } 243 return end; 244 } 245 246 @Override 247 protected boolean needsToReloadBeforeWriting() { 248 return true; 249 } 250 251 @Override 252 protected boolean hasContentChanged() { 253 final long startTime = SystemClock.uptimeMillis(); 254 final int contactCount = getContactCount(); 255 if (contactCount > MAX_CONTACT_COUNT) { 256 // If there are too many contacts then return false. In this rare case it is impossible 257 // to include all of them anyways and the cost of rebuilding the dictionary is too high. 258 // TODO: Sort and check only the MAX_CONTACT_COUNT most recent contacts? 259 return false; 260 } 261 if (contactCount != sContactCountAtLastRebuild) { 262 if (DEBUG) { 263 Log.d(TAG, "Contact count changed: " + sContactCountAtLastRebuild + " to " 264 + contactCount); 265 } 266 return true; 267 } 268 // Check all contacts since it's not possible to find out which names have changed. 269 // This is needed because it's possible to receive extraneous onChange events even when no 270 // name has changed. 271 Cursor cursor = mContext.getContentResolver().query( 272 Contacts.CONTENT_URI, PROJECTION, null, null, null); 273 if (cursor != null) { 274 try { 275 if (cursor.moveToFirst()) { 276 while (!cursor.isAfterLast()) { 277 String name = cursor.getString(INDEX_NAME); 278 if (isValidName(name) && !isNameInDictionary(name)) { 279 if (DEBUG) { 280 Log.d(TAG, "Contact name missing: " + name + " (runtime = " 281 + (SystemClock.uptimeMillis() - startTime) + " ms)"); 282 } 283 return true; 284 } 285 cursor.moveToNext(); 286 } 287 } 288 } finally { 289 cursor.close(); 290 } 291 } 292 if (DEBUG) { 293 Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime) 294 + " ms)"); 295 } 296 return false; 297 } 298 299 private static boolean isValidName(final String name) { 300 if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) { 301 return true; 302 } 303 return false; 304 } 305 306 /** 307 * Checks if the words in a name are in the current binary dictionary. 308 */ 309 private boolean isNameInDictionary(final String name) { 310 int len = StringUtils.codePointCount(name); 311 String prevWord = null; 312 for (int i = 0; i < len; i++) { 313 if (Character.isLetter(name.codePointAt(i))) { 314 int end = getWordEndPosition(name, len, i); 315 String word = name.substring(i, end); 316 i = end - 1; 317 final int wordLen = StringUtils.codePointCount(word); 318 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) { 319 if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) { 320 if (!super.isValidBigramLocked(prevWord, word)) { 321 return false; 322 } 323 } else { 324 if (!super.isValidWordLocked(word)) { 325 return false; 326 } 327 } 328 prevWord = word; 329 } 330 } 331 } 332 return true; 333 } 334 } 335