1 /* 2 * Copyright (C) 2015 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.aggregation; 18 19 import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_PRIMARY; 20 import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SECONDARY; 21 import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SUGGEST; 22 import android.database.Cursor; 23 import android.database.sqlite.SQLiteDatabase; 24 import android.provider.ContactsContract.AggregationExceptions; 25 import android.provider.ContactsContract.CommonDataKinds.Email; 26 import android.provider.ContactsContract.CommonDataKinds.Identity; 27 import android.provider.ContactsContract.CommonDataKinds.Phone; 28 import android.provider.ContactsContract.Contacts.AggregationSuggestions; 29 import android.provider.ContactsContract.Data; 30 import android.provider.ContactsContract.FullNameStyle; 31 import android.provider.ContactsContract.PhotoFiles; 32 import android.provider.ContactsContract.RawContacts; 33 import android.text.TextUtils; 34 import android.util.ArraySet; 35 import android.util.Log; 36 import com.android.providers.contacts.ContactsDatabaseHelper; 37 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; 38 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; 39 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; 40 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns; 41 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; 42 import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 43 import com.android.providers.contacts.ContactsProvider2; 44 import com.android.providers.contacts.NameSplitter; 45 import com.android.providers.contacts.PhotoPriorityResolver; 46 import com.android.providers.contacts.TransactionContext; 47 import com.android.providers.contacts.aggregation.util.CommonNicknameCache; 48 import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper; 49 import com.android.providers.contacts.aggregation.util.MatchScore; 50 import com.android.providers.contacts.aggregation.util.RawContactMatcher; 51 import com.android.providers.contacts.aggregation.util.RawContactMatchingCandidates; 52 import com.android.providers.contacts.database.ContactsTableUtil; 53 import com.google.android.collect.Sets; 54 import com.google.common.collect.HashMultimap; 55 import com.google.common.collect.Multimap; 56 57 import java.util.ArrayList; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.Set; 61 62 /** 63 * ContactAggregator2 deals with aggregating contact information with sufficient matching data 64 * points. E.g., two John Doe contacts with same phone numbers are presumed to be the same 65 * person unless the user declares otherwise. 66 */ 67 public class ContactAggregator2 extends AbstractContactAggregator { 68 69 // Possible operation types for contacts aggregation. 70 private static final int CREATE_NEW_CONTACT = 1; 71 private static final int KEEP_INTACT = 0; 72 private static final int RE_AGGREGATE = -1; 73 74 private final RawContactMatcher mMatcher = new RawContactMatcher(); 75 76 /** 77 * Constructor. 78 */ ContactAggregator2(ContactsProvider2 contactsProvider, ContactsDatabaseHelper contactsDatabaseHelper, PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter, CommonNicknameCache commonNicknameCache)79 public ContactAggregator2(ContactsProvider2 contactsProvider, 80 ContactsDatabaseHelper contactsDatabaseHelper, 81 PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter, 82 CommonNicknameCache commonNicknameCache) { 83 super(contactsProvider, contactsDatabaseHelper, photoPriorityResolver, nameSplitter, 84 commonNicknameCache); 85 } 86 87 /** 88 * Given a specific raw contact, finds all matching raw contacts and re-aggregate them 89 * based on the matching connectivity. 90 */ aggregateContact(TransactionContext txContext, SQLiteDatabase db, long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates)91 synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db, 92 long rawContactId, long accountId, long currentContactId, 93 MatchCandidateList candidates) { 94 95 if (!needAggregate(db, rawContactId)) { 96 if (VERBOSE_LOGGING) { 97 Log.v(TAG, "Skip rid=" + rawContactId + " which has already been aggregated."); 98 } 99 return; 100 } 101 102 if (VERBOSE_LOGGING) { 103 Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId); 104 } 105 106 int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; 107 108 Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId); 109 if (aggModeObject != null) { 110 aggregationMode = aggModeObject; 111 } 112 113 RawContactMatcher matcher = new RawContactMatcher(); 114 RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates(); 115 if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { 116 // If this is a newly inserted contact or a visible contact, look for 117 // data matches. 118 if (currentContactId == 0 119 || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) { 120 // Find the set of matching candidates 121 matchingCandidates = findRawContactMatchingCandidates(db, rawContactId, candidates, 122 matcher); 123 } 124 } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) { 125 return; 126 } 127 128 // # of raw_contacts in the [currentContactId] contact excluding the [rawContactId] 129 // raw_contact. 130 long currentContactContentsCount = 0; 131 132 if (currentContactId != 0) { 133 mRawContactCountQuery.bindLong(1, currentContactId); 134 mRawContactCountQuery.bindLong(2, rawContactId); 135 currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong(); 136 } 137 138 // Set aggregation operation, i.e., re-aggregate, keep intact, or create new contact based 139 // on the number of matching candidates and the number of raw_contacts in the 140 // [currentContactId] excluding the [rawContactId]. 141 final int operation; 142 final int candidatesCount = matchingCandidates.getCount(); 143 if (candidatesCount >= AGGREGATION_CONTACT_SIZE_LIMIT) { 144 operation = KEEP_INTACT; 145 if (VERBOSE_LOGGING) { 146 Log.v(TAG, "Too many matching raw contacts (" + candidatesCount 147 + ") are found, so skip aggregation"); 148 } 149 } else if (candidatesCount > 0) { 150 operation = RE_AGGREGATE; 151 } else { 152 // When there is no matching raw contact found, if there are no other raw contacts in 153 // the current aggregate, we might as well reuse it. Also, if the aggregation mode is 154 // SUSPENDED, we must reuse the same aggregate. 155 if (currentContactId != 0 156 && (currentContactContentsCount == 0 157 || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) { 158 operation = KEEP_INTACT; 159 } else { 160 operation = CREATE_NEW_CONTACT; 161 } 162 } 163 164 if (operation == KEEP_INTACT) { 165 // Aggregation unchanged 166 if (VERBOSE_LOGGING) { 167 Log.v(TAG, "Aggregation unchanged"); 168 } 169 markAggregated(db, String.valueOf(rawContactId)); 170 } else if (operation == CREATE_NEW_CONTACT) { 171 // create new contact for [rawContactId] 172 if (VERBOSE_LOGGING) { 173 Log.v(TAG, "create new contact for rid=" + rawContactId); 174 } 175 createContactForRawContacts(db, txContext, Sets.newHashSet(rawContactId), null); 176 if (currentContactContentsCount > 0) { 177 updateAggregateData(txContext, currentContactId); 178 } 179 markAggregated(db, String.valueOf(rawContactId)); 180 } else { 181 // re-aggregate 182 if (VERBOSE_LOGGING) { 183 Log.v(TAG, "Re-aggregating rids=" + rawContactId + "," 184 + TextUtils.join(",", matchingCandidates.getRawContactIdSet())); 185 } 186 reAggregateRawContacts(txContext, db, currentContactId, rawContactId, accountId, 187 currentContactContentsCount, matchingCandidates); 188 } 189 } 190 needAggregate(SQLiteDatabase db, long rawContactId)191 private boolean needAggregate(SQLiteDatabase db, long rawContactId) { 192 final String sql = "SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS + 193 " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1" + 194 " AND " + RawContacts._ID + "=?"; 195 196 mSelectionArgs1[0] = String.valueOf(rawContactId); 197 final Cursor cursor = db.rawQuery(sql, mSelectionArgs1); 198 199 try { 200 return cursor.getCount() != 0; 201 } finally { 202 cursor.close(); 203 } 204 } 205 /** 206 * Find the set of matching raw contacts for given rawContactId. Add all the raw contact 207 * candidates with matching scores > threshold to RawContactMatchingCandidates. Keep doing 208 * this for every raw contact in RawContactMatchingCandidates until is it not changing. 209 */ findRawContactMatchingCandidates(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher)210 private RawContactMatchingCandidates findRawContactMatchingCandidates(SQLiteDatabase db, long 211 rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) { 212 updateMatchScores(db, rawContactId, candidates, matcher); 213 final RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates( 214 matcher.pickBestMatches()); 215 Set<Long> newIds = new ArraySet<>(); 216 newIds.addAll(matchingCandidates.getRawContactIdSet()); 217 // Keep doing the following until no new raw contact candidate is found. 218 while (!newIds.isEmpty()) { 219 if (matchingCandidates.getCount() >= AGGREGATION_CONTACT_SIZE_LIMIT) { 220 return matchingCandidates; 221 } 222 final Set<Long> tmpIdSet = new ArraySet<>(); 223 for (long rId : newIds) { 224 final RawContactMatcher rMatcher = new RawContactMatcher(); 225 updateMatchScores(db, rId, new MatchCandidateList(), 226 rMatcher); 227 List<MatchScore> newMatches = rMatcher.pickBestMatches(); 228 for (MatchScore newMatch : newMatches) { 229 final long newRawContactId = newMatch.getRawContactId(); 230 if (!matchingCandidates.getRawContactIdSet().contains(newRawContactId)) { 231 tmpIdSet.add(newRawContactId); 232 matchingCandidates.add(newMatch); 233 } 234 } 235 } 236 newIds.clear(); 237 newIds.addAll(tmpIdSet); 238 } 239 return matchingCandidates; 240 } 241 242 /** 243 * Find out which mime-types are shared by more than one contacts for {@code rawContactIds}. 244 * Clear the is_super_primary settings for these mime-types. 245 * {@code rawContactIds} should be a comma separated ID list. 246 */ clearSuperPrimarySetting(SQLiteDatabase db, String rawContactIds)247 private void clearSuperPrimarySetting(SQLiteDatabase db, String rawContactIds) { 248 final String sql = 249 "SELECT " + DataColumns.MIMETYPE_ID + ", count(1) c FROM " + 250 Tables.DATA +" WHERE " + Data.IS_SUPER_PRIMARY + " = 1 AND " + 251 Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ") group by " + 252 DataColumns.MIMETYPE_ID + " HAVING c > 1"; 253 254 // Find out which mime-types exist with is_super_primary=true on more then one contacts. 255 int index = 0; 256 final StringBuilder mimeTypeCondition = new StringBuilder(); 257 mimeTypeCondition.append(" AND " + DataColumns.MIMETYPE_ID + " IN ("); 258 259 final Cursor c = db.rawQuery(sql, null); 260 try { 261 c.moveToPosition(-1); 262 while (c.moveToNext()) { 263 if (index > 0) { 264 mimeTypeCondition.append(','); 265 } 266 mimeTypeCondition.append(c.getLong((0))); 267 index++; 268 } 269 } finally { 270 c.close(); 271 } 272 273 if (index == 0) { 274 return; 275 } 276 277 // Clear is_super_primary setting for all the mime-types with is_super_primary=true 278 // in both raw contact of rawContactId and raw contacts of contactId 279 String superPrimaryUpdateSql = "UPDATE " + Tables.DATA + 280 " SET " + Data.IS_SUPER_PRIMARY + "=0" + 281 " WHERE " + Data.RAW_CONTACT_ID + 282 " IN (" + rawContactIds + ")"; 283 284 mimeTypeCondition.append(')'); 285 superPrimaryUpdateSql += mimeTypeCondition.toString(); 286 db.execSQL(superPrimaryUpdateSql); 287 } 288 buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2, int aggregationType, boolean countOnly)289 private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2, 290 int aggregationType, boolean countOnly) { 291 final String idPairSelection = "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " + 292 AggregationExceptions.RAW_CONTACT_ID2; 293 final String sql = 294 " FROM " + Tables.AGGREGATION_EXCEPTIONS + 295 " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" + 296 rawContactIdSet1 + ")" + 297 " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" + 298 " AND " + AggregationExceptions.TYPE + "=" + aggregationType; 299 return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql : 300 idPairSelection + sql; 301 } 302 303 /** 304 * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of 305 * {@code matchingCandidates} into connected components. This only happens when a given 306 * raw contacts cannot be joined with its best matching contacts directly. 307 * 308 * Two raw contacts are considered connected if they share at least one email address, phone 309 * number or identity. Create new contact for each connected component except the very first 310 * one that doesn't contain rawContactId of {@code rawContactId}. 311 */ reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db, long currentCidForRawContact, long rawContactId, long accountId, long currentContactContentsCount, RawContactMatchingCandidates matchingCandidates)312 private void reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db, 313 long currentCidForRawContact, long rawContactId, long accountId, 314 long currentContactContentsCount, RawContactMatchingCandidates matchingCandidates) { 315 // Find the connected component based on the aggregation exceptions or 316 // identity/email/phone matching for all the raw contacts of [contactId] and the give 317 // raw contact. 318 final Set<Long> allIds = new ArraySet<>(); 319 allIds.add(rawContactId); 320 allIds.addAll(matchingCandidates.getRawContactIdSet()); 321 final Set<Set<Long>> connectedRawContactSets = findConnectedRawContacts(db, allIds); 322 323 final Map<Long, Long> rawContactsToAccounts = matchingCandidates.getRawContactToAccount(); 324 rawContactsToAccounts.put(rawContactId, accountId); 325 ContactAggregatorHelper.mergeComponentsWithDisjointAccounts(connectedRawContactSets, 326 rawContactsToAccounts); 327 breakComponentsByExceptions(db, connectedRawContactSets); 328 329 // Create new contact for each connected component. Use the first reusable contactId if 330 // possible. If no reusable contactId found, create new contact for the connected component. 331 // Update aggregate data for all the contactIds touched by this connected component, 332 for (Set<Long> connectedRawContactIds : connectedRawContactSets) { 333 Long contactId = null; 334 Set<Long> cidsNeedToBeUpdated = new ArraySet<>(); 335 if (connectedRawContactIds.contains(rawContactId)) { 336 // If there is no other raw contacts aggregated with the given raw contact currently 337 // or all the raw contacts in [currentCidForRawContact] are still in the same 338 // connected component, we might as well reuse it. 339 if (currentCidForRawContact != 0 && 340 (currentContactContentsCount == 0) || 341 canBeReused(db, currentCidForRawContact, connectedRawContactIds)) { 342 contactId = currentCidForRawContact; 343 for (Long connectedRawContactId : connectedRawContactIds) { 344 Long cid = matchingCandidates.getContactId(connectedRawContactId); 345 if (cid != null && !cid.equals(contactId)) { 346 cidsNeedToBeUpdated.add(cid); 347 } 348 } 349 } else if (currentCidForRawContact != 0){ 350 cidsNeedToBeUpdated.add(currentCidForRawContact); 351 } 352 } else { 353 boolean foundContactId = false; 354 for (Long connectedRawContactId : connectedRawContactIds) { 355 Long currentContactId = matchingCandidates.getContactId(connectedRawContactId); 356 if (!foundContactId && currentContactId != null && 357 canBeReused(db, currentContactId, connectedRawContactIds)) { 358 contactId = currentContactId; 359 foundContactId = true; 360 } else { 361 cidsNeedToBeUpdated.add(currentContactId); 362 } 363 } 364 } 365 final String connectedRids = TextUtils.join(",", connectedRawContactIds); 366 clearSuperPrimarySetting(db, connectedRids); 367 createContactForRawContacts(db, txContext, connectedRawContactIds, contactId); 368 // re-aggregate 369 if (VERBOSE_LOGGING) { 370 Log.v(TAG, "Aggregating rids=" + connectedRawContactIds); 371 } 372 markAggregated(db, connectedRids); 373 374 for (Long cid : cidsNeedToBeUpdated) { 375 long currentRcCount = 0; 376 if (cid != 0) { 377 mRawContactCountQuery.bindLong(1, cid); 378 mRawContactCountQuery.bindLong(2, 0); 379 currentRcCount = mRawContactCountQuery.simpleQueryForLong(); 380 } 381 382 if (currentRcCount == 0) { 383 // Delete a contact if it doesn't contain anything 384 ContactsTableUtil.deleteContact(db, cid); 385 mAggregatedPresenceDelete.bindLong(1, cid); 386 mAggregatedPresenceDelete.execute(); 387 } else { 388 updateAggregateData(txContext, cid); 389 } 390 } 391 } 392 } 393 394 /** 395 * Check if contactId can be reused as the contact Id for new aggregation of all the 396 * connectedRawContactIds. If connectedRawContactIds set contains all the raw contacts 397 * currently aggregated under contactId, return true; Otherwise, return false. 398 */ canBeReused(SQLiteDatabase db, Long contactId, Set<Long> connectedRawContactIds)399 private boolean canBeReused(SQLiteDatabase db, Long contactId, 400 Set<Long> connectedRawContactIds) { 401 final String sql = "SELECT " + RawContactsColumns.CONCRETE_ID + " FROM " + 402 Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=? AND " + 403 RawContacts.DELETED + "=0"; 404 mSelectionArgs1[0] = String.valueOf(contactId); 405 final Cursor cursor = db.rawQuery(sql, mSelectionArgs1); 406 try { 407 cursor.moveToPosition(-1); 408 while (cursor.moveToNext()) { 409 if (!connectedRawContactIds.contains(cursor.getLong(0))) { 410 return false; 411 } 412 } 413 } finally { 414 cursor.close(); 415 } 416 return true; 417 } 418 419 /** 420 * Separate all the raw_contacts which has "SEPARATE" aggregation exception to another 421 * raw_contacts in the same component. 422 */ breakComponentsByExceptions(SQLiteDatabase db, Set<Set<Long>> connectedRawContacts)423 private void breakComponentsByExceptions(SQLiteDatabase db, 424 Set<Set<Long>> connectedRawContacts) { 425 final Set<Set<Long>> tmpSets = new ArraySet<>(connectedRawContacts); 426 for (Set<Long> component : tmpSets) { 427 final String rawContacts = TextUtils.join(",", component); 428 // If "SEPARATE" exception is found inside an connected component [component], 429 // remove the [component] from [connectedRawContacts], and create new connected 430 // components for all raw contacts of [component] solely based on "JOIN" exceptions 431 // and add them to [connectedRawContacts]. 432 if (isFirstColumnGreaterThanZero(db, buildExceptionMatchingSql(rawContacts, rawContacts, 433 AggregationExceptions.TYPE_KEEP_SEPARATE, /* countOnly =*/true))) { 434 Multimap<Long, Long> joinPairs = HashMultimap.create(); 435 findIdPairs(db, buildExceptionMatchingSql(rawContacts, rawContacts), joinPairs); 436 connectedRawContacts.remove(component); 437 connectedRawContacts.addAll( 438 ContactAggregatorHelper.findConnectedComponents(component, joinPairs)); 439 } 440 } 441 } 442 443 /** 444 * Ensures that automatic aggregation rules are followed after a contact 445 * becomes visible or invisible. Specifically, consider this case: there are 446 * three contacts named Foo. Two of them come from account A1 and one comes 447 * from account A2. The aggregation rules say that in this case none of the 448 * three Foo's should be aggregated: two of them are in the same account, so 449 * they don't get aggregated; the third has two affinities, so it does not 450 * join either of them. 451 * <p> 452 * Consider what happens if one of the "Foo"s from account A1 becomes 453 * invisible. Nothing stands in the way of aggregating the other two 454 * anymore, so they should get joined. 455 * <p> 456 * What if the invisible "Foo" becomes visible after that? We should split the 457 * aggregate between the other two. 458 */ updateAggregationAfterVisibilityChange(long contactId)459 public void updateAggregationAfterVisibilityChange(long contactId) { 460 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 461 boolean visible = mDbHelper.isContactInDefaultDirectory(db, contactId); 462 if (visible) { 463 markContactForAggregation(db, contactId); 464 } else { 465 // Find all contacts that _could be_ aggregated with this one and 466 // rerun aggregation for all of them 467 mSelectionArgs1[0] = String.valueOf(contactId); 468 Cursor cursor = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS, 469 RawContactIdQuery.SELECTION, mSelectionArgs1, null, null, null); 470 try { 471 while (cursor.moveToNext()) { 472 long rawContactId = cursor.getLong(RawContactIdQuery.RAW_CONTACT_ID); 473 mMatcher.clear(); 474 475 updateMatchScoresBasedOnIdentityMatch(db, rawContactId, mMatcher); 476 updateMatchScoresBasedOnNameMatches(db, rawContactId, mMatcher); 477 List<MatchScore> bestMatches = 478 mMatcher.pickBestMatches(SCORE_THRESHOLD_PRIMARY); 479 for (MatchScore matchScore : bestMatches) { 480 markContactForAggregation(db, matchScore.getContactId()); 481 } 482 483 mMatcher.clear(); 484 updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher); 485 updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher); 486 bestMatches = 487 mMatcher.pickBestMatches(SCORE_THRESHOLD_SECONDARY); 488 for (MatchScore matchScore : bestMatches) { 489 markContactForAggregation(db, matchScore.getContactId()); 490 } 491 } 492 } finally { 493 cursor.close(); 494 } 495 } 496 } 497 498 /** 499 * Computes match scores based on exceptions entered by the user: always match and never match. 500 */ updateMatchScoresBasedOnExceptions(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)501 private void updateMatchScoresBasedOnExceptions(SQLiteDatabase db, long rawContactId, 502 RawContactMatcher matcher) { 503 if (!mAggregationExceptionIdsValid) { 504 prefetchAggregationExceptionIds(db); 505 } 506 507 // If there are no aggregation exceptions involving this raw contact, there is no need to 508 // run a query and we can just return -1, which stands for "nothing found" 509 if (!mAggregationExceptionIds.contains(rawContactId)) { 510 return; 511 } 512 513 final Cursor c = db.query(AggregateExceptionQuery.TABLE, 514 AggregateExceptionQuery.COLUMNS, 515 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId 516 + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId, 517 null, null, null, null); 518 519 try { 520 while (c.moveToNext()) { 521 int type = c.getInt(AggregateExceptionQuery.TYPE); 522 long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1); 523 long contactId = -1; 524 long rId = -1; 525 long accountId = -1; 526 if (rawContactId == rawContactId1) { 527 if (!c.isNull(AggregateExceptionQuery.RAW_CONTACT_ID2)) { 528 rId = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID2); 529 contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2); 530 accountId = c.getLong(AggregateExceptionQuery.ACCOUNT_ID2); 531 } 532 } else { 533 if (!c.isNull(AggregateExceptionQuery.RAW_CONTACT_ID1)) { 534 rId = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1); 535 contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1); 536 accountId = c.getLong(AggregateExceptionQuery.ACCOUNT_ID1); 537 } 538 } 539 if (rId != -1) { 540 if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) { 541 matcher.keepIn(rId, contactId, accountId); 542 } else { 543 matcher.keepOut(rId, contactId, accountId); 544 } 545 } 546 } 547 } finally { 548 c.close(); 549 } 550 } 551 552 /** 553 * Finds contacts with exact identity matches to the the specified raw contact. 554 */ updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)555 private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId, 556 RawContactMatcher matcher) { 557 mSelectionArgs2[0] = String.valueOf(rawContactId); 558 mSelectionArgs2[1] = String.valueOf(mMimeTypeIdIdentity); 559 Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS, 560 IdentityLookupMatchQuery.SELECTION, 561 mSelectionArgs2, RawContacts.CONTACT_ID, null, null); 562 try { 563 while (c.moveToNext()) { 564 final long rId = c.getLong(IdentityLookupMatchQuery.RAW_CONTACT_ID); 565 if (rId == rawContactId) { 566 continue; 567 } 568 final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID); 569 final long accountId = c.getLong(IdentityLookupMatchQuery.ACCOUNT_ID); 570 matcher.matchIdentity(rId, contactId, accountId); 571 } 572 } finally { 573 c.close(); 574 } 575 } 576 577 /** 578 * Finds contacts with names matching the name of the specified raw contact. 579 */ updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)580 private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId, 581 RawContactMatcher matcher) { 582 mSelectionArgs1[0] = String.valueOf(rawContactId); 583 Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS, 584 NameLookupMatchQuery.SELECTION, 585 mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING); 586 try { 587 while (c.moveToNext()) { 588 long rId = c.getLong(NameLookupMatchQuery.RAW_CONTACT_ID); 589 if (rId == rawContactId) { 590 continue; 591 } 592 long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID); 593 long accountId = c.getLong(NameLookupMatchQuery.ACCOUNT_ID); 594 String name = c.getString(NameLookupMatchQuery.NAME); 595 int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A); 596 int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B); 597 matcher.matchName(rId, contactId, accountId, nameTypeA, name, 598 nameTypeB, name, RawContactMatcher.MATCHING_ALGORITHM_EXACT); 599 if (nameTypeA == NameLookupType.NICKNAME && 600 nameTypeB == NameLookupType.NICKNAME) { 601 matcher.updateScoreWithNicknameMatch(rId, contactId, accountId); 602 } 603 } 604 } finally { 605 c.close(); 606 } 607 } 608 updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)609 private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, 610 RawContactMatcher matcher) { 611 mSelectionArgs2[0] = String.valueOf(rawContactId); 612 mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail); 613 Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS, 614 EmailLookupQuery.SELECTION, 615 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING); 616 try { 617 while (c.moveToNext()) { 618 long rId = c.getLong(EmailLookupQuery.RAW_CONTACT_ID); 619 if (rId == rawContactId) { 620 continue; 621 } 622 long contactId = c.getLong(EmailLookupQuery.CONTACT_ID); 623 long accountId = c.getLong(EmailLookupQuery.ACCOUNT_ID); 624 matcher.updateScoreWithEmailMatch(rId, contactId, accountId); 625 } 626 } finally { 627 c.close(); 628 } 629 } 630 631 /** 632 * Finds contacts with names matching the specified name. 633 */ updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query, MatchCandidateList candidates, RawContactMatcher matcher)634 private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query, 635 MatchCandidateList candidates, RawContactMatcher matcher) { 636 candidates.clear(); 637 NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder( 638 mNameSplitter, candidates); 639 builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED); 640 if (builder.isEmpty()) { 641 return; 642 } 643 644 Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE, 645 NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null, 646 null, PRIMARY_HIT_LIMIT_STRING); 647 try { 648 while (c.moveToNext()) { 649 long rId = c.getLong(NameLookupMatchQueryWithParameter.RAW_CONTACT_ID); 650 long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID); 651 long accountId = c.getLong(NameLookupMatchQueryWithParameter.ACCOUNT_ID); 652 String name = c.getString(NameLookupMatchQueryWithParameter.NAME); 653 int nameTypeA = builder.getLookupType(name); 654 int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE); 655 matcher.matchName(rId, contactId, accountId, nameTypeA, name, nameTypeB, name, 656 RawContactMatcher.MATCHING_ALGORITHM_EXACT); 657 if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) { 658 matcher.updateScoreWithNicknameMatch(rId, contactId, accountId); 659 } 660 } 661 } finally { 662 c.close(); 663 } 664 } 665 updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)666 private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId, 667 RawContactMatcher matcher) { 668 Cursor c; 669 String useStrictPhoneNumberComparison = 670 mDbHelper.getUseStrictPhoneNumberComparisonParameter(); 671 672 if (useStrictPhoneNumberComparison.equals("1")) { 673 mSelectionArgs2[0] = String.valueOf(rawContactId); 674 mSelectionArgs2[1] = useStrictPhoneNumberComparison; 675 c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS, 676 PhoneLookupQuery.SELECTION, 677 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING); 678 } else { 679 mSelectionArgs3[0] = String.valueOf(rawContactId); 680 mSelectionArgs3[1] = useStrictPhoneNumberComparison; 681 mSelectionArgs3[2] = mDbHelper.getMinMatchParameter(); 682 c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS, 683 PhoneLookupQuery.SELECTION_MIN_MATCH, 684 mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING); 685 } 686 687 try { 688 while (c.moveToNext()) { 689 long rId = c.getLong(PhoneLookupQuery.RAW_CONTACT_ID); 690 if (rId == rawContactId) { 691 continue; 692 } 693 long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID); 694 long accountId = c.getLong(PhoneLookupQuery.ACCOUNT_ID); 695 matcher.updateScoreWithPhoneNumberMatch(rId, contactId, accountId); 696 } 697 } finally { 698 c.close(); 699 } 700 } 701 702 /** 703 * Loads name lookup rows for approximate name matching and updates match scores based on that 704 * data. 705 */ lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates, RawContactMatcher matcher)706 private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates, 707 RawContactMatcher matcher) { 708 ArraySet<String> firstLetters = new ArraySet<>(); 709 for (int i = 0; i < candidates.mCount; i++) { 710 final NameMatchCandidate candidate = candidates.mList.get(i); 711 if (candidate.mName.length() >= 2) { 712 String firstLetter = candidate.mName.substring(0, 2); 713 if (!firstLetters.contains(firstLetter)) { 714 firstLetters.add(firstLetter); 715 final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '" 716 + firstLetter + "*') AND " 717 + "(" + NameLookupColumns.NAME_TYPE + " IN(" 718 + NameLookupType.NAME_COLLATION_KEY + "," 719 + NameLookupType.EMAIL_BASED_NICKNAME + "," 720 + NameLookupType.NICKNAME + ")) AND " 721 + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 722 matchAllCandidates(db, selection, candidates, matcher, 723 RawContactMatcher.MATCHING_ALGORITHM_APPROXIMATE, 724 String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT)); 725 } 726 } 727 } 728 } 729 730 private interface ContactNameLookupQuery { 731 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 732 733 String[] COLUMNS = new String[] { 734 RawContacts._ID, 735 RawContacts.CONTACT_ID, 736 RawContactsColumns.ACCOUNT_ID, 737 NameLookupColumns.NORMALIZED_NAME, 738 NameLookupColumns.NAME_TYPE 739 }; 740 741 int RAW_CONTACT_ID = 0; 742 int CONTACT_ID = 1; 743 int ACCOUNT_ID = 2; 744 int NORMALIZED_NAME = 3; 745 int NAME_TYPE = 4; 746 } 747 748 /** 749 * Loads all candidate rows from the name lookup table and updates match scores based 750 * on that data. 751 */ matchAllCandidates(SQLiteDatabase db, String selection, MatchCandidateList candidates, RawContactMatcher matcher, int algorithm, String limit)752 private void matchAllCandidates(SQLiteDatabase db, String selection, 753 MatchCandidateList candidates, RawContactMatcher matcher, int algorithm, String limit) { 754 final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS, 755 selection, null, null, null, null, limit); 756 757 try { 758 while (c.moveToNext()) { 759 Long rawContactId = c.getLong(ContactNameLookupQuery.RAW_CONTACT_ID); 760 Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID); 761 Long accountId = c.getLong(ContactNameLookupQuery.ACCOUNT_ID); 762 String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME); 763 int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE); 764 765 // Note the N^2 complexity of the following fragment. This is not a huge concern 766 // since the number of candidates is very small and in general secondary hits 767 // in the absence of primary hits are rare. 768 for (int i = 0; i < candidates.mCount; i++) { 769 NameMatchCandidate candidate = candidates.mList.get(i); 770 matcher.matchName(rawContactId, contactId, accountId, candidate.mLookupType, 771 candidate.mName, nameType, name, algorithm); 772 } 773 } 774 } finally { 775 c.close(); 776 } 777 } 778 779 private interface PhotoFileQuery { 780 final String[] COLUMNS = new String[] { 781 PhotoFiles.HEIGHT, 782 PhotoFiles.WIDTH, 783 PhotoFiles.FILESIZE 784 }; 785 786 int HEIGHT = 0; 787 int WIDTH = 1; 788 int FILESIZE = 2; 789 } 790 791 private class PhotoEntry implements Comparable<PhotoEntry> { 792 // Pixel count (width * height) for the image. 793 final int pixelCount; 794 795 // File size (in bytes) of the image. Not populated if the image is a thumbnail. 796 final int fileSize; 797 PhotoEntry(int pixelCount, int fileSize)798 private PhotoEntry(int pixelCount, int fileSize) { 799 this.pixelCount = pixelCount; 800 this.fileSize = fileSize; 801 } 802 803 @Override compareTo(PhotoEntry pe)804 public int compareTo(PhotoEntry pe) { 805 if (pe == null) { 806 return -1; 807 } 808 if (pixelCount == pe.pixelCount) { 809 return pe.fileSize - fileSize; 810 } else { 811 return pe.pixelCount - pixelCount; 812 } 813 } 814 } 815 816 /** 817 * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the 818 * descending order of match score. 819 * @param parameters 820 */ findMatchingContacts(final SQLiteDatabase db, long contactId, ArrayList<AggregationSuggestionParameter> parameters)821 protected List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId, 822 ArrayList<AggregationSuggestionParameter> parameters) { 823 824 MatchCandidateList candidates = new MatchCandidateList(); 825 RawContactMatcher matcher = new RawContactMatcher(); 826 827 if (parameters == null || parameters.size() == 0) { 828 final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS, 829 RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null); 830 try { 831 while (c.moveToNext()) { 832 long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID); 833 long accountId = c.getLong(RawContactIdQuery.ACCOUNT_ID); 834 // Don't aggregate a contact with its own raw contacts. 835 matcher.keepOut(rawContactId, contactId, accountId); 836 updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates, 837 matcher); 838 } 839 } finally { 840 c.close(); 841 } 842 } else { 843 updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates, 844 matcher, parameters); 845 } 846 847 return matcher.pickBestMatches(SCORE_THRESHOLD_SUGGEST); 848 } 849 850 /** 851 * Computes suggestion scores for contacts that have matching data rows. 852 * Aggregation suggestion doesn't consider aggregation exceptions, but is purely based on the 853 * raw contacts information. 854 */ updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher)855 private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, 856 long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) { 857 858 updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher); 859 updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher); 860 updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher); 861 updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher); 862 loadNameMatchCandidates(db, rawContactId, candidates, false); 863 lookupApproximateNameMatches(db, candidates, matcher); 864 } 865 866 /** 867 * Computes scores for contacts that have matching data rows. 868 */ updateMatchScores(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher)869 private void updateMatchScores(SQLiteDatabase db, long rawContactId, 870 MatchCandidateList candidates, RawContactMatcher matcher) { 871 //update primary score 872 updateMatchScoresBasedOnExceptions(db, rawContactId, matcher); 873 updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher); 874 // update scores only if the raw contact doesn't have structured name 875 if (rawContactWithoutName(db, rawContactId)) { 876 updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher); 877 updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher); 878 updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher); 879 final List<Long> secondaryRawContactIds = matcher.prepareSecondaryMatchCandidates(); 880 if (secondaryRawContactIds != null 881 && secondaryRawContactIds.size() <= SECONDARY_HIT_LIMIT) { 882 updateScoreForCandidatesWithoutName(db, secondaryRawContactIds, matcher); 883 } 884 } 885 } 886 updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, MatchCandidateList candidates, RawContactMatcher matcher, ArrayList<AggregationSuggestionParameter> parameters)887 private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, 888 MatchCandidateList candidates, RawContactMatcher matcher, 889 ArrayList<AggregationSuggestionParameter> parameters) { 890 for (AggregationSuggestionParameter parameter : parameters) { 891 if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) { 892 updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher); 893 } 894 895 // TODO: add support for other parameter kinds 896 } 897 } 898 rawContactWithoutName(SQLiteDatabase db, long rawContactId)899 private boolean rawContactWithoutName(SQLiteDatabase db, long rawContactId) { 900 String selection = RawContacts._ID + " =" + rawContactId; 901 final Cursor c = db.query(NullNameRawContactsIdsQuery.TABLE, 902 NullNameRawContactsIdsQuery.COLUMNS, selection, null, null, null, null); 903 904 try { 905 if (c.moveToFirst()) { 906 return TextUtils.isEmpty(c.getString(NullNameRawContactsIdsQuery.NAME)); 907 } 908 } finally { 909 c.close(); 910 } 911 return false; 912 } 913 914 /** 915 * Update scores for matches with secondary data matching but no structured name. 916 */ updateScoreForCandidatesWithoutName(SQLiteDatabase db, List<Long> secondaryRawContactIds, RawContactMatcher matcher)917 private void updateScoreForCandidatesWithoutName(SQLiteDatabase db, 918 List<Long> secondaryRawContactIds, RawContactMatcher matcher) { 919 920 mSb.setLength(0); 921 922 mSb.append(RawContacts._ID).append(" IN ("); 923 for (int i = 0; i < secondaryRawContactIds.size(); i++) { 924 if (i != 0) { 925 mSb.append(","); 926 } 927 mSb.append(secondaryRawContactIds.get(i)); 928 } 929 mSb.append( ")"); 930 final Cursor c = db.query(NullNameRawContactsIdsQuery.TABLE, 931 NullNameRawContactsIdsQuery.COLUMNS, mSb.toString(), null, null, null, null); 932 933 try { 934 while (c.moveToNext()) { 935 Long rId = c.getLong(NullNameRawContactsIdsQuery.RAW_CONTACT_ID); 936 Long contactId = c.getLong(NullNameRawContactsIdsQuery.CONTACT_ID); 937 Long accountId = c.getLong(NullNameRawContactsIdsQuery.ACCOUNT_ID); 938 String name = c.getString(NullNameRawContactsIdsQuery.NAME); 939 if (TextUtils.isEmpty(name)) { 940 matcher.matchNoName(rId, contactId, accountId); 941 } 942 } 943 } finally { 944 c.close(); 945 } 946 } 947 948 protected interface IdentityLookupMatchQuery { 949 final String TABLE = Tables.DATA + " dataA" 950 + " JOIN " + Tables.DATA + " dataB" + 951 " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE + 952 " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")" 953 + " JOIN " + Tables.RAW_CONTACTS + 954 " ON (dataB." + Data.RAW_CONTACT_ID + " = " 955 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 956 957 final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1" 958 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2" 959 + " AND dataA." + Identity.NAMESPACE + " NOT NULL" 960 + " AND dataA." + Identity.IDENTITY + " NOT NULL" 961 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2" 962 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 963 964 final String[] COLUMNS = new String[] { 965 RawContactsColumns.CONCRETE_ID, RawContacts.CONTACT_ID, 966 RawContactsColumns.ACCOUNT_ID 967 }; 968 969 int RAW_CONTACT_ID = 0; 970 int CONTACT_ID = 1; 971 int ACCOUNT_ID = 2; 972 } 973 974 protected interface NameLookupMatchQuery { 975 String TABLE = Tables.NAME_LOOKUP + " nameA" 976 + " JOIN " + Tables.NAME_LOOKUP + " nameB" + 977 " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "=" 978 + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")" 979 + " JOIN " + Tables.RAW_CONTACTS + 980 " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = " 981 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 982 983 String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?" 984 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 985 986 String[] COLUMNS = new String[] { 987 RawContacts._ID, 988 RawContacts.CONTACT_ID, 989 RawContactsColumns.ACCOUNT_ID, 990 "nameA." + NameLookupColumns.NORMALIZED_NAME, 991 "nameA." + NameLookupColumns.NAME_TYPE, 992 "nameB." + NameLookupColumns.NAME_TYPE, 993 }; 994 995 int RAW_CONTACT_ID = 0; 996 int CONTACT_ID = 1; 997 int ACCOUNT_ID = 2; 998 int NAME = 3; 999 int NAME_TYPE_A = 4; 1000 int NAME_TYPE_B = 5; 1001 } 1002 1003 protected interface EmailLookupQuery { 1004 String TABLE = Tables.DATA + " dataA" 1005 + " JOIN " + Tables.DATA + " dataB" + 1006 " ON dataA." + Email.DATA + "= dataB." + Email.DATA 1007 + " JOIN " + Tables.RAW_CONTACTS + 1008 " ON (dataB." + Data.RAW_CONTACT_ID + " = " 1009 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 1010 1011 String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1" 1012 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2" 1013 + " AND dataA." + Email.DATA + " NOT NULL" 1014 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2" 1015 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 1016 1017 String[] COLUMNS = new String[] { 1018 Tables.RAW_CONTACTS + "." + RawContacts._ID, 1019 RawContacts.CONTACT_ID, 1020 RawContactsColumns.ACCOUNT_ID 1021 }; 1022 1023 int RAW_CONTACT_ID = 0; 1024 int CONTACT_ID = 1; 1025 int ACCOUNT_ID = 2; 1026 } 1027 1028 protected interface PhoneLookupQuery { 1029 String TABLE = Tables.PHONE_LOOKUP + " phoneA" 1030 + " JOIN " + Tables.DATA + " dataA" 1031 + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")" 1032 + " JOIN " + Tables.PHONE_LOOKUP + " phoneB" 1033 + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "=" 1034 + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")" 1035 + " JOIN " + Tables.DATA + " dataB" 1036 + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")" 1037 + " JOIN " + Tables.RAW_CONTACTS 1038 + " ON (dataB." + Data.RAW_CONTACT_ID + " = " 1039 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; 1040 1041 String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" 1042 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", " 1043 + "dataB." + Phone.NUMBER + ",?)" 1044 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 1045 1046 String SELECTION_MIN_MATCH = "dataA." + Data.RAW_CONTACT_ID + "=?" 1047 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", " 1048 + "dataB." + Phone.NUMBER + ",?,?)" 1049 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 1050 1051 String[] COLUMNS = new String[] { 1052 Tables.RAW_CONTACTS + "." + RawContacts._ID, 1053 RawContacts.CONTACT_ID, 1054 RawContactsColumns.ACCOUNT_ID 1055 }; 1056 1057 int RAW_CONTACT_ID = 0; 1058 int CONTACT_ID = 1; 1059 int ACCOUNT_ID = 2; 1060 } 1061 1062 protected interface NullNameRawContactsIdsQuery { 1063 final String TABLE = Tables.RAW_CONTACTS + " LEFT OUTER JOIN " + Tables.NAME_LOOKUP 1064 + " ON "+ RawContacts._ID + " = " + NameLookupColumns.RAW_CONTACT_ID 1065 + " AND " + NameLookupColumns.NAME_TYPE + " = " + NameLookupType.NAME_EXACT; 1066 1067 final String[] COLUMNS = new String[] { 1068 RawContacts._ID, RawContacts.CONTACT_ID, RawContactsColumns.ACCOUNT_ID, 1069 NameLookupColumns.NORMALIZED_NAME}; 1070 1071 int RAW_CONTACT_ID = 0; 1072 int CONTACT_ID = 1; 1073 int ACCOUNT_ID = 2; 1074 int NAME = 3; 1075 } 1076 } 1077