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