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