ContactAggregator.java revision 49ed71913609193a00059df944f6259e9397b0bd
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 17package com.android.providers.contacts.aggregation; 18 19import com.android.providers.contacts.CommonNicknameCache; 20import com.android.providers.contacts.ContactLookupKey; 21import com.android.providers.contacts.ContactMatcher; 22import com.android.providers.contacts.ContactMatcher.MatchScore; 23import com.android.providers.contacts.ContactsDatabaseHelper; 24import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; 25import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; 26import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; 27import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; 28import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; 29import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; 30import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns; 31import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; 32import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; 33import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns; 34import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 35import com.android.providers.contacts.ContactsDatabaseHelper.Views; 36import com.android.providers.contacts.ContactsProvider2; 37import com.android.providers.contacts.NameLookupBuilder; 38import com.android.providers.contacts.NameNormalizer; 39import com.android.providers.contacts.NameSplitter; 40import com.android.providers.contacts.PhotoPriorityResolver; 41import com.android.providers.contacts.ReorderingCursorWrapper; 42import com.android.providers.contacts.TransactionContext; 43 44import android.database.Cursor; 45import android.database.DatabaseUtils; 46import android.database.sqlite.SQLiteDatabase; 47import android.database.sqlite.SQLiteQueryBuilder; 48import android.database.sqlite.SQLiteStatement; 49import android.net.Uri; 50import android.provider.ContactsContract.AggregationExceptions; 51import android.provider.ContactsContract.CommonDataKinds.Email; 52import android.provider.ContactsContract.CommonDataKinds.Identity; 53import android.provider.ContactsContract.CommonDataKinds.Phone; 54import android.provider.ContactsContract.CommonDataKinds.Photo; 55import android.provider.ContactsContract.Contacts; 56import android.provider.ContactsContract.Contacts.AggregationSuggestions; 57import android.provider.ContactsContract.Data; 58import android.provider.ContactsContract.DisplayNameSources; 59import android.provider.ContactsContract.FullNameStyle; 60import android.provider.ContactsContract.PhotoFiles; 61import android.provider.ContactsContract.RawContacts; 62import android.provider.ContactsContract.StatusUpdates; 63import android.text.TextUtils; 64import android.util.EventLog; 65import android.util.Log; 66 67import java.util.ArrayList; 68import java.util.Collections; 69import java.util.HashMap; 70import java.util.HashSet; 71import java.util.Iterator; 72import java.util.List; 73import java.util.Locale; 74 75/** 76 * ContactAggregator deals with aggregating contact information coming from different sources. 77 * Two John Doe contacts from two disjoint sources are presumed to be the same 78 * person unless the user declares otherwise. 79 */ 80public class ContactAggregator { 81 82 private static final String TAG = "ContactAggregator"; 83 84 private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 85 86 private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL = 87 NameLookupColumns.NAME_TYPE + " IN (" 88 + NameLookupType.NAME_EXACT + "," 89 + NameLookupType.NAME_VARIANT + "," 90 + NameLookupType.NAME_COLLATION_KEY + ")"; 91 92 93 /** 94 * SQL statement that sets the {@link ContactsColumns#LAST_STATUS_UPDATE_ID} column 95 * on the contact to point to the latest social status update. 96 */ 97 private static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL = 98 "UPDATE " + Tables.CONTACTS + 99 " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" + 100 "(SELECT " + DataColumns.CONCRETE_ID + 101 " FROM " + Tables.STATUS_UPDATES + 102 " JOIN " + Tables.DATA + 103 " ON (" + StatusUpdatesColumns.DATA_ID + "=" 104 + DataColumns.CONCRETE_ID + ")" + 105 " JOIN " + Tables.RAW_CONTACTS + 106 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" 107 + RawContactsColumns.CONCRETE_ID + ")" + 108 " WHERE " + RawContacts.CONTACT_ID + "=?" + 109 " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC," 110 + StatusUpdates.STATUS + 111 " LIMIT 1)" + 112 " WHERE " + ContactsColumns.CONCRETE_ID + "=?"; 113 114 // From system/core/logcat/event-log-tags 115 // aggregator [time, count] will be logged for each aggregator cycle. 116 // For the query (as opposed to the merge), count will be negative 117 public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747; 118 119 // If we encounter more than this many contacts with matching names, aggregate only this many 120 private static final int PRIMARY_HIT_LIMIT = 15; 121 private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT); 122 123 // If we encounter more than this many contacts with matching phone number or email, 124 // don't attempt to aggregate - this is likely an error or a shared corporate data element. 125 private static final int SECONDARY_HIT_LIMIT = 20; 126 private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT); 127 128 // If we encounter more than this many contacts with matching name during aggregation 129 // suggestion lookup, ignore the remaining results. 130 private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100; 131 132 private final ContactsProvider2 mContactsProvider; 133 private final ContactsDatabaseHelper mDbHelper; 134 private PhotoPriorityResolver mPhotoPriorityResolver; 135 private final NameSplitter mNameSplitter; 136 private final CommonNicknameCache mCommonNicknameCache; 137 138 private boolean mEnabled = true; 139 140 /** Precompiled sql statement for setting an aggregated presence */ 141 private SQLiteStatement mAggregatedPresenceReplace; 142 private SQLiteStatement mPresenceContactIdUpdate; 143 private SQLiteStatement mRawContactCountQuery; 144 private SQLiteStatement mContactDelete; 145 private SQLiteStatement mAggregatedPresenceDelete; 146 private SQLiteStatement mMarkForAggregation; 147 private SQLiteStatement mPhotoIdUpdate; 148 private SQLiteStatement mDisplayNameUpdate; 149 private SQLiteStatement mLookupKeyUpdate; 150 private SQLiteStatement mStarredUpdate; 151 private SQLiteStatement mContactIdAndMarkAggregatedUpdate; 152 private SQLiteStatement mContactIdUpdate; 153 private SQLiteStatement mMarkAggregatedUpdate; 154 private SQLiteStatement mContactUpdate; 155 private SQLiteStatement mContactInsert; 156 157 private HashMap<Long, Integer> mRawContactsMarkedForAggregation = new HashMap<Long, Integer>(); 158 159 private String[] mSelectionArgs1 = new String[1]; 160 private String[] mSelectionArgs2 = new String[2]; 161 private String[] mSelectionArgs3 = new String[3]; 162 private String[] mSelectionArgs4 = new String[4]; 163 private long mMimeTypeIdIdentity; 164 private long mMimeTypeIdEmail; 165 private long mMimeTypeIdPhoto; 166 private long mMimeTypeIdPhone; 167 private String mRawContactsQueryByRawContactId; 168 private String mRawContactsQueryByContactId; 169 private StringBuilder mSb = new StringBuilder(); 170 private MatchCandidateList mCandidates = new MatchCandidateList(); 171 private ContactMatcher mMatcher = new ContactMatcher(); 172 private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate(); 173 174 /** 175 * Parameter for the suggestion lookup query. 176 */ 177 public static final class AggregationSuggestionParameter { 178 public final String kind; 179 public final String value; 180 181 public AggregationSuggestionParameter(String kind, String value) { 182 this.kind = kind; 183 this.value = value; 184 } 185 } 186 187 /** 188 * Captures a potential match for a given name. The matching algorithm 189 * constructs a bunch of NameMatchCandidate objects for various potential matches 190 * and then executes the search in bulk. 191 */ 192 private static class NameMatchCandidate { 193 String mName; 194 int mLookupType; 195 196 public NameMatchCandidate(String name, int nameLookupType) { 197 mName = name; 198 mLookupType = nameLookupType; 199 } 200 } 201 202 /** 203 * A list of {@link NameMatchCandidate} that keeps its elements even when the list is 204 * truncated. This is done for optimization purposes to avoid excessive object allocation. 205 */ 206 private static class MatchCandidateList { 207 private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>(); 208 private int mCount; 209 210 /** 211 * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists. 212 */ 213 public void add(String name, int nameLookupType) { 214 if (mCount >= mList.size()) { 215 mList.add(new NameMatchCandidate(name, nameLookupType)); 216 } else { 217 NameMatchCandidate candidate = mList.get(mCount); 218 candidate.mName = name; 219 candidate.mLookupType = nameLookupType; 220 } 221 mCount++; 222 } 223 224 public void clear() { 225 mCount = 0; 226 } 227 228 public boolean isEmpty() { 229 return mCount == 0; 230 } 231 } 232 233 /** 234 * A convenience class used in the algorithm that figures out which of available 235 * display names to use for an aggregate contact. 236 */ 237 private static class DisplayNameCandidate { 238 long rawContactId; 239 String displayName; 240 int displayNameSource; 241 boolean verified; 242 boolean writableAccount; 243 244 public DisplayNameCandidate() { 245 clear(); 246 } 247 248 public void clear() { 249 rawContactId = -1; 250 displayName = null; 251 displayNameSource = DisplayNameSources.UNDEFINED; 252 verified = false; 253 writableAccount = false; 254 } 255 } 256 257 /** 258 * Constructor. 259 */ 260 public ContactAggregator(ContactsProvider2 contactsProvider, 261 ContactsDatabaseHelper contactsDatabaseHelper, 262 PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter, 263 CommonNicknameCache commonNicknameCache) { 264 mContactsProvider = contactsProvider; 265 mDbHelper = contactsDatabaseHelper; 266 mPhotoPriorityResolver = photoPriorityResolver; 267 mNameSplitter = nameSplitter; 268 mCommonNicknameCache = commonNicknameCache; 269 270 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 271 272 // Since we have no way of determining which custom status was set last, 273 // we'll just pick one randomly. We are using MAX as an approximation of randomness 274 final String replaceAggregatePresenceSql = 275 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "(" 276 + AggregatedPresenceColumns.CONTACT_ID + ", " 277 + StatusUpdates.PRESENCE + ", " 278 + StatusUpdates.CHAT_CAPABILITY + ")" 279 + " SELECT " + PresenceColumns.CONTACT_ID + "," 280 + StatusUpdates.PRESENCE + "," 281 + StatusUpdates.CHAT_CAPABILITY 282 + " FROM " + Tables.PRESENCE 283 + " WHERE " 284 + " (" + StatusUpdates.PRESENCE 285 + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" 286 + " = (SELECT " 287 + "MAX (" + StatusUpdates.PRESENCE 288 + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" 289 + " FROM " + Tables.PRESENCE 290 + " WHERE " + PresenceColumns.CONTACT_ID 291 + "=?)" 292 + " AND " + PresenceColumns.CONTACT_ID 293 + "=?;"; 294 mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql); 295 296 mRawContactCountQuery = db.compileStatement( 297 "SELECT COUNT(" + RawContacts._ID + ")" + 298 " FROM " + Tables.RAW_CONTACTS + 299 " WHERE " + RawContacts.CONTACT_ID + "=?" 300 + " AND " + RawContacts._ID + "<>?"); 301 302 mContactDelete = db.compileStatement( 303 "DELETE FROM " + Tables.CONTACTS + 304 " WHERE " + Contacts._ID + "=?"); 305 306 mAggregatedPresenceDelete = db.compileStatement( 307 "DELETE FROM " + Tables.AGGREGATED_PRESENCE + 308 " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?"); 309 310 mMarkForAggregation = db.compileStatement( 311 "UPDATE " + Tables.RAW_CONTACTS + 312 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" + 313 " WHERE " + RawContacts._ID + "=?" 314 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"); 315 316 mPhotoIdUpdate = db.compileStatement( 317 "UPDATE " + Tables.CONTACTS + 318 " SET " + Contacts.PHOTO_ID + "=?," + Contacts.PHOTO_FILE_ID + "=? " + 319 " WHERE " + Contacts._ID + "=?"); 320 321 mDisplayNameUpdate = db.compileStatement( 322 "UPDATE " + Tables.CONTACTS + 323 " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " + 324 " WHERE " + Contacts._ID + "=?"); 325 326 mLookupKeyUpdate = db.compileStatement( 327 "UPDATE " + Tables.CONTACTS + 328 " SET " + Contacts.LOOKUP_KEY + "=? " + 329 " WHERE " + Contacts._ID + "=?"); 330 331 mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET " 332 + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED 333 + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE " 334 + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND " 335 + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?"); 336 337 mContactIdAndMarkAggregatedUpdate = db.compileStatement( 338 "UPDATE " + Tables.RAW_CONTACTS + 339 " SET " + RawContacts.CONTACT_ID + "=?, " 340 + RawContactsColumns.AGGREGATION_NEEDED + "=0" + 341 " WHERE " + RawContacts._ID + "=?"); 342 343 mContactIdUpdate = db.compileStatement( 344 "UPDATE " + Tables.RAW_CONTACTS + 345 " SET " + RawContacts.CONTACT_ID + "=?" + 346 " WHERE " + RawContacts._ID + "=?"); 347 348 mMarkAggregatedUpdate = db.compileStatement( 349 "UPDATE " + Tables.RAW_CONTACTS + 350 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" + 351 " WHERE " + RawContacts._ID + "=?"); 352 353 mPresenceContactIdUpdate = db.compileStatement( 354 "UPDATE " + Tables.PRESENCE + 355 " SET " + PresenceColumns.CONTACT_ID + "=?" + 356 " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?"); 357 358 mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL); 359 mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL); 360 361 mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); 362 mMimeTypeIdIdentity = mDbHelper.getMimeTypeId(Identity.CONTENT_ITEM_TYPE); 363 mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE); 364 mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); 365 366 // Query used to retrieve data from raw contacts to populate the corresponding aggregate 367 mRawContactsQueryByRawContactId = String.format(Locale.US, 368 RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID, 369 mMimeTypeIdPhoto, mMimeTypeIdPhone); 370 371 mRawContactsQueryByContactId = String.format(Locale.US, 372 RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID, 373 mMimeTypeIdPhoto, mMimeTypeIdPhone); 374 } 375 376 public void setEnabled(boolean enabled) { 377 mEnabled = enabled; 378 } 379 380 public boolean isEnabled() { 381 return mEnabled; 382 } 383 384 private interface AggregationQuery { 385 String SQL = 386 "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID + 387 ", " + RawContactsColumns.ACCOUNT_ID + 388 " FROM " + Tables.RAW_CONTACTS + 389 " WHERE " + RawContacts._ID + " IN("; 390 391 int _ID = 0; 392 int CONTACT_ID = 1; 393 int ACCOUNT_ID = 2; 394 } 395 396 /** 397 * Aggregate all raw contacts that were marked for aggregation in the current transaction. 398 * Call just before committing the transaction. 399 */ 400 public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) { 401 int count = mRawContactsMarkedForAggregation.size(); 402 if (count == 0) { 403 return; 404 } 405 406 long start = System.currentTimeMillis(); 407 if (VERBOSE_LOGGING) { 408 Log.v(TAG, "Contact aggregation: " + count); 409 } 410 411 EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -count); 412 413 String selectionArgs[] = new String[count]; 414 415 int index = 0; 416 mSb.setLength(0); 417 mSb.append(AggregationQuery.SQL); 418 for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) { 419 if (index > 0) { 420 mSb.append(','); 421 } 422 mSb.append('?'); 423 selectionArgs[index++] = String.valueOf(rawContactId); 424 } 425 426 mSb.append(')'); 427 428 long rawContactIds[] = new long[count]; 429 long contactIds[] = new long[count]; 430 long accountIds[] = new long[count]; 431 Cursor c = db.rawQuery(mSb.toString(), selectionArgs); 432 try { 433 count = c.getCount(); 434 index = 0; 435 while (c.moveToNext()) { 436 rawContactIds[index] = c.getLong(AggregationQuery._ID); 437 contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID); 438 accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID); 439 index++; 440 } 441 } finally { 442 c.close(); 443 } 444 445 for (int i = 0; i < count; i++) { 446 aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i], 447 mCandidates, mMatcher); 448 } 449 450 long elapsedTime = System.currentTimeMillis() - start; 451 EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, count); 452 453 if (VERBOSE_LOGGING) { 454 String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact"; 455 Log.i(TAG, "Contact aggregation complete: " + count + performance); 456 } 457 } 458 459 @SuppressWarnings("deprecation") 460 public void triggerAggregation(TransactionContext txContext, long rawContactId) { 461 if (!mEnabled) { 462 return; 463 } 464 465 int aggregationMode = mDbHelper.getAggregationMode(rawContactId); 466 switch (aggregationMode) { 467 case RawContacts.AGGREGATION_MODE_DISABLED: 468 break; 469 470 case RawContacts.AGGREGATION_MODE_DEFAULT: { 471 markForAggregation(rawContactId, aggregationMode, false); 472 break; 473 } 474 475 case RawContacts.AGGREGATION_MODE_SUSPENDED: { 476 long contactId = mDbHelper.getContactId(rawContactId); 477 478 if (contactId != 0) { 479 updateAggregateData(txContext, contactId); 480 } 481 break; 482 } 483 484 case RawContacts.AGGREGATION_MODE_IMMEDIATE: { 485 aggregateContact(txContext, mDbHelper.getWritableDatabase(), rawContactId); 486 break; 487 } 488 } 489 } 490 491 public void clearPendingAggregations() { 492 mRawContactsMarkedForAggregation.clear(); 493 } 494 495 public void markNewForAggregation(long rawContactId, int aggregationMode) { 496 mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode); 497 } 498 499 public void markForAggregation(long rawContactId, int aggregationMode, boolean force) { 500 final int effectiveAggregationMode; 501 if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) { 502 // As per ContactsContract documentation, default aggregation mode 503 // does not override a previously set mode 504 if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { 505 effectiveAggregationMode = mRawContactsMarkedForAggregation.get(rawContactId); 506 } else { 507 effectiveAggregationMode = aggregationMode; 508 } 509 } else { 510 mMarkForAggregation.bindLong(1, rawContactId); 511 mMarkForAggregation.execute(); 512 effectiveAggregationMode = aggregationMode; 513 } 514 515 mRawContactsMarkedForAggregation.put(rawContactId, effectiveAggregationMode); 516 } 517 518 private static class RawContactIdAndAggregationModeQuery { 519 public static final String TABLE = Tables.RAW_CONTACTS; 520 521 public static final String[] COLUMNS = { RawContacts._ID, RawContacts.AGGREGATION_MODE }; 522 523 public static final String SELECTION = RawContacts.CONTACT_ID + "=?"; 524 525 public static final int _ID = 0; 526 public static final int AGGREGATION_MODE = 1; 527 } 528 529 /** 530 * Marks all constituent raw contacts of an aggregated contact for re-aggregation. 531 */ 532 private void markContactForAggregation(SQLiteDatabase db, long contactId) { 533 mSelectionArgs1[0] = String.valueOf(contactId); 534 Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE, 535 RawContactIdAndAggregationModeQuery.COLUMNS, 536 RawContactIdAndAggregationModeQuery.SELECTION, mSelectionArgs1, null, null, null); 537 try { 538 if (cursor.moveToFirst()) { 539 long rawContactId = cursor.getLong(RawContactIdAndAggregationModeQuery._ID); 540 int aggregationMode = cursor.getInt( 541 RawContactIdAndAggregationModeQuery.AGGREGATION_MODE); 542 if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { 543 markForAggregation(rawContactId, aggregationMode, true); 544 } 545 } 546 } finally { 547 cursor.close(); 548 } 549 } 550 551 /** 552 * Creates a new contact based on the given raw contact. Does not perform aggregation. Returns 553 * the ID of the contact that was created. 554 */ 555 public long onRawContactInsert( 556 TransactionContext txContext, SQLiteDatabase db, long rawContactId) { 557 long contactId = insertContact(db, rawContactId); 558 setContactId(rawContactId, contactId); 559 mDbHelper.updateContactVisible(txContext, contactId); 560 return contactId; 561 } 562 563 protected long insertContact(SQLiteDatabase db, long rawContactId) { 564 mSelectionArgs1[0] = String.valueOf(rawContactId); 565 computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert); 566 return mContactInsert.executeInsert(); 567 } 568 569 private static final class RawContactIdAndAccountQuery { 570 public static final String TABLE = Tables.RAW_CONTACTS; 571 572 public static final String[] COLUMNS = { 573 RawContacts.CONTACT_ID, 574 RawContactsColumns.ACCOUNT_ID 575 }; 576 577 public static final String SELECTION = RawContacts._ID + "=?"; 578 579 public static final int CONTACT_ID = 0; 580 public static final int ACCOUNT_ID = 1; 581 } 582 583 public void aggregateContact( 584 TransactionContext txContext, SQLiteDatabase db, long rawContactId) { 585 if (!mEnabled) { 586 return; 587 } 588 589 MatchCandidateList candidates = new MatchCandidateList(); 590 ContactMatcher matcher = new ContactMatcher(); 591 592 long contactId = 0; 593 long accountId = 0; 594 mSelectionArgs1[0] = String.valueOf(rawContactId); 595 Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE, 596 RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION, 597 mSelectionArgs1, null, null, null); 598 try { 599 if (cursor.moveToFirst()) { 600 contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID); 601 accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID); 602 } 603 } finally { 604 cursor.close(); 605 } 606 607 aggregateContact(txContext, db, rawContactId, accountId, contactId, 608 candidates, matcher); 609 } 610 611 public void updateAggregateData(TransactionContext txContext, long contactId) { 612 if (!mEnabled) { 613 return; 614 } 615 616 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 617 computeAggregateData(db, contactId, mContactUpdate); 618 mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId); 619 mContactUpdate.execute(); 620 621 mDbHelper.updateContactVisible(txContext, contactId); 622 updateAggregatedStatusUpdate(contactId); 623 } 624 625 private void updateAggregatedStatusUpdate(long contactId) { 626 mAggregatedPresenceReplace.bindLong(1, contactId); 627 mAggregatedPresenceReplace.bindLong(2, contactId); 628 mAggregatedPresenceReplace.execute(); 629 updateLastStatusUpdateId(contactId); 630 } 631 632 /** 633 * Adjusts the reference to the latest status update for the specified contact. 634 */ 635 public void updateLastStatusUpdateId(long contactId) { 636 String contactIdString = String.valueOf(contactId); 637 mDbHelper.getWritableDatabase().execSQL(UPDATE_LAST_STATUS_UPDATE_ID_SQL, 638 new String[]{contactIdString, contactIdString}); 639 } 640 641 /** 642 * Given a specific raw contact, finds all matching aggregate contacts and chooses the one 643 * with the highest match score. If no such contact is found, creates a new contact. 644 */ 645 private synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db, 646 long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates, 647 ContactMatcher matcher) { 648 649 int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; 650 651 Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId); 652 if (aggModeObject != null) { 653 aggregationMode = aggModeObject; 654 } 655 656 long contactId = -1; 657 long contactIdToSplit = -1; 658 659 if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { 660 candidates.clear(); 661 matcher.clear(); 662 663 contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher); 664 if (contactId == -1) { 665 666 // If this is a newly inserted contact or a visible contact, look for 667 // data matches. 668 if (currentContactId == 0 669 || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) { 670 contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher); 671 } 672 673 // If we found an aggregate to join, but it already contains raw contacts from 674 // the same account, not only will we not join it, but also we will split 675 // that other aggregate 676 if (contactId != -1 && contactId != currentContactId && 677 containsRawContactsFromAccount(db, contactId, accountId)) { 678 contactIdToSplit = contactId; 679 contactId = -1; 680 } 681 } 682 } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) { 683 return; 684 } 685 686 long currentContactContentsCount = 0; 687 688 if (currentContactId != 0) { 689 mRawContactCountQuery.bindLong(1, currentContactId); 690 mRawContactCountQuery.bindLong(2, rawContactId); 691 currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong(); 692 } 693 694 // If there are no other raw contacts in the current aggregate, we might as well reuse it. 695 // Also, if the aggregation mode is SUSPENDED, we must reuse the same aggregate. 696 if (contactId == -1 697 && currentContactId != 0 698 && (currentContactContentsCount == 0 699 || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) { 700 contactId = currentContactId; 701 } 702 703 if (contactId == currentContactId) { 704 // Aggregation unchanged 705 markAggregated(rawContactId); 706 } else if (contactId == -1) { 707 // Splitting an aggregate 708 createNewContactForRawContact(txContext, db, rawContactId); 709 if (currentContactContentsCount > 0) { 710 updateAggregateData(txContext, currentContactId); 711 } 712 } else { 713 // Joining with an existing aggregate 714 if (currentContactContentsCount == 0) { 715 // Delete a previous aggregate if it only contained this raw contact 716 mContactDelete.bindLong(1, currentContactId); 717 mContactDelete.execute(); 718 719 mAggregatedPresenceDelete.bindLong(1, currentContactId); 720 mAggregatedPresenceDelete.execute(); 721 } 722 723 setContactIdAndMarkAggregated(rawContactId, contactId); 724 computeAggregateData(db, contactId, mContactUpdate); 725 mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId); 726 mContactUpdate.execute(); 727 mDbHelper.updateContactVisible(txContext, contactId); 728 updateAggregatedStatusUpdate(contactId); 729 } 730 731 if (contactIdToSplit != -1) { 732 splitAutomaticallyAggregatedRawContacts(txContext, db, contactIdToSplit); 733 } 734 } 735 736 /** 737 * Returns true if the aggregate contains has any raw contacts from the specified account. 738 */ 739 private boolean containsRawContactsFromAccount( 740 SQLiteDatabase db, long contactId, long accountId) { 741 final String query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS + 742 " WHERE " + RawContacts.CONTACT_ID + "=?" + 743 " AND " + RawContactsColumns.ACCOUNT_ID + "=?"; 744 Cursor cursor = db.rawQuery(query, new String[] { 745 Long.toString(contactId), Long.toString(accountId) 746 }); 747 try { 748 cursor.moveToFirst(); 749 return cursor.getInt(0) != 0; 750 } finally { 751 cursor.close(); 752 } 753 } 754 755 /** 756 * Breaks up an existing aggregate when a new raw contact is inserted that has 757 * come from the same account as one of the raw contacts in this aggregate. 758 */ 759 private void splitAutomaticallyAggregatedRawContacts( 760 TransactionContext txContext, SQLiteDatabase db, long contactId) { 761 mSelectionArgs1[0] = String.valueOf(contactId); 762 int count = (int) DatabaseUtils.longForQuery(db, 763 "SELECT COUNT(" + RawContacts._ID + ")" + 764 " FROM " + Tables.RAW_CONTACTS + 765 " WHERE " + RawContacts.CONTACT_ID + "=?", mSelectionArgs1); 766 if (count < 2) { 767 // A single-raw-contact aggregate does not need to be split up 768 return; 769 } 770 771 // Find all constituent raw contacts that are not held together by 772 // an explicit aggregation exception 773 String query = 774 "SELECT " + RawContacts._ID + 775 " FROM " + Tables.RAW_CONTACTS + 776 " WHERE " + RawContacts.CONTACT_ID + "=?" + 777 " AND " + RawContacts._ID + " NOT IN " + 778 "(SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + 779 " FROM " + Tables.AGGREGATION_EXCEPTIONS + 780 " WHERE " + AggregationExceptions.TYPE + "=" 781 + AggregationExceptions.TYPE_KEEP_TOGETHER + 782 " UNION SELECT " + AggregationExceptions.RAW_CONTACT_ID2 + 783 " FROM " + Tables.AGGREGATION_EXCEPTIONS + 784 " WHERE " + AggregationExceptions.TYPE + "=" 785 + AggregationExceptions.TYPE_KEEP_TOGETHER + 786 ")"; 787 788 Cursor cursor = db.rawQuery(query, mSelectionArgs1); 789 try { 790 // Process up to count-1 raw contact, leaving the last one alone. 791 for (int i = 0; i < count - 1; i++) { 792 if (!cursor.moveToNext()) { 793 break; 794 } 795 long rawContactId = cursor.getLong(0); 796 createNewContactForRawContact(txContext, db, rawContactId); 797 } 798 } finally { 799 cursor.close(); 800 } 801 if (contactId > 0) { 802 updateAggregateData(txContext, contactId); 803 } 804 } 805 806 /** 807 * Creates a stand-alone Contact for the given raw contact ID. 808 */ 809 private void createNewContactForRawContact( 810 TransactionContext txContext, SQLiteDatabase db, long rawContactId) { 811 mSelectionArgs1[0] = String.valueOf(rawContactId); 812 computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, 813 mContactInsert); 814 long contactId = mContactInsert.executeInsert(); 815 setContactIdAndMarkAggregated(rawContactId, contactId); 816 mDbHelper.updateContactVisible(txContext, contactId); 817 setPresenceContactId(rawContactId, contactId); 818 updateAggregatedStatusUpdate(contactId); 819 } 820 821 private static class RawContactIdQuery { 822 public static final String TABLE = Tables.RAW_CONTACTS; 823 public static final String[] COLUMNS = { RawContacts._ID }; 824 public static final String SELECTION = RawContacts.CONTACT_ID + "=?"; 825 public static final int RAW_CONTACT_ID = 0; 826 } 827 828 /** 829 * Ensures that automatic aggregation rules are followed after a contact 830 * becomes visible or invisible. Specifically, consider this case: there are 831 * three contacts named Foo. Two of them come from account A1 and one comes 832 * from account A2. The aggregation rules say that in this case none of the 833 * three Foo's should be aggregated: two of them are in the same account, so 834 * they don't get aggregated; the third has two affinities, so it does not 835 * join either of them. 836 * <p> 837 * Consider what happens if one of the "Foo"s from account A1 becomes 838 * invisible. Nothing stands in the way of aggregating the other two 839 * anymore, so they should get joined. 840 * <p> 841 * What if the invisible "Foo" becomes visible after that? We should split the 842 * aggregate between the other two. 843 */ 844 public void updateAggregationAfterVisibilityChange(long contactId) { 845 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 846 boolean visible = mDbHelper.isContactInDefaultDirectory(db, contactId); 847 if (visible) { 848 markContactForAggregation(db, contactId); 849 } else { 850 // Find all contacts that _could be_ aggregated with this one and 851 // rerun aggregation for all of them 852 mSelectionArgs1[0] = String.valueOf(contactId); 853 Cursor cursor = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS, 854 RawContactIdQuery.SELECTION, mSelectionArgs1, null, null, null); 855 try { 856 while (cursor.moveToNext()) { 857 long rawContactId = cursor.getLong(RawContactIdQuery.RAW_CONTACT_ID); 858 mMatcher.clear(); 859 860 updateMatchScoresBasedOnIdentityMatch(db, rawContactId, mMatcher); 861 updateMatchScoresBasedOnNameMatches(db, rawContactId, mMatcher); 862 List<MatchScore> bestMatches = 863 mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_PRIMARY); 864 for (MatchScore matchScore : bestMatches) { 865 markContactForAggregation(db, matchScore.getContactId()); 866 } 867 868 mMatcher.clear(); 869 updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher); 870 updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher); 871 bestMatches = 872 mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SECONDARY); 873 for (MatchScore matchScore : bestMatches) { 874 markContactForAggregation(db, matchScore.getContactId()); 875 } 876 } 877 } finally { 878 cursor.close(); 879 } 880 } 881 } 882 883 /** 884 * Updates the contact ID for the specified contact. 885 */ 886 protected void setContactId(long rawContactId, long contactId) { 887 mContactIdUpdate.bindLong(1, contactId); 888 mContactIdUpdate.bindLong(2, rawContactId); 889 mContactIdUpdate.execute(); 890 } 891 892 /** 893 * Marks the specified raw contact ID as aggregated 894 */ 895 private void markAggregated(long rawContactId) { 896 mMarkAggregatedUpdate.bindLong(1, rawContactId); 897 mMarkAggregatedUpdate.execute(); 898 } 899 900 /** 901 * Updates the contact ID for the specified contact and marks the raw contact as aggregated. 902 */ 903 private void setContactIdAndMarkAggregated(long rawContactId, long contactId) { 904 mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId); 905 mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId); 906 mContactIdAndMarkAggregatedUpdate.execute(); 907 } 908 909 private void setPresenceContactId(long rawContactId, long contactId) { 910 mPresenceContactIdUpdate.bindLong(1, contactId); 911 mPresenceContactIdUpdate.bindLong(2, rawContactId); 912 mPresenceContactIdUpdate.execute(); 913 } 914 915 interface AggregateExceptionPrefetchQuery { 916 String TABLE = Tables.AGGREGATION_EXCEPTIONS; 917 918 String[] COLUMNS = { 919 AggregationExceptions.RAW_CONTACT_ID1, 920 AggregationExceptions.RAW_CONTACT_ID2, 921 }; 922 923 int RAW_CONTACT_ID1 = 0; 924 int RAW_CONTACT_ID2 = 1; 925 } 926 927 // A set of raw contact IDs for which there are aggregation exceptions 928 private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>(); 929 private boolean mAggregationExceptionIdsValid; 930 931 public void invalidateAggregationExceptionCache() { 932 mAggregationExceptionIdsValid = false; 933 } 934 935 /** 936 * Finds all raw contact IDs for which there are aggregation exceptions. The list of 937 * ids is used as an optimization in aggregation: there is no point to run a query against 938 * the agg_exceptions table if it is known that there are no records there for a given 939 * raw contact ID. 940 */ 941 private void prefetchAggregationExceptionIds(SQLiteDatabase db) { 942 mAggregationExceptionIds.clear(); 943 final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE, 944 AggregateExceptionPrefetchQuery.COLUMNS, 945 null, null, null, null, null); 946 947 try { 948 while (c.moveToNext()) { 949 long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1); 950 long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2); 951 mAggregationExceptionIds.add(rawContactId1); 952 mAggregationExceptionIds.add(rawContactId2); 953 } 954 } finally { 955 c.close(); 956 } 957 958 mAggregationExceptionIdsValid = true; 959 } 960 961 interface AggregateExceptionQuery { 962 String TABLE = Tables.AGGREGATION_EXCEPTIONS 963 + " JOIN raw_contacts raw_contacts1 " 964 + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) " 965 + " JOIN raw_contacts raw_contacts2 " 966 + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) "; 967 968 String[] COLUMNS = { 969 AggregationExceptions.TYPE, 970 AggregationExceptions.RAW_CONTACT_ID1, 971 "raw_contacts1." + RawContacts.CONTACT_ID, 972 "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED, 973 "raw_contacts2." + RawContacts.CONTACT_ID, 974 "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED, 975 }; 976 977 int TYPE = 0; 978 int RAW_CONTACT_ID1 = 1; 979 int CONTACT_ID1 = 2; 980 int AGGREGATION_NEEDED_1 = 3; 981 int CONTACT_ID2 = 4; 982 int AGGREGATION_NEEDED_2 = 5; 983 } 984 985 /** 986 * Computes match scores based on exceptions entered by the user: always match and never match. 987 * Returns the aggregate contact with the always match exception if any. 988 */ 989 private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId, 990 ContactMatcher matcher) { 991 if (!mAggregationExceptionIdsValid) { 992 prefetchAggregationExceptionIds(db); 993 } 994 995 // If there are no aggregation exceptions involving this raw contact, there is no need to 996 // run a query and we can just return -1, which stands for "nothing found" 997 if (!mAggregationExceptionIds.contains(rawContactId)) { 998 return -1; 999 } 1000 1001 final Cursor c = db.query(AggregateExceptionQuery.TABLE, 1002 AggregateExceptionQuery.COLUMNS, 1003 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId 1004 + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId, 1005 null, null, null, null); 1006 1007 try { 1008 while (c.moveToNext()) { 1009 int type = c.getInt(AggregateExceptionQuery.TYPE); 1010 long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1); 1011 long contactId = -1; 1012 if (rawContactId == rawContactId1) { 1013 if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0 1014 && !c.isNull(AggregateExceptionQuery.CONTACT_ID2)) { 1015 contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2); 1016 } 1017 } else { 1018 if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0 1019 && !c.isNull(AggregateExceptionQuery.CONTACT_ID1)) { 1020 contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1); 1021 } 1022 } 1023 if (contactId != -1) { 1024 if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) { 1025 matcher.keepIn(contactId); 1026 } else { 1027 matcher.keepOut(contactId); 1028 } 1029 } 1030 } 1031 } finally { 1032 c.close(); 1033 } 1034 1035 return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true); 1036 } 1037 1038 /** 1039 * Picks the best matching contact based on matches between data elements. It considers 1040 * name match to be primary and phone, email etc matches to be secondary. A good primary 1041 * match triggers aggregation, while a good secondary match only triggers aggregation in 1042 * the absence of a strong primary mismatch. 1043 * <p> 1044 * Consider these examples: 1045 * <p> 1046 * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should 1047 * be aggregated (same number, similar names). 1048 * <p> 1049 * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should 1050 * not be aggregated (same number, different names). 1051 */ 1052 private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId, 1053 MatchCandidateList candidates, ContactMatcher matcher) { 1054 1055 // Find good matches based on name alone 1056 long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, matcher); 1057 if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) { 1058 // We found multiple matches on the name - do not aggregate because of the ambiguity 1059 return -1; 1060 } else if (bestMatch == -1) { 1061 // We haven't found a good match on name, see if we have any matches on phone, email etc 1062 bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher); 1063 if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) { 1064 return -1; 1065 } 1066 } 1067 1068 return bestMatch; 1069 } 1070 1071 1072 /** 1073 * Picks the best matching contact based on secondary data matches. The method loads 1074 * structured names for all candidate contacts and recomputes match scores using approximate 1075 * matching. 1076 */ 1077 private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db, 1078 long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) { 1079 List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates( 1080 ContactMatcher.SCORE_THRESHOLD_PRIMARY); 1081 if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) { 1082 return -1; 1083 } 1084 1085 loadNameMatchCandidates(db, rawContactId, candidates, true); 1086 1087 mSb.setLength(0); 1088 mSb.append(RawContacts.CONTACT_ID).append(" IN ("); 1089 for (int i = 0; i < secondaryContactIds.size(); i++) { 1090 if (i != 0) { 1091 mSb.append(','); 1092 } 1093 mSb.append(secondaryContactIds.get(i)); 1094 } 1095 1096 // We only want to compare structured names to structured names 1097 // at this stage, we need to ignore all other sources of name lookup data. 1098 mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL); 1099 1100 matchAllCandidates(db, mSb.toString(), candidates, matcher, 1101 ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null); 1102 1103 return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false); 1104 } 1105 1106 private interface NameLookupQuery { 1107 String TABLE = Tables.NAME_LOOKUP; 1108 1109 String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?"; 1110 String SELECTION_STRUCTURED_NAME_BASED = 1111 SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL; 1112 1113 String[] COLUMNS = new String[] { 1114 NameLookupColumns.NORMALIZED_NAME, 1115 NameLookupColumns.NAME_TYPE 1116 }; 1117 1118 int NORMALIZED_NAME = 0; 1119 int NAME_TYPE = 1; 1120 } 1121 1122 private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId, 1123 MatchCandidateList candidates, boolean structuredNameBased) { 1124 candidates.clear(); 1125 mSelectionArgs1[0] = String.valueOf(rawContactId); 1126 Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS, 1127 structuredNameBased 1128 ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED 1129 : NameLookupQuery.SELECTION, 1130 mSelectionArgs1, null, null, null); 1131 try { 1132 while (c.moveToNext()) { 1133 String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME); 1134 int type = c.getInt(NameLookupQuery.NAME_TYPE); 1135 candidates.add(normalizedName, type); 1136 } 1137 } finally { 1138 c.close(); 1139 } 1140 } 1141 1142 /** 1143 * Computes scores for contacts that have matching data rows. 1144 */ 1145 private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId, 1146 ContactMatcher matcher) { 1147 1148 updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher); 1149 updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher); 1150 long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY, false); 1151 if (bestMatch != -1) { 1152 return bestMatch; 1153 } 1154 1155 updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher); 1156 updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher); 1157 1158 return -1; 1159 } 1160 1161 private interface IdentityLookupMatchQuery { 1162 final String TABLE = Tables.DATA + " dataA" 1163 + " JOIN " + Tables.DATA + " dataB" + 1164 " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE + 1165 " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")" 1166 + " JOIN " + Tables.RAW_CONTACTS + 1167 " ON (dataB." + Data.RAW_CONTACT_ID + " = " 1168 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 1169 1170 final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" 1171 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?" 1172 + " AND dataA." + Identity.NAMESPACE + " NOT NULL" 1173 + " AND dataA." + Identity.IDENTITY + " NOT NULL" 1174 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?" 1175 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" 1176 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 1177 1178 final String[] COLUMNS = new String[] { 1179 RawContacts.CONTACT_ID 1180 }; 1181 1182 int CONTACT_ID = 0; 1183 } 1184 1185 /** 1186 * Finds contacts with exact identity matches to the the specified raw contact. 1187 */ 1188 private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId, 1189 ContactMatcher matcher) { 1190 mSelectionArgs3[0] = String.valueOf(rawContactId); 1191 mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdIdentity); 1192 Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS, 1193 IdentityLookupMatchQuery.SELECTION, 1194 mSelectionArgs3, RawContacts.CONTACT_ID, null, null); 1195 try { 1196 while (c.moveToNext()) { 1197 final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID); 1198 matcher.matchIdentity(contactId); 1199 } 1200 } finally { 1201 c.close(); 1202 } 1203 1204 } 1205 1206 private interface NameLookupMatchQuery { 1207 String TABLE = Tables.NAME_LOOKUP + " nameA" 1208 + " JOIN " + Tables.NAME_LOOKUP + " nameB" + 1209 " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "=" 1210 + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")" 1211 + " JOIN " + Tables.RAW_CONTACTS + 1212 " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = " 1213 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 1214 1215 String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?" 1216 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" 1217 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 1218 1219 String[] COLUMNS = new String[] { 1220 RawContacts.CONTACT_ID, 1221 "nameA." + NameLookupColumns.NORMALIZED_NAME, 1222 "nameA." + NameLookupColumns.NAME_TYPE, 1223 "nameB." + NameLookupColumns.NAME_TYPE, 1224 }; 1225 1226 int CONTACT_ID = 0; 1227 int NAME = 1; 1228 int NAME_TYPE_A = 2; 1229 int NAME_TYPE_B = 3; 1230 } 1231 1232 /** 1233 * Finds contacts with names matching the name of the specified raw contact. 1234 */ 1235 private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId, 1236 ContactMatcher matcher) { 1237 mSelectionArgs1[0] = String.valueOf(rawContactId); 1238 Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS, 1239 NameLookupMatchQuery.SELECTION, 1240 mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING); 1241 try { 1242 while (c.moveToNext()) { 1243 long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID); 1244 String name = c.getString(NameLookupMatchQuery.NAME); 1245 int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A); 1246 int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B); 1247 matcher.matchName(contactId, nameTypeA, name, 1248 nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT); 1249 if (nameTypeA == NameLookupType.NICKNAME && 1250 nameTypeB == NameLookupType.NICKNAME) { 1251 matcher.updateScoreWithNicknameMatch(contactId); 1252 } 1253 } 1254 } finally { 1255 c.close(); 1256 } 1257 } 1258 1259 private interface NameLookupMatchQueryWithParameter { 1260 String TABLE = Tables.NAME_LOOKUP 1261 + " JOIN " + Tables.RAW_CONTACTS + 1262 " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = " 1263 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 1264 1265 String[] COLUMNS = new String[] { 1266 RawContacts.CONTACT_ID, 1267 NameLookupColumns.NORMALIZED_NAME, 1268 NameLookupColumns.NAME_TYPE, 1269 }; 1270 1271 int CONTACT_ID = 0; 1272 int NAME = 1; 1273 int NAME_TYPE = 2; 1274 } 1275 1276 private final class NameLookupSelectionBuilder extends NameLookupBuilder { 1277 1278 private final MatchCandidateList mNameLookupCandidates; 1279 1280 private StringBuilder mSelection = new StringBuilder( 1281 NameLookupColumns.NORMALIZED_NAME + " IN("); 1282 1283 1284 public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) { 1285 super(splitter); 1286 this.mNameLookupCandidates = candidates; 1287 } 1288 1289 @Override 1290 protected String[] getCommonNicknameClusters(String normalizedName) { 1291 return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); 1292 } 1293 1294 @Override 1295 protected void insertNameLookup( 1296 long rawContactId, long dataId, int lookupType, String string) { 1297 mNameLookupCandidates.add(string, lookupType); 1298 DatabaseUtils.appendEscapedSQLString(mSelection, string); 1299 mSelection.append(','); 1300 } 1301 1302 public boolean isEmpty() { 1303 return mNameLookupCandidates.isEmpty(); 1304 } 1305 1306 public String getSelection() { 1307 mSelection.setLength(mSelection.length() - 1); // Strip last comma 1308 mSelection.append(')'); 1309 return mSelection.toString(); 1310 } 1311 1312 public int getLookupType(String name) { 1313 for (int i = 0; i < mNameLookupCandidates.mCount; i++) { 1314 if (mNameLookupCandidates.mList.get(i).mName.equals(name)) { 1315 return mNameLookupCandidates.mList.get(i).mLookupType; 1316 } 1317 } 1318 throw new IllegalStateException(); 1319 } 1320 } 1321 1322 /** 1323 * Finds contacts with names matching the specified name. 1324 */ 1325 private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query, 1326 MatchCandidateList candidates, ContactMatcher matcher) { 1327 candidates.clear(); 1328 NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder( 1329 mNameSplitter, candidates); 1330 builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED); 1331 if (builder.isEmpty()) { 1332 return; 1333 } 1334 1335 Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE, 1336 NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null, 1337 null, PRIMARY_HIT_LIMIT_STRING); 1338 try { 1339 while (c.moveToNext()) { 1340 long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID); 1341 String name = c.getString(NameLookupMatchQueryWithParameter.NAME); 1342 int nameTypeA = builder.getLookupType(name); 1343 int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE); 1344 matcher.matchName(contactId, nameTypeA, name, nameTypeB, name, 1345 ContactMatcher.MATCHING_ALGORITHM_EXACT); 1346 if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) { 1347 matcher.updateScoreWithNicknameMatch(contactId); 1348 } 1349 } 1350 } finally { 1351 c.close(); 1352 } 1353 } 1354 1355 private interface EmailLookupQuery { 1356 String TABLE = Tables.DATA + " dataA" 1357 + " JOIN " + Tables.DATA + " dataB" + 1358 " ON (" + "dataA." + Email.DATA + "=dataB." + Email.DATA + ")" 1359 + " JOIN " + Tables.RAW_CONTACTS + 1360 " ON (dataB." + Data.RAW_CONTACT_ID + " = " 1361 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 1362 1363 String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" 1364 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?" 1365 + " AND dataA." + Email.DATA + " NOT NULL" 1366 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?" 1367 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" 1368 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 1369 1370 String[] COLUMNS = new String[] { 1371 RawContacts.CONTACT_ID 1372 }; 1373 1374 int CONTACT_ID = 0; 1375 } 1376 1377 private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, 1378 ContactMatcher matcher) { 1379 mSelectionArgs3[0] = String.valueOf(rawContactId); 1380 mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdEmail); 1381 Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS, 1382 EmailLookupQuery.SELECTION, 1383 mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING); 1384 try { 1385 while (c.moveToNext()) { 1386 long contactId = c.getLong(EmailLookupQuery.CONTACT_ID); 1387 matcher.updateScoreWithEmailMatch(contactId); 1388 } 1389 } finally { 1390 c.close(); 1391 } 1392 } 1393 1394 private interface PhoneLookupQuery { 1395 String TABLE = Tables.PHONE_LOOKUP + " phoneA" 1396 + " JOIN " + Tables.DATA + " dataA" 1397 + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")" 1398 + " JOIN " + Tables.PHONE_LOOKUP + " phoneB" 1399 + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "=" 1400 + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")" 1401 + " JOIN " + Tables.DATA + " dataB" 1402 + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")" 1403 + " JOIN " + Tables.RAW_CONTACTS 1404 + " ON (dataB." + Data.RAW_CONTACT_ID + " = " 1405 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 1406 1407 String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" 1408 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", " 1409 + "dataB." + Phone.NUMBER + ",?)" 1410 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" 1411 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 1412 1413 String[] COLUMNS = new String[] { 1414 RawContacts.CONTACT_ID 1415 }; 1416 1417 int CONTACT_ID = 0; 1418 } 1419 1420 private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId, 1421 ContactMatcher matcher) { 1422 mSelectionArgs2[0] = String.valueOf(rawContactId); 1423 mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter(); 1424 Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS, 1425 PhoneLookupQuery.SELECTION, 1426 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING); 1427 try { 1428 while (c.moveToNext()) { 1429 long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID); 1430 matcher.updateScoreWithPhoneNumberMatch(contactId); 1431 } 1432 } finally { 1433 c.close(); 1434 } 1435 } 1436 1437 /** 1438 * Loads name lookup rows for approximate name matching and updates match scores based on that 1439 * data. 1440 */ 1441 private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates, 1442 ContactMatcher matcher) { 1443 HashSet<String> firstLetters = new HashSet<String>(); 1444 for (int i = 0; i < candidates.mCount; i++) { 1445 final NameMatchCandidate candidate = candidates.mList.get(i); 1446 if (candidate.mName.length() >= 2) { 1447 String firstLetter = candidate.mName.substring(0, 2); 1448 if (!firstLetters.contains(firstLetter)) { 1449 firstLetters.add(firstLetter); 1450 final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '" 1451 + firstLetter + "*') AND " 1452 + NameLookupColumns.NAME_TYPE + " IN(" 1453 + NameLookupType.NAME_COLLATION_KEY + "," 1454 + NameLookupType.EMAIL_BASED_NICKNAME + "," 1455 + NameLookupType.NICKNAME + ")"; 1456 matchAllCandidates(db, selection, candidates, matcher, 1457 ContactMatcher.MATCHING_ALGORITHM_APPROXIMATE, 1458 String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT)); 1459 } 1460 } 1461 } 1462 } 1463 1464 private interface ContactNameLookupQuery { 1465 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 1466 1467 String[] COLUMNS = new String[] { 1468 RawContacts.CONTACT_ID, 1469 NameLookupColumns.NORMALIZED_NAME, 1470 NameLookupColumns.NAME_TYPE 1471 }; 1472 1473 int CONTACT_ID = 0; 1474 int NORMALIZED_NAME = 1; 1475 int NAME_TYPE = 2; 1476 } 1477 1478 /** 1479 * Loads all candidate rows from the name lookup table and updates match scores based 1480 * on that data. 1481 */ 1482 private void matchAllCandidates(SQLiteDatabase db, String selection, 1483 MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) { 1484 final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS, 1485 selection, null, null, null, null, limit); 1486 1487 try { 1488 while (c.moveToNext()) { 1489 Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID); 1490 String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME); 1491 int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE); 1492 1493 // Note the N^2 complexity of the following fragment. This is not a huge concern 1494 // since the number of candidates is very small and in general secondary hits 1495 // in the absence of primary hits are rare. 1496 for (int i = 0; i < candidates.mCount; i++) { 1497 NameMatchCandidate candidate = candidates.mList.get(i); 1498 matcher.matchName(contactId, candidate.mLookupType, candidate.mName, 1499 nameType, name, algorithm); 1500 } 1501 } 1502 } finally { 1503 c.close(); 1504 } 1505 } 1506 1507 private interface RawContactsQuery { 1508 String SQL_FORMAT = 1509 "SELECT " 1510 + RawContactsColumns.CONCRETE_ID + "," 1511 + RawContactsColumns.DISPLAY_NAME + "," 1512 + RawContactsColumns.DISPLAY_NAME_SOURCE + "," 1513 + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "," 1514 + AccountsColumns.CONCRETE_ACCOUNT_NAME + "," 1515 + AccountsColumns.CONCRETE_DATA_SET + "," 1516 + RawContacts.SOURCE_ID + "," 1517 + RawContacts.CUSTOM_RINGTONE + "," 1518 + RawContacts.SEND_TO_VOICEMAIL + "," 1519 + RawContacts.LAST_TIME_CONTACTED + "," 1520 + RawContacts.TIMES_CONTACTED + "," 1521 + RawContacts.STARRED + "," 1522 + RawContacts.NAME_VERIFIED + "," 1523 + DataColumns.CONCRETE_ID + "," 1524 + DataColumns.CONCRETE_MIMETYPE_ID + "," 1525 + Data.IS_SUPER_PRIMARY + "," 1526 + Photo.PHOTO_FILE_ID + 1527 " FROM " + Tables.RAW_CONTACTS + 1528 " JOIN " + Tables.ACCOUNTS + " ON (" 1529 + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID 1530 + ")" + 1531 " LEFT OUTER JOIN " + Tables.DATA + 1532 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID 1533 + " AND ((" + DataColumns.MIMETYPE_ID + "=%d" 1534 + " AND " + Photo.PHOTO + " NOT NULL)" 1535 + " OR (" + DataColumns.MIMETYPE_ID + "=%d" 1536 + " AND " + Phone.NUMBER + " NOT NULL)))"; 1537 1538 String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT + 1539 " WHERE " + RawContactsColumns.CONCRETE_ID + "=?"; 1540 1541 String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT + 1542 " WHERE " + RawContacts.CONTACT_ID + "=?" 1543 + " AND " + RawContacts.DELETED + "=0"; 1544 1545 int RAW_CONTACT_ID = 0; 1546 int DISPLAY_NAME = 1; 1547 int DISPLAY_NAME_SOURCE = 2; 1548 int ACCOUNT_TYPE = 3; 1549 int ACCOUNT_NAME = 4; 1550 int DATA_SET = 5; 1551 int SOURCE_ID = 6; 1552 int CUSTOM_RINGTONE = 7; 1553 int SEND_TO_VOICEMAIL = 8; 1554 int LAST_TIME_CONTACTED = 9; 1555 int TIMES_CONTACTED = 10; 1556 int STARRED = 11; 1557 int NAME_VERIFIED = 12; 1558 int DATA_ID = 13; 1559 int MIMETYPE_ID = 14; 1560 int IS_SUPER_PRIMARY = 15; 1561 int PHOTO_FILE_ID = 16; 1562 } 1563 1564 private interface ContactReplaceSqlStatement { 1565 String UPDATE_SQL = 1566 "UPDATE " + Tables.CONTACTS + 1567 " SET " 1568 + Contacts.NAME_RAW_CONTACT_ID + "=?, " 1569 + Contacts.PHOTO_ID + "=?, " 1570 + Contacts.PHOTO_FILE_ID + "=?, " 1571 + Contacts.SEND_TO_VOICEMAIL + "=?, " 1572 + Contacts.CUSTOM_RINGTONE + "=?, " 1573 + Contacts.LAST_TIME_CONTACTED + "=?, " 1574 + Contacts.TIMES_CONTACTED + "=?, " 1575 + Contacts.STARRED + "=?, " 1576 + Contacts.HAS_PHONE_NUMBER + "=?, " 1577 + Contacts.LOOKUP_KEY + "=? " + 1578 " WHERE " + Contacts._ID + "=?"; 1579 1580 String INSERT_SQL = 1581 "INSERT INTO " + Tables.CONTACTS + " (" 1582 + Contacts.NAME_RAW_CONTACT_ID + ", " 1583 + Contacts.PHOTO_ID + ", " 1584 + Contacts.PHOTO_FILE_ID + ", " 1585 + Contacts.SEND_TO_VOICEMAIL + ", " 1586 + Contacts.CUSTOM_RINGTONE + ", " 1587 + Contacts.LAST_TIME_CONTACTED + ", " 1588 + Contacts.TIMES_CONTACTED + ", " 1589 + Contacts.STARRED + ", " 1590 + Contacts.HAS_PHONE_NUMBER + ", " 1591 + Contacts.LOOKUP_KEY + ") " + 1592 " VALUES (?,?,?,?,?,?,?,?,?,?)"; 1593 1594 int NAME_RAW_CONTACT_ID = 1; 1595 int PHOTO_ID = 2; 1596 int PHOTO_FILE_ID = 3; 1597 int SEND_TO_VOICEMAIL = 4; 1598 int CUSTOM_RINGTONE = 5; 1599 int LAST_TIME_CONTACTED = 6; 1600 int TIMES_CONTACTED = 7; 1601 int STARRED = 8; 1602 int HAS_PHONE_NUMBER = 9; 1603 int LOOKUP_KEY = 10; 1604 int CONTACT_ID = 11; 1605 } 1606 1607 /** 1608 * Computes aggregate-level data for the specified aggregate contact ID. 1609 */ 1610 private void computeAggregateData(SQLiteDatabase db, long contactId, 1611 SQLiteStatement statement) { 1612 mSelectionArgs1[0] = String.valueOf(contactId); 1613 computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement); 1614 } 1615 1616 /** 1617 * Indicates whether the given photo entry and priority gives this photo a higher overall 1618 * priority than the current best photo entry and priority. 1619 */ 1620 private boolean hasHigherPhotoPriority(PhotoEntry photoEntry, int priority, 1621 PhotoEntry bestPhotoEntry, int bestPriority) { 1622 int photoComparison = photoEntry.compareTo(bestPhotoEntry); 1623 return photoComparison < 0 || photoComparison == 0 && priority > bestPriority; 1624 } 1625 1626 /** 1627 * Computes aggregate-level data from constituent raw contacts. 1628 */ 1629 private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs, 1630 SQLiteStatement statement) { 1631 long currentRawContactId = -1; 1632 long bestPhotoId = -1; 1633 long bestPhotoFileId = 0; 1634 PhotoEntry bestPhotoEntry = null; 1635 boolean foundSuperPrimaryPhoto = false; 1636 int photoPriority = -1; 1637 int totalRowCount = 0; 1638 int contactSendToVoicemail = 0; 1639 String contactCustomRingtone = null; 1640 long contactLastTimeContacted = 0; 1641 int contactTimesContacted = 0; 1642 int contactStarred = 0; 1643 int hasPhoneNumber = 0; 1644 StringBuilder lookupKey = new StringBuilder(); 1645 1646 mDisplayNameCandidate.clear(); 1647 1648 Cursor c = db.rawQuery(sql, sqlArgs); 1649 try { 1650 while (c.moveToNext()) { 1651 long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID); 1652 if (rawContactId != currentRawContactId) { 1653 currentRawContactId = rawContactId; 1654 totalRowCount++; 1655 1656 // Assemble sub-account. 1657 String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); 1658 String dataSet = c.getString(RawContactsQuery.DATA_SET); 1659 String accountWithDataSet = (!TextUtils.isEmpty(dataSet)) 1660 ? accountType + "/" + dataSet 1661 : accountType; 1662 1663 // Display name 1664 String displayName = c.getString(RawContactsQuery.DISPLAY_NAME); 1665 int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE); 1666 int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED); 1667 processDisplayNameCandidate(rawContactId, displayName, displayNameSource, 1668 mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet), 1669 nameVerified != 0); 1670 1671 // Contact options 1672 if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) { 1673 boolean sendToVoicemail = 1674 (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0); 1675 if (sendToVoicemail) { 1676 contactSendToVoicemail++; 1677 } 1678 } 1679 1680 if (contactCustomRingtone == null 1681 && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) { 1682 contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE); 1683 } 1684 1685 long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED); 1686 if (lastTimeContacted > contactLastTimeContacted) { 1687 contactLastTimeContacted = lastTimeContacted; 1688 } 1689 1690 int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED); 1691 if (timesContacted > contactTimesContacted) { 1692 contactTimesContacted = timesContacted; 1693 } 1694 1695 if (c.getInt(RawContactsQuery.STARRED) != 0) { 1696 contactStarred = 1; 1697 } 1698 1699 appendLookupKey( 1700 lookupKey, 1701 accountWithDataSet, 1702 c.getString(RawContactsQuery.ACCOUNT_NAME), 1703 rawContactId, 1704 c.getString(RawContactsQuery.SOURCE_ID), 1705 displayName); 1706 } 1707 1708 if (!c.isNull(RawContactsQuery.DATA_ID)) { 1709 long dataId = c.getLong(RawContactsQuery.DATA_ID); 1710 long photoFileId = c.getLong(RawContactsQuery.PHOTO_FILE_ID); 1711 int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID); 1712 boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0; 1713 if (mimetypeId == mMimeTypeIdPhoto) { 1714 if (!foundSuperPrimaryPhoto) { 1715 // Lookup the metadata for the photo, if available. Note that data set 1716 // does not come into play here, since accounts are looked up in the 1717 // account manager in the priority resolver. 1718 PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId); 1719 String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); 1720 int priority = mPhotoPriorityResolver.getPhotoPriority(accountType); 1721 if (superPrimary || hasHigherPhotoPriority( 1722 photoEntry, priority, bestPhotoEntry, photoPriority)) { 1723 bestPhotoEntry = photoEntry; 1724 photoPriority = priority; 1725 bestPhotoId = dataId; 1726 bestPhotoFileId = photoFileId; 1727 foundSuperPrimaryPhoto |= superPrimary; 1728 } 1729 } 1730 } else if (mimetypeId == mMimeTypeIdPhone) { 1731 hasPhoneNumber = 1; 1732 } 1733 } 1734 } 1735 } finally { 1736 c.close(); 1737 } 1738 1739 statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID, 1740 mDisplayNameCandidate.rawContactId); 1741 1742 if (bestPhotoId != -1) { 1743 statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId); 1744 } else { 1745 statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID); 1746 } 1747 1748 if (bestPhotoFileId != 0) { 1749 statement.bindLong(ContactReplaceSqlStatement.PHOTO_FILE_ID, bestPhotoFileId); 1750 } else { 1751 statement.bindNull(ContactReplaceSqlStatement.PHOTO_FILE_ID); 1752 } 1753 1754 statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL, 1755 totalRowCount == contactSendToVoicemail ? 1 : 0); 1756 DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE, 1757 contactCustomRingtone); 1758 statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED, 1759 contactLastTimeContacted); 1760 statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED, 1761 contactTimesContacted); 1762 statement.bindLong(ContactReplaceSqlStatement.STARRED, 1763 contactStarred); 1764 statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER, 1765 hasPhoneNumber); 1766 statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY, 1767 Uri.encode(lookupKey.toString())); 1768 } 1769 1770 /** 1771 * Builds a lookup key using the given data. 1772 */ 1773 protected void appendLookupKey(StringBuilder sb, String accountTypeWithDataSet, 1774 String accountName, long rawContactId, String sourceId, String displayName) { 1775 ContactLookupKey.appendToLookupKey(sb, accountTypeWithDataSet, accountName, rawContactId, 1776 sourceId, displayName); 1777 } 1778 1779 /** 1780 * Uses the supplied values to determine if they represent a "better" display name 1781 * for the aggregate contact currently evaluated. If so, it updates 1782 * {@link #mDisplayNameCandidate} with the new values. 1783 */ 1784 private void processDisplayNameCandidate(long rawContactId, String displayName, 1785 int displayNameSource, boolean writableAccount, boolean verified) { 1786 1787 boolean replace = false; 1788 if (mDisplayNameCandidate.rawContactId == -1) { 1789 // No previous values available 1790 replace = true; 1791 } else if (!TextUtils.isEmpty(displayName)) { 1792 if (!mDisplayNameCandidate.verified && verified) { 1793 // A verified name is better than any other name 1794 replace = true; 1795 } else if (mDisplayNameCandidate.verified == verified) { 1796 if (mDisplayNameCandidate.displayNameSource < displayNameSource) { 1797 // New values come from an superior source, e.g. structured name vs phone number 1798 replace = true; 1799 } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) { 1800 if (!mDisplayNameCandidate.writableAccount && writableAccount) { 1801 replace = true; 1802 } else if (mDisplayNameCandidate.writableAccount == writableAccount) { 1803 if (NameNormalizer.compareComplexity(displayName, 1804 mDisplayNameCandidate.displayName) > 0) { 1805 // New name is more complex than the previously found one 1806 replace = true; 1807 } 1808 } 1809 } 1810 } 1811 } 1812 1813 if (replace) { 1814 mDisplayNameCandidate.rawContactId = rawContactId; 1815 mDisplayNameCandidate.displayName = displayName; 1816 mDisplayNameCandidate.displayNameSource = displayNameSource; 1817 mDisplayNameCandidate.verified = verified; 1818 mDisplayNameCandidate.writableAccount = writableAccount; 1819 } 1820 } 1821 1822 private interface PhotoIdQuery { 1823 final String[] COLUMNS = new String[] { 1824 AccountsColumns.CONCRETE_ACCOUNT_TYPE, 1825 DataColumns.CONCRETE_ID, 1826 Data.IS_SUPER_PRIMARY, 1827 Photo.PHOTO_FILE_ID, 1828 }; 1829 1830 int ACCOUNT_TYPE = 0; 1831 int DATA_ID = 1; 1832 int IS_SUPER_PRIMARY = 2; 1833 int PHOTO_FILE_ID = 3; 1834 } 1835 1836 public void updatePhotoId(SQLiteDatabase db, long rawContactId) { 1837 1838 long contactId = mDbHelper.getContactId(rawContactId); 1839 if (contactId == 0) { 1840 return; 1841 } 1842 1843 long bestPhotoId = -1; 1844 long bestPhotoFileId = 0; 1845 int photoPriority = -1; 1846 1847 long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE); 1848 1849 String tables = Tables.RAW_CONTACTS 1850 + " JOIN " + Tables.ACCOUNTS + " ON (" 1851 + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID 1852 + ")" 1853 + " JOIN " + Tables.DATA + " ON(" 1854 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID 1855 + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND " 1856 + Photo.PHOTO + " NOT NULL))"; 1857 1858 mSelectionArgs1[0] = String.valueOf(contactId); 1859 final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS, 1860 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null); 1861 try { 1862 PhotoEntry bestPhotoEntry = null; 1863 while (c.moveToNext()) { 1864 long dataId = c.getLong(PhotoIdQuery.DATA_ID); 1865 long photoFileId = c.getLong(PhotoIdQuery.PHOTO_FILE_ID); 1866 boolean superPrimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0; 1867 PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId); 1868 1869 // Note that data set does not come into play here, since accounts are looked up in 1870 // the account manager in the priority resolver. 1871 String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE); 1872 int priority = mPhotoPriorityResolver.getPhotoPriority(accountType); 1873 if (superPrimary || hasHigherPhotoPriority( 1874 photoEntry, priority, bestPhotoEntry, photoPriority)) { 1875 bestPhotoEntry = photoEntry; 1876 photoPriority = priority; 1877 bestPhotoId = dataId; 1878 bestPhotoFileId = photoFileId; 1879 if (superPrimary) { 1880 break; 1881 } 1882 } 1883 } 1884 } finally { 1885 c.close(); 1886 } 1887 1888 if (bestPhotoId == -1) { 1889 mPhotoIdUpdate.bindNull(1); 1890 } else { 1891 mPhotoIdUpdate.bindLong(1, bestPhotoId); 1892 } 1893 1894 if (bestPhotoFileId == 0) { 1895 mPhotoIdUpdate.bindNull(2); 1896 } else { 1897 mPhotoIdUpdate.bindLong(2, bestPhotoFileId); 1898 } 1899 1900 mPhotoIdUpdate.bindLong(3, contactId); 1901 mPhotoIdUpdate.execute(); 1902 } 1903 1904 private interface PhotoFileQuery { 1905 final String[] COLUMNS = new String[] { 1906 PhotoFiles.HEIGHT, 1907 PhotoFiles.WIDTH, 1908 PhotoFiles.FILESIZE 1909 }; 1910 1911 int HEIGHT = 0; 1912 int WIDTH = 1; 1913 int FILESIZE = 2; 1914 } 1915 1916 private class PhotoEntry implements Comparable<PhotoEntry> { 1917 // Pixel count (width * height) for the image. 1918 final int pixelCount; 1919 1920 // File size (in bytes) of the image. Not populated if the image is a thumbnail. 1921 final int fileSize; 1922 1923 private PhotoEntry(int pixelCount, int fileSize) { 1924 this.pixelCount = pixelCount; 1925 this.fileSize = fileSize; 1926 } 1927 1928 @Override 1929 public int compareTo(PhotoEntry pe) { 1930 if (pe == null) { 1931 return -1; 1932 } 1933 if (pixelCount == pe.pixelCount) { 1934 return pe.fileSize - fileSize; 1935 } else { 1936 return pe.pixelCount - pixelCount; 1937 } 1938 } 1939 } 1940 1941 private PhotoEntry getPhotoMetadata(SQLiteDatabase db, long photoFileId) { 1942 if (photoFileId == 0) { 1943 // Assume standard thumbnail size. Don't bother getting a file size for priority; 1944 // we should fall back to photo priority resolver if all we have are thumbnails. 1945 int thumbDim = mContactsProvider.getMaxThumbnailPhotoDim(); 1946 return new PhotoEntry(thumbDim * thumbDim, 0); 1947 } else { 1948 Cursor c = db.query(Tables.PHOTO_FILES, PhotoFileQuery.COLUMNS, PhotoFiles._ID + "=?", 1949 new String[]{String.valueOf(photoFileId)}, null, null, null); 1950 try { 1951 if (c.getCount() == 1) { 1952 c.moveToFirst(); 1953 int pixelCount = 1954 c.getInt(PhotoFileQuery.HEIGHT) * c.getInt(PhotoFileQuery.WIDTH); 1955 return new PhotoEntry(pixelCount, c.getInt(PhotoFileQuery.FILESIZE)); 1956 } 1957 } finally { 1958 c.close(); 1959 } 1960 } 1961 return new PhotoEntry(0, 0); 1962 } 1963 1964 private interface DisplayNameQuery { 1965 String[] COLUMNS = new String[] { 1966 RawContacts._ID, 1967 RawContactsColumns.DISPLAY_NAME, 1968 RawContactsColumns.DISPLAY_NAME_SOURCE, 1969 RawContacts.NAME_VERIFIED, 1970 RawContacts.SOURCE_ID, 1971 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 1972 }; 1973 1974 int _ID = 0; 1975 int DISPLAY_NAME = 1; 1976 int DISPLAY_NAME_SOURCE = 2; 1977 int NAME_VERIFIED = 3; 1978 int SOURCE_ID = 4; 1979 int ACCOUNT_TYPE_AND_DATA_SET = 5; 1980 } 1981 1982 public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) { 1983 long contactId = mDbHelper.getContactId(rawContactId); 1984 if (contactId == 0) { 1985 return; 1986 } 1987 1988 updateDisplayNameForContact(db, contactId); 1989 } 1990 1991 public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) { 1992 boolean lookupKeyUpdateNeeded = false; 1993 1994 mDisplayNameCandidate.clear(); 1995 1996 mSelectionArgs1[0] = String.valueOf(contactId); 1997 final Cursor c = db.query(Views.RAW_CONTACTS, DisplayNameQuery.COLUMNS, 1998 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null); 1999 try { 2000 while (c.moveToNext()) { 2001 long rawContactId = c.getLong(DisplayNameQuery._ID); 2002 String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME); 2003 int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE); 2004 int nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED); 2005 String accountTypeAndDataSet = c.getString( 2006 DisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET); 2007 processDisplayNameCandidate(rawContactId, displayName, displayNameSource, 2008 mContactsProvider.isWritableAccountWithDataSet(accountTypeAndDataSet), 2009 nameVerified != 0); 2010 2011 // If the raw contact has no source id, the lookup key is based on the display 2012 // name, so the lookup key needs to be updated. 2013 lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID); 2014 } 2015 } finally { 2016 c.close(); 2017 } 2018 2019 if (mDisplayNameCandidate.rawContactId != -1) { 2020 mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId); 2021 mDisplayNameUpdate.bindLong(2, contactId); 2022 mDisplayNameUpdate.execute(); 2023 } 2024 2025 if (lookupKeyUpdateNeeded) { 2026 updateLookupKeyForContact(db, contactId); 2027 } 2028 } 2029 2030 2031 /** 2032 * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the 2033 * specified raw contact. 2034 */ 2035 public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) { 2036 2037 long contactId = mDbHelper.getContactId(rawContactId); 2038 if (contactId == 0) { 2039 return; 2040 } 2041 2042 final SQLiteStatement hasPhoneNumberUpdate = db.compileStatement( 2043 "UPDATE " + Tables.CONTACTS + 2044 " SET " + Contacts.HAS_PHONE_NUMBER + "=" 2045 + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)" 2046 + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS 2047 + " WHERE " + DataColumns.MIMETYPE_ID + "=?" 2048 + " AND " + Phone.NUMBER + " NOT NULL" 2049 + " AND " + RawContacts.CONTACT_ID + "=?)" + 2050 " WHERE " + Contacts._ID + "=?"); 2051 try { 2052 hasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE)); 2053 hasPhoneNumberUpdate.bindLong(2, contactId); 2054 hasPhoneNumberUpdate.bindLong(3, contactId); 2055 hasPhoneNumberUpdate.execute(); 2056 } finally { 2057 hasPhoneNumberUpdate.close(); 2058 } 2059 } 2060 2061 private interface LookupKeyQuery { 2062 String TABLE = Views.RAW_CONTACTS; 2063 String[] COLUMNS = new String[] { 2064 RawContacts._ID, 2065 RawContactsColumns.DISPLAY_NAME, 2066 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 2067 RawContacts.ACCOUNT_NAME, 2068 RawContacts.SOURCE_ID, 2069 }; 2070 2071 int ID = 0; 2072 int DISPLAY_NAME = 1; 2073 int ACCOUNT_TYPE_AND_DATA_SET = 2; 2074 int ACCOUNT_NAME = 3; 2075 int SOURCE_ID = 4; 2076 } 2077 2078 public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) { 2079 long contactId = mDbHelper.getContactId(rawContactId); 2080 if (contactId == 0) { 2081 return; 2082 } 2083 2084 updateLookupKeyForContact(db, contactId); 2085 } 2086 2087 private void updateLookupKeyForContact(SQLiteDatabase db, long contactId) { 2088 String lookupKey = computeLookupKeyForContact(db, contactId); 2089 2090 if (lookupKey == null) { 2091 mLookupKeyUpdate.bindNull(1); 2092 } else { 2093 mLookupKeyUpdate.bindString(1, Uri.encode(lookupKey)); 2094 } 2095 mLookupKeyUpdate.bindLong(2, contactId); 2096 2097 mLookupKeyUpdate.execute(); 2098 } 2099 2100 protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) { 2101 StringBuilder sb = new StringBuilder(); 2102 mSelectionArgs1[0] = String.valueOf(contactId); 2103 final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS, 2104 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID); 2105 try { 2106 while (c.moveToNext()) { 2107 ContactLookupKey.appendToLookupKey(sb, 2108 c.getString(LookupKeyQuery.ACCOUNT_TYPE_AND_DATA_SET), 2109 c.getString(LookupKeyQuery.ACCOUNT_NAME), 2110 c.getLong(LookupKeyQuery.ID), 2111 c.getString(LookupKeyQuery.SOURCE_ID), 2112 c.getString(LookupKeyQuery.DISPLAY_NAME)); 2113 } 2114 } finally { 2115 c.close(); 2116 } 2117 return sb.length() == 0 ? null : sb.toString(); 2118 } 2119 2120 /** 2121 * Execute {@link SQLiteStatement} that will update the 2122 * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}. 2123 */ 2124 public void updateStarred(long rawContactId) { 2125 long contactId = mDbHelper.getContactId(rawContactId); 2126 if (contactId == 0) { 2127 return; 2128 } 2129 2130 mStarredUpdate.bindLong(1, contactId); 2131 mStarredUpdate.execute(); 2132 } 2133 2134 /** 2135 * Finds matching contacts and returns a cursor on those. 2136 */ 2137 public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb, 2138 String[] projection, long contactId, int maxSuggestions, String filter, 2139 ArrayList<AggregationSuggestionParameter> parameters) { 2140 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 2141 db.beginTransaction(); 2142 try { 2143 List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters); 2144 return queryMatchingContacts(qb, db, projection, bestMatches, maxSuggestions, filter); 2145 } finally { 2146 db.endTransaction(); 2147 } 2148 } 2149 2150 private interface ContactIdQuery { 2151 String[] COLUMNS = new String[] { 2152 Contacts._ID 2153 }; 2154 2155 int _ID = 0; 2156 } 2157 2158 /** 2159 * Loads contacts with specified IDs and returns them in the order of IDs in the 2160 * supplied list. 2161 */ 2162 private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db, 2163 String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) { 2164 StringBuilder sb = new StringBuilder(); 2165 sb.append(Contacts._ID); 2166 sb.append(" IN ("); 2167 for (int i = 0; i < bestMatches.size(); i++) { 2168 MatchScore matchScore = bestMatches.get(i); 2169 if (i != 0) { 2170 sb.append(","); 2171 } 2172 sb.append(matchScore.getContactId()); 2173 } 2174 sb.append(")"); 2175 2176 if (!TextUtils.isEmpty(filter)) { 2177 sb.append(" AND " + Contacts._ID + " IN "); 2178 mContactsProvider.appendContactFilterAsNestedQuery(sb, filter); 2179 } 2180 2181 // Run a query and find ids of best matching contacts satisfying the filter (if any) 2182 HashSet<Long> foundIds = new HashSet<Long>(); 2183 Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(), 2184 null, null, null, null); 2185 try { 2186 while(cursor.moveToNext()) { 2187 foundIds.add(cursor.getLong(ContactIdQuery._ID)); 2188 } 2189 } finally { 2190 cursor.close(); 2191 } 2192 2193 // Exclude all contacts that did not match the filter 2194 Iterator<MatchScore> iter = bestMatches.iterator(); 2195 while (iter.hasNext()) { 2196 long id = iter.next().getContactId(); 2197 if (!foundIds.contains(id)) { 2198 iter.remove(); 2199 } 2200 } 2201 2202 // Limit the number of returned suggestions 2203 final List<MatchScore> limitedMatches; 2204 if (bestMatches.size() > maxSuggestions) { 2205 limitedMatches = bestMatches.subList(0, maxSuggestions); 2206 } else { 2207 limitedMatches = bestMatches; 2208 } 2209 2210 // Build an in-clause with the remaining contact IDs 2211 sb.setLength(0); 2212 sb.append(Contacts._ID); 2213 sb.append(" IN ("); 2214 for (int i = 0; i < limitedMatches.size(); i++) { 2215 MatchScore matchScore = limitedMatches.get(i); 2216 if (i != 0) { 2217 sb.append(","); 2218 } 2219 sb.append(matchScore.getContactId()); 2220 } 2221 sb.append(")"); 2222 2223 // Run the final query with the required projection and contact IDs found by the first query 2224 cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID); 2225 2226 // Build a sorted list of discovered IDs 2227 ArrayList<Long> sortedContactIds = new ArrayList<Long>(limitedMatches.size()); 2228 for (MatchScore matchScore : limitedMatches) { 2229 sortedContactIds.add(matchScore.getContactId()); 2230 } 2231 2232 Collections.sort(sortedContactIds); 2233 2234 // Map cursor indexes according to the descending order of match scores 2235 int[] positionMap = new int[limitedMatches.size()]; 2236 for (int i = 0; i < positionMap.length; i++) { 2237 long id = limitedMatches.get(i).getContactId(); 2238 positionMap[i] = sortedContactIds.indexOf(id); 2239 } 2240 2241 return new ReorderingCursorWrapper(cursor, positionMap); 2242 } 2243 2244 /** 2245 * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the 2246 * descending order of match score. 2247 * @param parameters 2248 */ 2249 private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId, 2250 ArrayList<AggregationSuggestionParameter> parameters) { 2251 2252 MatchCandidateList candidates = new MatchCandidateList(); 2253 ContactMatcher matcher = new ContactMatcher(); 2254 2255 // Don't aggregate a contact with itself 2256 matcher.keepOut(contactId); 2257 2258 if (parameters == null || parameters.size() == 0) { 2259 final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS, 2260 RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null); 2261 try { 2262 while (c.moveToNext()) { 2263 long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID); 2264 updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates, 2265 matcher); 2266 } 2267 } finally { 2268 c.close(); 2269 } 2270 } else { 2271 updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates, 2272 matcher, parameters); 2273 } 2274 2275 return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST); 2276 } 2277 2278 /** 2279 * Computes scores for contacts that have matching data rows. 2280 */ 2281 private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, 2282 long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) { 2283 2284 updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher); 2285 updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher); 2286 updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher); 2287 updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher); 2288 loadNameMatchCandidates(db, rawContactId, candidates, false); 2289 lookupApproximateNameMatches(db, candidates, matcher); 2290 } 2291 2292 private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, 2293 MatchCandidateList candidates, ContactMatcher matcher, 2294 ArrayList<AggregationSuggestionParameter> parameters) { 2295 for (AggregationSuggestionParameter parameter : parameters) { 2296 if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) { 2297 updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher); 2298 } 2299 2300 // TODO: add support for other parameter kinds 2301 } 2302 } 2303} 2304