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