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