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