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