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