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