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