1 /* 2 * Copyright (C) 2009 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.providers.contacts; 18 19 import com.android.providers.contacts.ContactMatcher.MatchScore; 20 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; 21 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; 22 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; 23 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; 24 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; 25 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns; 26 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; 27 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; 28 import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 29 30 import android.content.ContentValues; 31 import android.database.Cursor; 32 import android.database.DatabaseUtils; 33 import android.database.sqlite.SQLiteDatabase; 34 import android.database.sqlite.SQLiteQueryBuilder; 35 import android.database.sqlite.SQLiteStatement; 36 import android.net.Uri; 37 import android.provider.ContactsContract.AggregationExceptions; 38 import android.provider.ContactsContract.Contacts; 39 import android.provider.ContactsContract.Data; 40 import android.provider.ContactsContract.DisplayNameSources; 41 import android.provider.ContactsContract.RawContacts; 42 import android.provider.ContactsContract.StatusUpdates; 43 import android.provider.ContactsContract.CommonDataKinds.Email; 44 import android.provider.ContactsContract.CommonDataKinds.Phone; 45 import android.provider.ContactsContract.CommonDataKinds.Photo; 46 import android.text.TextUtils; 47 import android.util.EventLog; 48 import android.util.Log; 49 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.Iterator; 55 import java.util.List; 56 57 58 /** 59 * ContactAggregator deals with aggregating contact information coming from different sources. 60 * Two John Doe contacts from two disjoint sources are presumed to be the same 61 * person unless the user declares otherwise. 62 */ 63 public class ContactAggregator { 64 65 private static final String TAG = "ContactAggregator"; 66 67 private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 68 69 private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL = 70 NameLookupColumns.NAME_TYPE + " IN (" 71 + NameLookupType.NAME_EXACT + "," 72 + NameLookupType.NAME_VARIANT + "," 73 + NameLookupType.NAME_COLLATION_KEY + ")"; 74 75 // From system/core/logcat/event-log-tags 76 // aggregator [time, count] will be logged for each aggregator cycle. 77 // For the query (as opposed to the merge), count will be negative 78 public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747; 79 80 // If we encounter more than this many contacts with matching names, aggregate only this many 81 private static final int PRIMARY_HIT_LIMIT = 15; 82 private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT); 83 84 // If we encounter more than this many contacts with matching phone number or email, 85 // don't attempt to aggregate - this is likely an error or a shared corporate data element. 86 private static final int SECONDARY_HIT_LIMIT = 20; 87 private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT); 88 89 // If we encounter more than this many contacts with matching name during aggregation 90 // suggestion lookup, ignore the remaining results. 91 private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100; 92 93 private final ContactsProvider2 mContactsProvider; 94 private final ContactsDatabaseHelper mDbHelper; 95 private PhotoPriorityResolver mPhotoPriorityResolver; 96 private boolean mEnabled = true; 97 98 /** Precompiled sql statement for setting an aggregated presence */ 99 private SQLiteStatement mAggregatedPresenceReplace; 100 private SQLiteStatement mPresenceContactIdUpdate; 101 private SQLiteStatement mRawContactCountQuery; 102 private SQLiteStatement mContactDelete; 103 private SQLiteStatement mAggregatedPresenceDelete; 104 private SQLiteStatement mMarkForAggregation; 105 private SQLiteStatement mPhotoIdUpdate; 106 private SQLiteStatement mDisplayNameUpdate; 107 private SQLiteStatement mHasPhoneNumberUpdate; 108 private SQLiteStatement mLookupKeyUpdate; 109 private SQLiteStatement mStarredUpdate; 110 private SQLiteStatement mContactIdAndMarkAggregatedUpdate; 111 private SQLiteStatement mContactIdUpdate; 112 private SQLiteStatement mMarkAggregatedUpdate; 113 private SQLiteStatement mContactUpdate; 114 private SQLiteStatement mContactInsert; 115 116 private HashMap<Long, Integer> mRawContactsMarkedForAggregation = new HashMap<Long, Integer>(); 117 118 private String[] mSelectionArgs1 = new String[1]; 119 private String[] mSelectionArgs2 = new String[2]; 120 private String[] mSelectionArgs3 = new String[3]; 121 private long mMimeTypeIdEmail; 122 private long mMimeTypeIdPhoto; 123 private long mMimeTypeIdPhone; 124 private String mRawContactsQueryByRawContactId; 125 private String mRawContactsQueryByContactId; 126 private StringBuilder mSb = new StringBuilder(); 127 private MatchCandidateList mCandidates = new MatchCandidateList(); 128 private ContactMatcher mMatcher = new ContactMatcher(); 129 private ContentValues mValues = new ContentValues(); 130 private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate(); 131 132 /** 133 * Captures a potential match for a given name. The matching algorithm 134 * constructs a bunch of NameMatchCandidate objects for various potential matches 135 * and then executes the search in bulk. 136 */ 137 private static class NameMatchCandidate { 138 String mName; 139 int mLookupType; 140 141 public NameMatchCandidate(String name, int nameLookupType) { 142 mName = name; 143 mLookupType = nameLookupType; 144 } 145 } 146 147 /** 148 * A list of {@link NameMatchCandidate} that keeps its elements even when the list is 149 * truncated. This is done for optimization purposes to avoid excessive object allocation. 150 */ 151 private static class MatchCandidateList { 152 private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>(); 153 private int mCount; 154 155 /** 156 * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists. 157 */ 158 public void add(String name, int nameLookupType) { 159 if (mCount >= mList.size()) { 160 mList.add(new NameMatchCandidate(name, nameLookupType)); 161 } else { 162 NameMatchCandidate candidate = mList.get(mCount); 163 candidate.mName = name; 164 candidate.mLookupType = nameLookupType; 165 } 166 mCount++; 167 } 168 169 public void clear() { 170 mCount = 0; 171 } 172 } 173 174 /** 175 * A convenience class used in the algorithm that figures out which of available 176 * display names to use for an aggregate contact. 177 */ 178 private static class DisplayNameCandidate { 179 long rawContactId; 180 String displayName; 181 int displayNameSource; 182 boolean verified; 183 boolean writableAccount; 184 185 public DisplayNameCandidate() { 186 clear(); 187 } 188 189 public void clear() { 190 rawContactId = -1; 191 displayName = null; 192 displayNameSource = DisplayNameSources.UNDEFINED; 193 verified = false; 194 writableAccount = false; 195 } 196 } 197 198 /** 199 * Constructor. 200 */ 201 public ContactAggregator(ContactsProvider2 contactsProvider, 202 ContactsDatabaseHelper contactsDatabaseHelper, 203 PhotoPriorityResolver photoPriorityResolver) { 204 mContactsProvider = contactsProvider; 205 mDbHelper = contactsDatabaseHelper; 206 mPhotoPriorityResolver = photoPriorityResolver; 207 208 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 209 210 // Since we have no way of determining which custom status was set last, 211 // we'll just pick one randomly. We are using MAX as an approximation of randomness 212 final String replaceAggregatePresenceSql = 213 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "(" 214 + AggregatedPresenceColumns.CONTACT_ID + ", " 215 + StatusUpdates.PRESENCE_STATUS + ", " 216 + StatusUpdates.CHAT_CAPABILITY + ")" 217 + " SELECT " + PresenceColumns.CONTACT_ID + "," 218 + StatusUpdates.PRESENCE_STATUS + "," 219 + StatusUpdates.CHAT_CAPABILITY 220 + " FROM " + Tables.PRESENCE 221 + " WHERE " 222 + " (" + StatusUpdates.PRESENCE_STATUS 223 + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" 224 + " = (SELECT " 225 + "MAX (" + StatusUpdates.PRESENCE_STATUS 226 + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" 227 + " FROM " + Tables.PRESENCE 228 + " WHERE " + PresenceColumns.CONTACT_ID 229 + "=?)" 230 + " AND " + PresenceColumns.CONTACT_ID 231 + "=?;"; 232 mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql); 233 234 mRawContactCountQuery = db.compileStatement( 235 "SELECT COUNT(" + RawContacts._ID + ")" + 236 " FROM " + Tables.RAW_CONTACTS + 237 " WHERE " + RawContacts.CONTACT_ID + "=?" 238 + " AND " + RawContacts._ID + "<>?"); 239 240 mContactDelete = db.compileStatement( 241 "DELETE FROM " + Tables.CONTACTS + 242 " WHERE " + Contacts._ID + "=?"); 243 244 mAggregatedPresenceDelete = db.compileStatement( 245 "DELETE FROM " + Tables.AGGREGATED_PRESENCE + 246 " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?"); 247 248 mMarkForAggregation = db.compileStatement( 249 "UPDATE " + Tables.RAW_CONTACTS + 250 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" + 251 " WHERE " + RawContacts._ID + "=?" 252 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"); 253 254 mPhotoIdUpdate = db.compileStatement( 255 "UPDATE " + Tables.CONTACTS + 256 " SET " + Contacts.PHOTO_ID + "=? " + 257 " WHERE " + Contacts._ID + "=?"); 258 259 mDisplayNameUpdate = db.compileStatement( 260 "UPDATE " + Tables.CONTACTS + 261 " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " + 262 " WHERE " + Contacts._ID + "=?"); 263 264 mLookupKeyUpdate = db.compileStatement( 265 "UPDATE " + Tables.CONTACTS + 266 " SET " + Contacts.LOOKUP_KEY + "=? " + 267 " WHERE " + Contacts._ID + "=?"); 268 269 mHasPhoneNumberUpdate = db.compileStatement( 270 "UPDATE " + Tables.CONTACTS + 271 " SET " + Contacts.HAS_PHONE_NUMBER + "=" 272 + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)" 273 + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS 274 + " WHERE " + DataColumns.MIMETYPE_ID + "=?" 275 + " AND " + Phone.NUMBER + " NOT NULL" 276 + " AND " + RawContacts.CONTACT_ID + "=?)" + 277 " WHERE " + Contacts._ID + "=?"); 278 279 mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET " 280 + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED 281 + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE " 282 + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND " 283 + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?"); 284 285 mContactIdAndMarkAggregatedUpdate = db.compileStatement( 286 "UPDATE " + Tables.RAW_CONTACTS + 287 " SET " + RawContacts.CONTACT_ID + "=?, " 288 + RawContactsColumns.AGGREGATION_NEEDED + "=0" + 289 " WHERE " + RawContacts._ID + "=?"); 290 291 mContactIdUpdate = db.compileStatement( 292 "UPDATE " + Tables.RAW_CONTACTS + 293 " SET " + RawContacts.CONTACT_ID + "=?" + 294 " WHERE " + RawContacts._ID + "=?"); 295 296 mMarkAggregatedUpdate = db.compileStatement( 297 "UPDATE " + Tables.RAW_CONTACTS + 298 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" + 299 " WHERE " + RawContacts._ID + "=?"); 300 301 mPresenceContactIdUpdate = db.compileStatement( 302 "UPDATE " + Tables.PRESENCE + 303 " SET " + PresenceColumns.CONTACT_ID + "=?" + 304 " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?"); 305 306 mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL); 307 mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL); 308 309 mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); 310 mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE); 311 mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); 312 313 // Query used to retrieve data from raw contacts to populate the corresponding aggregate 314 mRawContactsQueryByRawContactId = String.format( 315 RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID, 316 mMimeTypeIdPhoto, mMimeTypeIdPhone); 317 318 mRawContactsQueryByContactId = String.format( 319 RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID, 320 mMimeTypeIdPhoto, mMimeTypeIdPhone); 321 } 322 323 public void setEnabled(boolean enabled) { 324 mEnabled = enabled; 325 } 326 327 public boolean isEnabled() { 328 return mEnabled; 329 } 330 331 private interface AggregationQuery { 332 String SQL = 333 "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID + 334 ", " + RawContacts.ACCOUNT_TYPE + "," + RawContacts.ACCOUNT_NAME + 335 " FROM " + Tables.RAW_CONTACTS + 336 " WHERE " + RawContacts._ID + " IN("; 337 338 int _ID = 0; 339 int CONTACT_ID = 1; 340 int ACCOUNT_TYPE = 2; 341 int ACCOUNT_NAME = 3; 342 } 343 344 /** 345 * Aggregate all raw contacts that were marked for aggregation in the current transaction. 346 * Call just before committing the transaction. 347 */ 348 public void aggregateInTransaction(SQLiteDatabase db) { 349 int count = mRawContactsMarkedForAggregation.size(); 350 if (count == 0) { 351 return; 352 } 353 354 long start = System.currentTimeMillis(); 355 if (VERBOSE_LOGGING) { 356 Log.v(TAG, "Contact aggregation: " + count); 357 } 358 359 EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -count); 360 361 String selectionArgs[] = new String[count]; 362 363 int index = 0; 364 mSb.setLength(0); 365 mSb.append(AggregationQuery.SQL); 366 for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) { 367 if (index > 0) { 368 mSb.append(','); 369 } 370 mSb.append('?'); 371 selectionArgs[index++] = String.valueOf(rawContactId); 372 } 373 374 mSb.append(')'); 375 376 long rawContactIds[] = new long[count]; 377 long contactIds[] = new long[count]; 378 String accountTypes[] = new String[count]; 379 String accountNames[] = new String[count]; 380 Cursor c = db.rawQuery(mSb.toString(), selectionArgs); 381 try { 382 count = c.getCount(); 383 index = 0; 384 while (c.moveToNext()) { 385 rawContactIds[index] = c.getLong(AggregationQuery._ID); 386 contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID); 387 accountTypes[index] = c.getString(AggregationQuery.ACCOUNT_TYPE); 388 accountNames[index] = c.getString(AggregationQuery.ACCOUNT_NAME); 389 index++; 390 } 391 } finally { 392 c.close(); 393 } 394 395 for (int i = 0; i < count; i++) { 396 aggregateContact(db, rawContactIds[i], accountTypes[i], accountNames[i], contactIds[i], 397 mCandidates, mMatcher, mValues); 398 } 399 400 long elapsedTime = System.currentTimeMillis() - start; 401 EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, count); 402 403 if (VERBOSE_LOGGING) { 404 String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact"; 405 Log.i(TAG, "Contact aggregation complete: " + count + performance); 406 } 407 } 408 409 public void clearPendingAggregations() { 410 mRawContactsMarkedForAggregation.clear(); 411 } 412 413 public void markNewForAggregation(long rawContactId, int aggregationMode) { 414 mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode); 415 } 416 417 public void markForAggregation(long rawContactId, int aggregationMode, boolean force) { 418 if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) { 419 // As per ContactsContract documentation, default aggregation mode 420 // does not override a previously set mode 421 if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { 422 aggregationMode = mRawContactsMarkedForAggregation.get(rawContactId); 423 } 424 } else { 425 mMarkForAggregation.bindLong(1, rawContactId); 426 mMarkForAggregation.execute(); 427 } 428 429 mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode); 430 } 431 432 /** 433 * Creates a new contact based on the given raw contact. Does not perform aggregation. 434 */ 435 public void onRawContactInsert(SQLiteDatabase db, long rawContactId) { 436 mSelectionArgs1[0] = String.valueOf(rawContactId); 437 computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert); 438 long contactId = mContactInsert.executeInsert(); 439 setContactId(rawContactId, contactId); 440 mDbHelper.updateContactVisible(contactId); 441 } 442 443 private static final class RawContactIdAndAccountQuery { 444 public static final String TABLE = Tables.RAW_CONTACTS; 445 446 public static final String[] COLUMNS = { 447 RawContacts.CONTACT_ID, RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME }; 448 449 public static final String SELECTION = RawContacts._ID + "=?"; 450 451 public static final int CONTACT_ID = 0; 452 public static final int ACCOUNT_TYPE = 1; 453 public static final int ACCOUNT_NAME = 2; 454 } 455 456 public void aggregateContact(SQLiteDatabase db, long rawContactId) { 457 long contactId = 0; 458 String accountName = null; 459 String accountType = null; 460 mSelectionArgs1[0] = String.valueOf(rawContactId); 461 Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE, 462 RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION, 463 mSelectionArgs1, null, null, null); 464 try { 465 if (cursor.moveToFirst()) { 466 contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID); 467 accountType = cursor.getString(RawContactIdAndAccountQuery.ACCOUNT_TYPE); 468 accountName = cursor.getString(RawContactIdAndAccountQuery.ACCOUNT_NAME); 469 } 470 } finally { 471 cursor.close(); 472 } 473 aggregateContact(db, rawContactId, accountType, accountName, contactId); 474 } 475 476 /** 477 * Synchronously aggregate the specified contact assuming an open transaction. 478 */ 479 public void aggregateContact(SQLiteDatabase db, long rawContactId, String accountType, 480 String accountName, long currentContactId) { 481 if (!mEnabled) { 482 return; 483 } 484 485 MatchCandidateList candidates = new MatchCandidateList(); 486 ContactMatcher matcher = new ContactMatcher(); 487 ContentValues values = new ContentValues(); 488 489 aggregateContact(db, rawContactId, accountType, accountName, currentContactId, candidates, 490 matcher, values); 491 } 492 493 public void updateAggregateData(long contactId) { 494 if (!mEnabled) { 495 return; 496 } 497 498 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 499 computeAggregateData(db, contactId, mContactUpdate); 500 mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId); 501 mContactUpdate.execute(); 502 503 mDbHelper.updateContactVisible(contactId); 504 updateAggregatedPresence(contactId); 505 } 506 507 private void updateAggregatedPresence(long contactId) { 508 mAggregatedPresenceReplace.bindLong(1, contactId); 509 mAggregatedPresenceReplace.bindLong(2, contactId); 510 mAggregatedPresenceReplace.execute(); 511 } 512 513 /** 514 * Given a specific raw contact, finds all matching aggregate contacts and chooses the one 515 * with the highest match score. If no such contact is found, creates a new contact. 516 */ 517 private synchronized void aggregateContact(SQLiteDatabase db, long rawContactId, 518 String accountType, String accountName, long currentContactId, 519 MatchCandidateList candidates, ContactMatcher matcher, ContentValues values) { 520 521 int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; 522 523 Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId); 524 if (aggModeObject != null) { 525 aggregationMode = aggModeObject; 526 } 527 528 long contactId = -1; 529 long contactIdToSplit = -1; 530 531 if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { 532 candidates.clear(); 533 matcher.clear(); 534 535 contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher); 536 if (contactId == -1) { 537 contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher); 538 539 // If we found an aggregate to join, but it already contains raw contacts from 540 // the same account, not only will we not join it, but also we will split 541 // that other aggregate 542 if (contactId != -1 && contactId != currentContactId && 543 containsRawContactsFromAccount(db, contactId, accountType, accountName)) { 544 contactIdToSplit = contactId; 545 contactId = -1; 546 } 547 } 548 } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) { 549 return; 550 } 551 552 long currentContactContentsCount = 0; 553 554 if (currentContactId != 0) { 555 mRawContactCountQuery.bindLong(1, currentContactId); 556 mRawContactCountQuery.bindLong(2, rawContactId); 557 currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong(); 558 } 559 560 // If there are no other raw contacts in the current aggregate, we might as well reuse it. 561 // Also, if the aggregation mode is SUSPENDED, we must reuse the same aggregate. 562 if (contactId == -1 563 && currentContactId != 0 564 && (currentContactContentsCount == 0 565 || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) { 566 contactId = currentContactId; 567 } 568 569 if (contactId == currentContactId) { 570 // Aggregation unchanged 571 markAggregated(rawContactId); 572 } else if (contactId == -1) { 573 // Splitting an aggregate 574 createNewContactForRawContact(db, rawContactId); 575 if (currentContactContentsCount > 0) { 576 updateAggregateData(currentContactId); 577 } 578 } else { 579 // Joining with an existing aggregate 580 if (currentContactContentsCount == 0) { 581 // Delete a previous aggregate if it only contained this raw contact 582 mContactDelete.bindLong(1, currentContactId); 583 mContactDelete.execute(); 584 585 mAggregatedPresenceDelete.bindLong(1, currentContactId); 586 mAggregatedPresenceDelete.execute(); 587 } 588 589 setContactIdAndMarkAggregated(rawContactId, contactId); 590 computeAggregateData(db, contactId, mContactUpdate); 591 mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId); 592 mContactUpdate.execute(); 593 mDbHelper.updateContactVisible(contactId); 594 updateAggregatedPresence(contactId); 595 } 596 597 if (contactIdToSplit != -1) { 598 splitAutomaticallyAggregatedRawContacts(db, contactIdToSplit); 599 } 600 } 601 602 /** 603 * Returns true if the aggregate contains has any raw contacts from the specified account. 604 */ 605 private boolean containsRawContactsFromAccount( 606 SQLiteDatabase db, long contactId, String accountType, String accountName) { 607 String query; 608 String[] args; 609 if (accountType == null) { 610 query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS + 611 " WHERE " + RawContacts.CONTACT_ID + "=?" + 612 " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL " + 613 " AND " + RawContacts.ACCOUNT_NAME + " IS NULL "; 614 args = mSelectionArgs1; 615 args[0] = String.valueOf(contactId); 616 } else { 617 query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS + 618 " WHERE " + RawContacts.CONTACT_ID + "=?" + 619 " AND " + RawContacts.ACCOUNT_TYPE + "=?" + 620 " AND " + RawContacts.ACCOUNT_NAME + "=?"; 621 args = mSelectionArgs3; 622 args[0] = String.valueOf(contactId); 623 args[1] = accountType; 624 args[2] = accountName; 625 } 626 Cursor cursor = db.rawQuery(query, args); 627 try { 628 cursor.moveToFirst(); 629 return cursor.getInt(0) != 0; 630 } finally { 631 cursor.close(); 632 } 633 } 634 635 /** 636 * Breaks up an existing aggregate when a new raw contact is inserted that has 637 * comes from the same account as one of the raw contacts in this aggregate. 638 */ 639 private void splitAutomaticallyAggregatedRawContacts(SQLiteDatabase db, long contactId) { 640 mSelectionArgs1[0] = String.valueOf(contactId); 641 int count = (int) DatabaseUtils.longForQuery(db, 642 "SELECT COUNT(" + RawContacts._ID + ")" + 643 " FROM " + Tables.RAW_CONTACTS + 644 " WHERE " + RawContacts.CONTACT_ID + "=?", mSelectionArgs1); 645 if (count < 2) { 646 // A single-raw-contact aggregate does not need to be split up 647 return; 648 } 649 650 // Find all constituent raw contacts that are not held together by 651 // an explicit aggregation exception 652 String query = 653 "SELECT " + RawContacts._ID + 654 " FROM " + Tables.RAW_CONTACTS + 655 " WHERE " + RawContacts.CONTACT_ID + "=?" + 656 " AND " + RawContacts._ID + " NOT IN " + 657 "(SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + 658 " FROM " + Tables.AGGREGATION_EXCEPTIONS + 659 " WHERE " + AggregationExceptions.TYPE + "=" 660 + AggregationExceptions.TYPE_KEEP_TOGETHER + 661 " UNION SELECT " + AggregationExceptions.RAW_CONTACT_ID2 + 662 " FROM " + Tables.AGGREGATION_EXCEPTIONS + 663 " WHERE " + AggregationExceptions.TYPE + "=" 664 + AggregationExceptions.TYPE_KEEP_TOGETHER + 665 ")"; 666 Cursor cursor = db.rawQuery(query, mSelectionArgs1); 667 try { 668 // Process up to count-1 raw contact, leaving the last one alone. 669 for (int i = 0; i < count - 1; i++) { 670 if (!cursor.moveToNext()) { 671 break; 672 } 673 long rawContactId = cursor.getLong(0); 674 createNewContactForRawContact(db, rawContactId); 675 } 676 } finally { 677 cursor.close(); 678 } 679 if (contactId > 0) { 680 updateAggregateData(contactId); 681 } 682 } 683 684 /** 685 * Creates a stand-alone Contact for the given raw contact ID. 686 */ 687 private void createNewContactForRawContact(SQLiteDatabase db, long rawContactId) { 688 mSelectionArgs1[0] = String.valueOf(rawContactId); 689 computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, 690 mContactInsert); 691 long contactId = mContactInsert.executeInsert(); 692 setContactIdAndMarkAggregated(rawContactId, contactId); 693 mDbHelper.updateContactVisible(contactId); 694 setPresenceContactId(rawContactId, contactId); 695 updateAggregatedPresence(contactId); 696 } 697 698 /** 699 * Updates the contact ID for the specified contact. 700 */ 701 private void setContactId(long rawContactId, long contactId) { 702 mContactIdUpdate.bindLong(1, contactId); 703 mContactIdUpdate.bindLong(2, rawContactId); 704 mContactIdUpdate.execute(); 705 } 706 707 /** 708 * Marks the specified raw contact ID as aggregated 709 */ 710 private void markAggregated(long rawContactId) { 711 mMarkAggregatedUpdate.bindLong(1, rawContactId); 712 mMarkAggregatedUpdate.execute(); 713 } 714 715 /** 716 * Updates the contact ID for the specified contact and marks the raw contact as aggregated. 717 */ 718 private void setContactIdAndMarkAggregated(long rawContactId, long contactId) { 719 mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId); 720 mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId); 721 mContactIdAndMarkAggregatedUpdate.execute(); 722 } 723 724 private void setPresenceContactId(long rawContactId, long contactId) { 725 mPresenceContactIdUpdate.bindLong(1, contactId); 726 mPresenceContactIdUpdate.bindLong(2, rawContactId); 727 mPresenceContactIdUpdate.execute(); 728 } 729 730 interface AggregateExceptionPrefetchQuery { 731 String TABLE = Tables.AGGREGATION_EXCEPTIONS; 732 733 String[] COLUMNS = { 734 AggregationExceptions.RAW_CONTACT_ID1, 735 AggregationExceptions.RAW_CONTACT_ID2, 736 }; 737 738 int RAW_CONTACT_ID1 = 0; 739 int RAW_CONTACT_ID2 = 1; 740 } 741 742 // A set of raw contact IDs for which there are aggregation exceptions 743 private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>(); 744 private boolean mAggregationExceptionIdsValid; 745 746 public void invalidateAggregationExceptionCache() { 747 mAggregationExceptionIdsValid = false; 748 } 749 750 /** 751 * Finds all raw contact IDs for which there are aggregation exceptions. The list of 752 * ids is used as an optimization in aggregation: there is no point to run a query against 753 * the agg_exceptions table if it is known that there are no records there for a given 754 * raw contact ID. 755 */ 756 private void prefetchAggregationExceptionIds(SQLiteDatabase db) { 757 mAggregationExceptionIds.clear(); 758 final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE, 759 AggregateExceptionPrefetchQuery.COLUMNS, 760 null, null, null, null, null); 761 762 try { 763 while (c.moveToNext()) { 764 long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1); 765 long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2); 766 mAggregationExceptionIds.add(rawContactId1); 767 mAggregationExceptionIds.add(rawContactId2); 768 } 769 } finally { 770 c.close(); 771 } 772 773 mAggregationExceptionIdsValid = true; 774 } 775 776 interface AggregateExceptionQuery { 777 String TABLE = Tables.AGGREGATION_EXCEPTIONS 778 + " JOIN raw_contacts raw_contacts1 " 779 + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) " 780 + " JOIN raw_contacts raw_contacts2 " 781 + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) "; 782 783 String[] COLUMNS = { 784 AggregationExceptions.TYPE, 785 AggregationExceptions.RAW_CONTACT_ID1, 786 "raw_contacts1." + RawContacts.CONTACT_ID, 787 "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED, 788 "raw_contacts2." + RawContacts.CONTACT_ID, 789 "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED, 790 }; 791 792 int TYPE = 0; 793 int RAW_CONTACT_ID1 = 1; 794 int CONTACT_ID1 = 2; 795 int AGGREGATION_NEEDED_1 = 3; 796 int CONTACT_ID2 = 4; 797 int AGGREGATION_NEEDED_2 = 5; 798 } 799 800 /** 801 * Computes match scores based on exceptions entered by the user: always match and never match. 802 * Returns the aggregate contact with the always match exception if any. 803 */ 804 private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId, 805 ContactMatcher matcher) { 806 if (!mAggregationExceptionIdsValid) { 807 prefetchAggregationExceptionIds(db); 808 } 809 810 // If there are no aggregation exceptions involving this raw contact, there is no need to 811 // run a query and we can just return -1, which stands for "nothing found" 812 if (!mAggregationExceptionIds.contains(rawContactId)) { 813 return -1; 814 } 815 816 final Cursor c = db.query(AggregateExceptionQuery.TABLE, 817 AggregateExceptionQuery.COLUMNS, 818 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId 819 + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId, 820 null, null, null, null); 821 822 try { 823 while (c.moveToNext()) { 824 int type = c.getInt(AggregateExceptionQuery.TYPE); 825 long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1); 826 long contactId = -1; 827 if (rawContactId == rawContactId1) { 828 if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0 829 && !c.isNull(AggregateExceptionQuery.CONTACT_ID2)) { 830 contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2); 831 } 832 } else { 833 if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0 834 && !c.isNull(AggregateExceptionQuery.CONTACT_ID1)) { 835 contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1); 836 } 837 } 838 if (contactId != -1) { 839 if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) { 840 matcher.keepIn(contactId); 841 } else { 842 matcher.keepOut(contactId); 843 } 844 } 845 } 846 } finally { 847 c.close(); 848 } 849 850 return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true); 851 } 852 853 /** 854 * Picks the best matching contact based on matches between data elements. It considers 855 * name match to be primary and phone, email etc matches to be secondary. A good primary 856 * match triggers aggregation, while a good secondary match only triggers aggregation in 857 * the absence of a strong primary mismatch. 858 * <p> 859 * Consider these examples: 860 * <p> 861 * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should 862 * be aggregated (same number, similar names). 863 * <p> 864 * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should 865 * not be aggregated (same number, different names). 866 */ 867 private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId, 868 MatchCandidateList candidates, ContactMatcher matcher) { 869 870 // Find good matches based on name alone 871 long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, candidates, matcher); 872 if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) { 873 // We found multiple matches on the name - do not aggregate because of the ambiguity 874 return -1; 875 } else if (bestMatch == -1) { 876 // We haven't found a good match on name, see if we have any matches on phone, email etc 877 bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher); 878 if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) { 879 return -1; 880 } 881 } 882 883 return bestMatch; 884 } 885 886 887 /** 888 * Picks the best matching contact based on secondary data matches. The method loads 889 * structured names for all candidate contacts and recomputes match scores using approximate 890 * matching. 891 */ 892 private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db, 893 long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) { 894 List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates( 895 ContactMatcher.SCORE_THRESHOLD_PRIMARY); 896 if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) { 897 return -1; 898 } 899 900 loadNameMatchCandidates(db, rawContactId, candidates, true); 901 902 mSb.setLength(0); 903 mSb.append(RawContacts.CONTACT_ID).append(" IN ("); 904 for (int i = 0; i < secondaryContactIds.size(); i++) { 905 if (i != 0) { 906 mSb.append(','); 907 } 908 mSb.append(secondaryContactIds.get(i)); 909 } 910 911 // We only want to compare structured names to structured names 912 // at this stage, we need to ignore all other sources of name lookup data. 913 mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL); 914 915 matchAllCandidates(db, mSb.toString(), candidates, matcher, 916 ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null); 917 918 return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false); 919 } 920 921 private interface NameLookupQuery { 922 String TABLE = Tables.NAME_LOOKUP; 923 924 String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?"; 925 String SELECTION_STRUCTURED_NAME_BASED = 926 SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL; 927 928 String[] COLUMNS = new String[] { 929 NameLookupColumns.NORMALIZED_NAME, 930 NameLookupColumns.NAME_TYPE 931 }; 932 933 int NORMALIZED_NAME = 0; 934 int NAME_TYPE = 1; 935 } 936 937 private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId, 938 MatchCandidateList candidates, boolean structuredNameBased) { 939 candidates.clear(); 940 mSelectionArgs1[0] = String.valueOf(rawContactId); 941 Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS, 942 structuredNameBased 943 ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED 944 : NameLookupQuery.SELECTION, 945 mSelectionArgs1, null, null, null); 946 try { 947 while (c.moveToNext()) { 948 String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME); 949 int type = c.getInt(NameLookupQuery.NAME_TYPE); 950 candidates.add(normalizedName, type); 951 } 952 } finally { 953 c.close(); 954 } 955 } 956 957 /** 958 * Computes scores for contacts that have matching data rows. 959 */ 960 private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId, 961 MatchCandidateList candidates, ContactMatcher matcher) { 962 963 updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher); 964 long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY, false); 965 if (bestMatch != -1) { 966 return bestMatch; 967 } 968 969 updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher); 970 updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher); 971 972 return -1; 973 } 974 975 private interface NameLookupMatchQuery { 976 String TABLE = Tables.NAME_LOOKUP + " nameA" 977 + " JOIN " + Tables.NAME_LOOKUP + " nameB" + 978 " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "=" 979 + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")" 980 + " JOIN " + Tables.RAW_CONTACTS + 981 " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = " 982 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 983 984 String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?" 985 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"; 986 987 String[] COLUMNS = new String[] { 988 RawContacts.CONTACT_ID, 989 "nameA." + NameLookupColumns.NORMALIZED_NAME, 990 "nameA." + NameLookupColumns.NAME_TYPE, 991 "nameB." + NameLookupColumns.NAME_TYPE, 992 }; 993 994 int CONTACT_ID = 0; 995 int NAME = 1; 996 int NAME_TYPE_A = 2; 997 int NAME_TYPE_B = 3; 998 } 999 1000 private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId, 1001 ContactMatcher matcher) { 1002 mSelectionArgs1[0] = String.valueOf(rawContactId); 1003 Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS, 1004 NameLookupMatchQuery.SELECTION, 1005 mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING); 1006 try { 1007 while (c.moveToNext()) { 1008 long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID); 1009 String name = c.getString(NameLookupMatchQuery.NAME); 1010 int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A); 1011 int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B); 1012 matcher.matchName(contactId, nameTypeA, name, 1013 nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT); 1014 if (nameTypeA == NameLookupType.NICKNAME && 1015 nameTypeB == NameLookupType.NICKNAME) { 1016 matcher.updateScoreWithNicknameMatch(contactId); 1017 } 1018 } 1019 } finally { 1020 c.close(); 1021 } 1022 } 1023 1024 private interface EmailLookupQuery { 1025 String TABLE = Tables.DATA + " dataA" 1026 + " JOIN " + Tables.DATA + " dataB" + 1027 " ON (" + "dataA." + Email.DATA + "=dataB." + Email.DATA + ")" 1028 + " JOIN " + Tables.RAW_CONTACTS + 1029 " ON (dataB." + Data.RAW_CONTACT_ID + " = " 1030 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 1031 1032 String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" 1033 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?" 1034 + " AND dataA." + Email.DATA + " NOT NULL" 1035 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?" 1036 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"; 1037 1038 String[] COLUMNS = new String[] { 1039 RawContacts.CONTACT_ID 1040 }; 1041 1042 int CONTACT_ID = 0; 1043 } 1044 1045 private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, 1046 ContactMatcher matcher) { 1047 mSelectionArgs3[0] = String.valueOf(rawContactId); 1048 mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdEmail); 1049 Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS, 1050 EmailLookupQuery.SELECTION, 1051 mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING); 1052 try { 1053 while (c.moveToNext()) { 1054 long contactId = c.getLong(EmailLookupQuery.CONTACT_ID); 1055 matcher.updateScoreWithEmailMatch(contactId); 1056 } 1057 } finally { 1058 c.close(); 1059 } 1060 } 1061 1062 private interface PhoneLookupQuery { 1063 String TABLE = Tables.PHONE_LOOKUP + " phoneA" 1064 + " JOIN " + Tables.DATA + " dataA" 1065 + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")" 1066 + " JOIN " + Tables.PHONE_LOOKUP + " phoneB" 1067 + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "=" 1068 + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")" 1069 + " JOIN " + Tables.DATA + " dataB" 1070 + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")" 1071 + " JOIN " + Tables.RAW_CONTACTS 1072 + " ON (dataB." + Data.RAW_CONTACT_ID + " = " 1073 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 1074 1075 String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" 1076 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", " 1077 + "dataB." + Phone.NUMBER + ",?)" 1078 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"; 1079 1080 String[] COLUMNS = new String[] { 1081 RawContacts.CONTACT_ID 1082 }; 1083 1084 int CONTACT_ID = 0; 1085 } 1086 1087 private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId, 1088 ContactMatcher matcher) { 1089 mSelectionArgs2[0] = String.valueOf(rawContactId); 1090 mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter(); 1091 Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS, 1092 PhoneLookupQuery.SELECTION, 1093 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING); 1094 try { 1095 while (c.moveToNext()) { 1096 long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID); 1097 matcher.updateScoreWithPhoneNumberMatch(contactId); 1098 } 1099 } finally { 1100 c.close(); 1101 } 1102 1103 } 1104 1105 /** 1106 * Loads name lookup rows for approximate name matching and updates match scores based on that 1107 * data. 1108 */ 1109 private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates, 1110 ContactMatcher matcher) { 1111 HashSet<String> firstLetters = new HashSet<String>(); 1112 for (int i = 0; i < candidates.mCount; i++) { 1113 final NameMatchCandidate candidate = candidates.mList.get(i); 1114 if (candidate.mName.length() >= 2) { 1115 String firstLetter = candidate.mName.substring(0, 2); 1116 if (!firstLetters.contains(firstLetter)) { 1117 firstLetters.add(firstLetter); 1118 final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '" 1119 + firstLetter + "*') AND " 1120 + NameLookupColumns.NAME_TYPE + " IN(" 1121 + NameLookupType.NAME_COLLATION_KEY + "," 1122 + NameLookupType.EMAIL_BASED_NICKNAME + "," 1123 + NameLookupType.NICKNAME + ")"; 1124 matchAllCandidates(db, selection, candidates, matcher, 1125 ContactMatcher.MATCHING_ALGORITHM_APPROXIMATE, 1126 String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT)); 1127 } 1128 } 1129 } 1130 } 1131 1132 private interface ContactNameLookupQuery { 1133 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 1134 1135 String[] COLUMNS = new String[] { 1136 RawContacts.CONTACT_ID, 1137 NameLookupColumns.NORMALIZED_NAME, 1138 NameLookupColumns.NAME_TYPE 1139 }; 1140 1141 int CONTACT_ID = 0; 1142 int NORMALIZED_NAME = 1; 1143 int NAME_TYPE = 2; 1144 } 1145 1146 /** 1147 * Loads all candidate rows from the name lookup table and updates match scores based 1148 * on that data. 1149 */ 1150 private void matchAllCandidates(SQLiteDatabase db, String selection, 1151 MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) { 1152 final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS, 1153 selection, null, null, null, null, limit); 1154 1155 try { 1156 while (c.moveToNext()) { 1157 Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID); 1158 String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME); 1159 int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE); 1160 1161 // Note the N^2 complexity of the following fragment. This is not a huge concern 1162 // since the number of candidates is very small and in general secondary hits 1163 // in the absence of primary hits are rare. 1164 for (int i = 0; i < candidates.mCount; i++) { 1165 NameMatchCandidate candidate = candidates.mList.get(i); 1166 matcher.matchName(contactId, candidate.mLookupType, candidate.mName, 1167 nameType, name, algorithm); 1168 } 1169 } 1170 } finally { 1171 c.close(); 1172 } 1173 } 1174 1175 private interface RawContactsQuery { 1176 String SQL_FORMAT = 1177 "SELECT " 1178 + RawContactsColumns.CONCRETE_ID + "," 1179 + RawContactsColumns.DISPLAY_NAME + "," 1180 + RawContactsColumns.DISPLAY_NAME_SOURCE + "," 1181 + RawContacts.ACCOUNT_TYPE + "," 1182 + RawContacts.ACCOUNT_NAME + "," 1183 + RawContacts.SOURCE_ID + "," 1184 + RawContacts.CUSTOM_RINGTONE + "," 1185 + RawContacts.SEND_TO_VOICEMAIL + "," 1186 + RawContacts.LAST_TIME_CONTACTED + "," 1187 + RawContacts.TIMES_CONTACTED + "," 1188 + RawContacts.STARRED + "," 1189 + RawContacts.IS_RESTRICTED + "," 1190 + RawContacts.NAME_VERIFIED + "," 1191 + DataColumns.CONCRETE_ID + "," 1192 + DataColumns.CONCRETE_MIMETYPE_ID + "," 1193 + Data.IS_SUPER_PRIMARY + 1194 " FROM " + Tables.RAW_CONTACTS + 1195 " LEFT OUTER JOIN " + Tables.DATA + 1196 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID 1197 + " AND ((" + DataColumns.MIMETYPE_ID + "=%d" 1198 + " AND " + Photo.PHOTO + " NOT NULL)" 1199 + " OR (" + DataColumns.MIMETYPE_ID + "=%d" 1200 + " AND " + Phone.NUMBER + " NOT NULL)))"; 1201 1202 String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT + 1203 " WHERE " + RawContactsColumns.CONCRETE_ID + "=?"; 1204 1205 String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT + 1206 " WHERE " + RawContacts.CONTACT_ID + "=?" 1207 + " AND " + RawContacts.DELETED + "=0"; 1208 1209 int RAW_CONTACT_ID = 0; 1210 int DISPLAY_NAME = 1; 1211 int DISPLAY_NAME_SOURCE = 2; 1212 int ACCOUNT_TYPE = 3; 1213 int ACCOUNT_NAME = 4; 1214 int SOURCE_ID = 5; 1215 int CUSTOM_RINGTONE = 6; 1216 int SEND_TO_VOICEMAIL = 7; 1217 int LAST_TIME_CONTACTED = 8; 1218 int TIMES_CONTACTED = 9; 1219 int STARRED = 10; 1220 int IS_RESTRICTED = 11; 1221 int NAME_VERIFIED = 12; 1222 int DATA_ID = 13; 1223 int MIMETYPE_ID = 14; 1224 int IS_SUPER_PRIMARY = 15; 1225 } 1226 1227 private interface ContactReplaceSqlStatement { 1228 String UPDATE_SQL = 1229 "UPDATE " + Tables.CONTACTS + 1230 " SET " 1231 + Contacts.NAME_RAW_CONTACT_ID + "=?, " 1232 + Contacts.PHOTO_ID + "=?, " 1233 + Contacts.SEND_TO_VOICEMAIL + "=?, " 1234 + Contacts.CUSTOM_RINGTONE + "=?, " 1235 + Contacts.LAST_TIME_CONTACTED + "=?, " 1236 + Contacts.TIMES_CONTACTED + "=?, " 1237 + Contacts.STARRED + "=?, " 1238 + Contacts.HAS_PHONE_NUMBER + "=?, " 1239 + ContactsColumns.SINGLE_IS_RESTRICTED + "=?, " 1240 + Contacts.LOOKUP_KEY + "=? " + 1241 " WHERE " + Contacts._ID + "=?"; 1242 1243 String INSERT_SQL = 1244 "INSERT INTO " + Tables.CONTACTS + " (" 1245 + Contacts.NAME_RAW_CONTACT_ID + ", " 1246 + Contacts.PHOTO_ID + ", " 1247 + Contacts.SEND_TO_VOICEMAIL + ", " 1248 + Contacts.CUSTOM_RINGTONE + ", " 1249 + Contacts.LAST_TIME_CONTACTED + ", " 1250 + Contacts.TIMES_CONTACTED + ", " 1251 + Contacts.STARRED + ", " 1252 + Contacts.HAS_PHONE_NUMBER + ", " 1253 + ContactsColumns.SINGLE_IS_RESTRICTED + ", " 1254 + Contacts.LOOKUP_KEY + ", " 1255 + Contacts.IN_VISIBLE_GROUP + ") " + 1256 " VALUES (?,?,?,?,?,?,?,?,?,?,0)"; 1257 1258 int NAME_RAW_CONTACT_ID = 1; 1259 int PHOTO_ID = 2; 1260 int SEND_TO_VOICEMAIL = 3; 1261 int CUSTOM_RINGTONE = 4; 1262 int LAST_TIME_CONTACTED = 5; 1263 int TIMES_CONTACTED = 6; 1264 int STARRED = 7; 1265 int HAS_PHONE_NUMBER = 8; 1266 int SINGLE_IS_RESTRICTED = 9; 1267 int LOOKUP_KEY = 10; 1268 int CONTACT_ID = 11; 1269 } 1270 1271 /** 1272 * Computes aggregate-level data for the specified aggregate contact ID. 1273 */ 1274 private void computeAggregateData(SQLiteDatabase db, long contactId, 1275 SQLiteStatement statement) { 1276 mSelectionArgs1[0] = String.valueOf(contactId); 1277 computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement); 1278 } 1279 1280 /** 1281 * Computes aggregate-level data from constituent raw contacts. 1282 */ 1283 private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs, 1284 SQLiteStatement statement) { 1285 long currentRawContactId = -1; 1286 long bestPhotoId = -1; 1287 boolean foundSuperPrimaryPhoto = false; 1288 int photoPriority = -1; 1289 int totalRowCount = 0; 1290 int contactSendToVoicemail = 0; 1291 String contactCustomRingtone = null; 1292 long contactLastTimeContacted = 0; 1293 int contactTimesContacted = 0; 1294 int contactStarred = 0; 1295 int singleIsRestricted = 1; 1296 int hasPhoneNumber = 0; 1297 1298 mDisplayNameCandidate.clear(); 1299 1300 mSb.setLength(0); // Lookup key 1301 Cursor c = db.rawQuery(sql, sqlArgs); 1302 try { 1303 while (c.moveToNext()) { 1304 long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID); 1305 if (rawContactId != currentRawContactId) { 1306 currentRawContactId = rawContactId; 1307 totalRowCount++; 1308 1309 // Display name 1310 String displayName = c.getString(RawContactsQuery.DISPLAY_NAME); 1311 int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE); 1312 int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED); 1313 String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); 1314 processDisplayNameCanditate(rawContactId, displayName, displayNameSource, 1315 mContactsProvider.isWritableAccount(accountType), nameVerified != 0); 1316 1317 1318 // Contact options 1319 if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) { 1320 boolean sendToVoicemail = 1321 (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0); 1322 if (sendToVoicemail) { 1323 contactSendToVoicemail++; 1324 } 1325 } 1326 1327 if (contactCustomRingtone == null 1328 && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) { 1329 contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE); 1330 } 1331 1332 long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED); 1333 if (lastTimeContacted > contactLastTimeContacted) { 1334 contactLastTimeContacted = lastTimeContacted; 1335 } 1336 1337 int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED); 1338 if (timesContacted > contactTimesContacted) { 1339 contactTimesContacted = timesContacted; 1340 } 1341 1342 if (c.getInt(RawContactsQuery.STARRED) != 0) { 1343 contactStarred = 1; 1344 } 1345 1346 // Single restricted 1347 if (totalRowCount > 1) { 1348 // Not single 1349 singleIsRestricted = 0; 1350 } else { 1351 int isRestricted = c.getInt(RawContactsQuery.IS_RESTRICTED); 1352 1353 if (isRestricted == 0) { 1354 // Not restricted 1355 singleIsRestricted = 0; 1356 } 1357 } 1358 1359 ContactLookupKey.appendToLookupKey(mSb, 1360 c.getString(RawContactsQuery.ACCOUNT_TYPE), 1361 c.getString(RawContactsQuery.ACCOUNT_NAME), 1362 rawContactId, 1363 c.getString(RawContactsQuery.SOURCE_ID), 1364 displayName); 1365 } 1366 1367 if (!c.isNull(RawContactsQuery.DATA_ID)) { 1368 long dataId = c.getLong(RawContactsQuery.DATA_ID); 1369 int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID); 1370 boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0; 1371 if (mimetypeId == mMimeTypeIdPhoto) { 1372 if (!foundSuperPrimaryPhoto) { 1373 String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); 1374 int priority = mPhotoPriorityResolver.getPhotoPriority(accountType); 1375 if (superPrimary || priority > photoPriority) { 1376 photoPriority = priority; 1377 bestPhotoId = dataId; 1378 foundSuperPrimaryPhoto |= superPrimary; 1379 } 1380 } 1381 } else if (mimetypeId == mMimeTypeIdPhone) { 1382 hasPhoneNumber = 1; 1383 } 1384 } 1385 } 1386 } finally { 1387 c.close(); 1388 } 1389 1390 statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID, 1391 mDisplayNameCandidate.rawContactId); 1392 1393 if (bestPhotoId != -1) { 1394 statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId); 1395 } else { 1396 statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID); 1397 } 1398 1399 statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL, 1400 totalRowCount == contactSendToVoicemail ? 1 : 0); 1401 DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE, 1402 contactCustomRingtone); 1403 statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED, 1404 contactLastTimeContacted); 1405 statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED, 1406 contactTimesContacted); 1407 statement.bindLong(ContactReplaceSqlStatement.STARRED, 1408 contactStarred); 1409 statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER, 1410 hasPhoneNumber); 1411 statement.bindLong(ContactReplaceSqlStatement.SINGLE_IS_RESTRICTED, 1412 singleIsRestricted); 1413 statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY, 1414 Uri.encode(mSb.toString())); 1415 } 1416 1417 /** 1418 * Uses the supplied values to determine if they represent a "better" display name 1419 * for the aggregate contact currently evaluated. If so, it updates 1420 * {@link #mDisplayNameCandidate} with the new values. 1421 */ 1422 private void processDisplayNameCanditate(long rawContactId, String displayName, 1423 int displayNameSource, boolean writableAccount, boolean verified) { 1424 1425 boolean replace = false; 1426 if (mDisplayNameCandidate.rawContactId == -1) { 1427 // No previous values available 1428 replace = true; 1429 } else if (!TextUtils.isEmpty(displayName)) { 1430 if (!mDisplayNameCandidate.verified && verified) { 1431 // A verified name is better than any other name 1432 replace = true; 1433 } else if (mDisplayNameCandidate.verified == verified) { 1434 if (mDisplayNameCandidate.displayNameSource < displayNameSource) { 1435 // New values come from an superior source, e.g. structured name vs phone number 1436 replace = true; 1437 } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) { 1438 if (!mDisplayNameCandidate.writableAccount && writableAccount) { 1439 replace = true; 1440 } else if (mDisplayNameCandidate.writableAccount == writableAccount) { 1441 if (NameNormalizer.compareComplexity(displayName, 1442 mDisplayNameCandidate.displayName) > 0) { 1443 // New name is more complex than the previously found one 1444 replace = true; 1445 } 1446 } 1447 } 1448 } 1449 } 1450 1451 if (replace) { 1452 mDisplayNameCandidate.rawContactId = rawContactId; 1453 mDisplayNameCandidate.displayName = displayName; 1454 mDisplayNameCandidate.displayNameSource = displayNameSource; 1455 mDisplayNameCandidate.verified = verified; 1456 mDisplayNameCandidate.writableAccount = writableAccount; 1457 } 1458 } 1459 1460 private interface PhotoIdQuery { 1461 String[] COLUMNS = new String[] { 1462 RawContacts.ACCOUNT_TYPE, 1463 DataColumns.CONCRETE_ID, 1464 Data.IS_SUPER_PRIMARY, 1465 }; 1466 1467 int ACCOUNT_TYPE = 0; 1468 int DATA_ID = 1; 1469 int IS_SUPER_PRIMARY = 2; 1470 } 1471 1472 public void updatePhotoId(SQLiteDatabase db, long rawContactId) { 1473 1474 long contactId = mDbHelper.getContactId(rawContactId); 1475 if (contactId == 0) { 1476 return; 1477 } 1478 1479 long bestPhotoId = -1; 1480 int photoPriority = -1; 1481 1482 long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE); 1483 1484 String tables = Tables.RAW_CONTACTS + " JOIN " + Tables.DATA + " ON(" 1485 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID 1486 + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND " 1487 + Photo.PHOTO + " NOT NULL))"; 1488 1489 mSelectionArgs1[0] = String.valueOf(contactId); 1490 final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS, 1491 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null); 1492 try { 1493 while (c.moveToNext()) { 1494 long dataId = c.getLong(PhotoIdQuery.DATA_ID); 1495 boolean superprimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0; 1496 if (superprimary) { 1497 bestPhotoId = dataId; 1498 break; 1499 } 1500 1501 String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE); 1502 int priority = mPhotoPriorityResolver.getPhotoPriority(accountType); 1503 if (priority > photoPriority) { 1504 photoPriority = priority; 1505 bestPhotoId = dataId; 1506 } 1507 } 1508 } finally { 1509 c.close(); 1510 } 1511 1512 if (bestPhotoId == -1) { 1513 mPhotoIdUpdate.bindNull(1); 1514 } else { 1515 mPhotoIdUpdate.bindLong(1, bestPhotoId); 1516 } 1517 mPhotoIdUpdate.bindLong(2, contactId); 1518 mPhotoIdUpdate.execute(); 1519 } 1520 1521 private interface DisplayNameQuery { 1522 String[] COLUMNS = new String[] { 1523 RawContacts._ID, 1524 RawContactsColumns.DISPLAY_NAME, 1525 RawContactsColumns.DISPLAY_NAME_SOURCE, 1526 RawContacts.NAME_VERIFIED, 1527 RawContacts.SOURCE_ID, 1528 RawContacts.ACCOUNT_TYPE, 1529 }; 1530 1531 int _ID = 0; 1532 int DISPLAY_NAME = 1; 1533 int DISPLAY_NAME_SOURCE = 2; 1534 int NAME_VERIFIED = 3; 1535 int SOURCE_ID = 4; 1536 int ACCOUNT_TYPE = 5; 1537 } 1538 1539 public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) { 1540 long contactId = mDbHelper.getContactId(rawContactId); 1541 if (contactId == 0) { 1542 return; 1543 } 1544 1545 updateDisplayNameForContact(db, contactId); 1546 } 1547 1548 public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) { 1549 boolean lookupKeyUpdateNeeded = false; 1550 1551 mDisplayNameCandidate.clear(); 1552 1553 mSelectionArgs1[0] = String.valueOf(contactId); 1554 final Cursor c = db.query(Tables.RAW_CONTACTS, DisplayNameQuery.COLUMNS, 1555 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null); 1556 try { 1557 while (c.moveToNext()) { 1558 long rawContactId = c.getLong(DisplayNameQuery._ID); 1559 String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME); 1560 int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE); 1561 int nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED); 1562 String accountType = c.getString(DisplayNameQuery.ACCOUNT_TYPE); 1563 1564 processDisplayNameCanditate(rawContactId, displayName, displayNameSource, 1565 mContactsProvider.isWritableAccount(accountType), nameVerified != 0); 1566 1567 // If the raw contact has no source id, the lookup key is based on the display 1568 // name, so the lookup key needs to be updated. 1569 lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID); 1570 } 1571 } finally { 1572 c.close(); 1573 } 1574 1575 if (mDisplayNameCandidate.rawContactId != -1) { 1576 mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId); 1577 mDisplayNameUpdate.bindLong(2, contactId); 1578 mDisplayNameUpdate.execute(); 1579 } 1580 1581 if (lookupKeyUpdateNeeded) { 1582 updateLookupKeyForContact(db, contactId); 1583 } 1584 } 1585 1586 /** 1587 * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the 1588 * specified raw contact. 1589 */ 1590 public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) { 1591 1592 long contactId = mDbHelper.getContactId(rawContactId); 1593 if (contactId == 0) { 1594 return; 1595 } 1596 1597 mHasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE)); 1598 mHasPhoneNumberUpdate.bindLong(2, contactId); 1599 mHasPhoneNumberUpdate.bindLong(3, contactId); 1600 mHasPhoneNumberUpdate.execute(); 1601 } 1602 1603 private interface LookupKeyQuery { 1604 String[] COLUMNS = new String[] { 1605 RawContacts._ID, 1606 RawContactsColumns.DISPLAY_NAME, 1607 RawContacts.ACCOUNT_TYPE, 1608 RawContacts.ACCOUNT_NAME, 1609 RawContacts.SOURCE_ID, 1610 }; 1611 1612 int ID = 0; 1613 int DISPLAY_NAME = 1; 1614 int ACCOUNT_TYPE = 2; 1615 int ACCOUNT_NAME = 3; 1616 int SOURCE_ID = 4; 1617 } 1618 1619 public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) { 1620 long contactId = mDbHelper.getContactId(rawContactId); 1621 if (contactId == 0) { 1622 return; 1623 } 1624 1625 updateLookupKeyForContact(db, contactId); 1626 } 1627 1628 public void updateLookupKeyForContact(SQLiteDatabase db, long contactId) { 1629 mSb.setLength(0); 1630 mSelectionArgs1[0] = String.valueOf(contactId); 1631 final Cursor c = db.query(Tables.RAW_CONTACTS, LookupKeyQuery.COLUMNS, 1632 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID); 1633 try { 1634 while (c.moveToNext()) { 1635 ContactLookupKey.appendToLookupKey(mSb, 1636 c.getString(LookupKeyQuery.ACCOUNT_TYPE), 1637 c.getString(LookupKeyQuery.ACCOUNT_NAME), 1638 c.getLong(LookupKeyQuery.ID), 1639 c.getString(LookupKeyQuery.SOURCE_ID), 1640 c.getString(LookupKeyQuery.DISPLAY_NAME)); 1641 } 1642 } finally { 1643 c.close(); 1644 } 1645 1646 if (mSb.length() == 0) { 1647 mLookupKeyUpdate.bindNull(1); 1648 } else { 1649 mLookupKeyUpdate.bindString(1, Uri.encode(mSb.toString())); 1650 } 1651 mLookupKeyUpdate.bindLong(2, contactId); 1652 1653 mLookupKeyUpdate.execute(); 1654 } 1655 1656 /** 1657 * Execute {@link SQLiteStatement} that will update the 1658 * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}. 1659 */ 1660 protected void updateStarred(long rawContactId) { 1661 long contactId = mDbHelper.getContactId(rawContactId); 1662 if (contactId == 0) { 1663 return; 1664 } 1665 1666 mStarredUpdate.bindLong(1, contactId); 1667 mStarredUpdate.execute(); 1668 } 1669 1670 /** 1671 * Finds matching contacts and returns a cursor on those. 1672 */ 1673 public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb, String[] projection, 1674 long contactId, int maxSuggestions, String filter) { 1675 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 1676 1677 List<MatchScore> bestMatches = findMatchingContacts(db, contactId); 1678 return queryMatchingContacts(qb, db, contactId, projection, bestMatches, maxSuggestions, 1679 filter); 1680 } 1681 1682 private interface ContactIdQuery { 1683 String[] COLUMNS = new String[] { 1684 Contacts._ID 1685 }; 1686 1687 int _ID = 0; 1688 } 1689 1690 /** 1691 * Loads contacts with specified IDs and returns them in the order of IDs in the 1692 * supplied list. 1693 */ 1694 private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db, long contactId, 1695 String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) { 1696 1697 StringBuilder sb = new StringBuilder(); 1698 sb.append(Contacts._ID); 1699 sb.append(" IN ("); 1700 for (int i = 0; i < bestMatches.size(); i++) { 1701 MatchScore matchScore = bestMatches.get(i); 1702 if (i != 0) { 1703 sb.append(","); 1704 } 1705 sb.append(matchScore.getContactId()); 1706 } 1707 sb.append(")"); 1708 1709 if (!TextUtils.isEmpty(filter)) { 1710 sb.append(" AND " + Contacts._ID + " IN "); 1711 mContactsProvider.appendContactFilterAsNestedQuery(sb, filter); 1712 } 1713 1714 // Run a query and find ids of best matching contacts satisfying the filter (if any) 1715 HashSet<Long> foundIds = new HashSet<Long>(); 1716 Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(), 1717 null, null, null, null); 1718 try { 1719 while(cursor.moveToNext()) { 1720 foundIds.add(cursor.getLong(ContactIdQuery._ID)); 1721 } 1722 } finally { 1723 cursor.close(); 1724 } 1725 1726 // Exclude all contacts that did not match the filter 1727 Iterator<MatchScore> iter = bestMatches.iterator(); 1728 while (iter.hasNext()) { 1729 long id = iter.next().getContactId(); 1730 if (!foundIds.contains(id)) { 1731 iter.remove(); 1732 } 1733 } 1734 1735 // Limit the number of returned suggestions 1736 if (bestMatches.size() > maxSuggestions) { 1737 bestMatches = bestMatches.subList(0, maxSuggestions); 1738 } 1739 1740 // Build an in-clause with the remaining contact IDs 1741 sb.setLength(0); 1742 sb.append(Contacts._ID); 1743 sb.append(" IN ("); 1744 for (int i = 0; i < bestMatches.size(); i++) { 1745 MatchScore matchScore = bestMatches.get(i); 1746 if (i != 0) { 1747 sb.append(","); 1748 } 1749 sb.append(matchScore.getContactId()); 1750 } 1751 sb.append(")"); 1752 1753 // Run the final query with the required projection and contact IDs found by the first query 1754 cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID); 1755 1756 // Build a sorted list of discovered IDs 1757 ArrayList<Long> sortedContactIds = new ArrayList<Long>(bestMatches.size()); 1758 for (MatchScore matchScore : bestMatches) { 1759 sortedContactIds.add(matchScore.getContactId()); 1760 } 1761 1762 Collections.sort(sortedContactIds); 1763 1764 // Map cursor indexes according to the descending order of match scores 1765 int[] positionMap = new int[bestMatches.size()]; 1766 for (int i = 0; i < positionMap.length; i++) { 1767 long id = bestMatches.get(i).getContactId(); 1768 positionMap[i] = sortedContactIds.indexOf(id); 1769 } 1770 1771 return new ReorderingCursorWrapper(cursor, positionMap); 1772 } 1773 1774 private interface RawContactIdQuery { 1775 String TABLE = Tables.RAW_CONTACTS; 1776 1777 String[] COLUMNS = new String[] { 1778 RawContacts._ID 1779 }; 1780 1781 int _ID = 0; 1782 } 1783 1784 /** 1785 * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the 1786 * descending order of match score. 1787 */ 1788 private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId) { 1789 1790 MatchCandidateList candidates = new MatchCandidateList(); 1791 ContactMatcher matcher = new ContactMatcher(); 1792 1793 // Don't aggregate a contact with itself 1794 matcher.keepOut(contactId); 1795 1796 final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS, 1797 RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null); 1798 try { 1799 while (c.moveToNext()) { 1800 long rawContactId = c.getLong(RawContactIdQuery._ID); 1801 updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates, 1802 matcher); 1803 } 1804 } finally { 1805 c.close(); 1806 } 1807 1808 return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST); 1809 } 1810 1811 /** 1812 * Computes scores for contacts that have matching data rows. 1813 */ 1814 private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, 1815 long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) { 1816 1817 updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher); 1818 updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher); 1819 updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher); 1820 loadNameMatchCandidates(db, rawContactId, candidates, false); 1821 lookupApproximateNameMatches(db, candidates, matcher); 1822 } 1823 } 1824