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