Home | History | Annotate | Download | only in aggregation
      1 /*
      2  * Copyright (C) 2009 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 android.database.Cursor;
     20 import android.database.DatabaseUtils;
     21 import android.database.sqlite.SQLiteDatabase;
     22 import android.database.sqlite.SQLiteQueryBuilder;
     23 import android.database.sqlite.SQLiteStatement;
     24 import android.net.Uri;
     25 import android.provider.ContactsContract.AggregationExceptions;
     26 import android.provider.ContactsContract.CommonDataKinds.Email;
     27 import android.provider.ContactsContract.CommonDataKinds.Identity;
     28 import android.provider.ContactsContract.CommonDataKinds.Phone;
     29 import android.provider.ContactsContract.CommonDataKinds.Photo;
     30 import android.provider.ContactsContract.Contacts;
     31 import android.provider.ContactsContract.Contacts.AggregationSuggestions;
     32 import android.provider.ContactsContract.Data;
     33 import android.provider.ContactsContract.DisplayNameSources;
     34 import android.provider.ContactsContract.FullNameStyle;
     35 import android.provider.ContactsContract.PhotoFiles;
     36 import android.provider.ContactsContract.PinnedPositions;
     37 import android.provider.ContactsContract.RawContacts;
     38 import android.provider.ContactsContract.StatusUpdates;
     39 import android.text.TextUtils;
     40 import android.util.EventLog;
     41 import android.util.Log;
     42 
     43 import com.android.providers.contacts.ContactLookupKey;
     44 import com.android.providers.contacts.ContactsDatabaseHelper;
     45 import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
     46 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
     47 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
     48 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
     49 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
     50 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
     51 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
     52 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
     53 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
     54 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
     55 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     56 import com.android.providers.contacts.ContactsDatabaseHelper.Views;
     57 import com.android.providers.contacts.ContactsProvider2;
     58 import com.android.providers.contacts.NameLookupBuilder;
     59 import com.android.providers.contacts.NameNormalizer;
     60 import com.android.providers.contacts.NameSplitter;
     61 import com.android.providers.contacts.PhotoPriorityResolver;
     62 import com.android.providers.contacts.ReorderingCursorWrapper;
     63 import com.android.providers.contacts.TransactionContext;
     64 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
     65 import com.android.providers.contacts.aggregation.util.ContactMatcher;
     66 import com.android.providers.contacts.aggregation.util.ContactMatcher.MatchScore;
     67 import com.android.providers.contacts.database.ContactsTableUtil;
     68 import com.android.providers.contacts.util.Clock;
     69 
     70 import com.google.android.collect.Maps;
     71 
     72 import java.util.ArrayList;
     73 import java.util.Collections;
     74 import java.util.HashMap;
     75 import java.util.HashSet;
     76 import java.util.Iterator;
     77 import java.util.List;
     78 import java.util.Locale;
     79 
     80 /**
     81  * ContactAggregator deals with aggregating contact information coming from different sources.
     82  * Two John Doe contacts from two disjoint sources are presumed to be the same
     83  * person unless the user declares otherwise.
     84  */
     85 public class ContactAggregator {
     86 
     87     private static final String TAG = "ContactAggregator";
     88 
     89     private static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG);
     90     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
     91 
     92     private 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     private 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     public 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     private static final int PRIMARY_HIT_LIMIT = 15;
    127     private 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     private static final int SECONDARY_HIT_LIMIT = 20;
    132     private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
    133 
    134     // If we encounter more than this many contacts with matching name during aggregation
    135     // suggestion lookup, ignore the remaining results.
    136     private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100;
    137 
    138     private final ContactsProvider2 mContactsProvider;
    139     private final ContactsDatabaseHelper mDbHelper;
    140     private PhotoPriorityResolver mPhotoPriorityResolver;
    141     private final NameSplitter mNameSplitter;
    142     private final CommonNicknameCache mCommonNicknameCache;
    143 
    144     private boolean mEnabled = true;
    145 
    146     /** Precompiled sql statement for setting an aggregated presence */
    147     private SQLiteStatement mAggregatedPresenceReplace;
    148     private SQLiteStatement mPresenceContactIdUpdate;
    149     private SQLiteStatement mRawContactCountQuery;
    150     private SQLiteStatement mAggregatedPresenceDelete;
    151     private SQLiteStatement mMarkForAggregation;
    152     private SQLiteStatement mPhotoIdUpdate;
    153     private SQLiteStatement mDisplayNameUpdate;
    154     private SQLiteStatement mLookupKeyUpdate;
    155     private SQLiteStatement mStarredUpdate;
    156     private SQLiteStatement mPinnedUpdate;
    157     private SQLiteStatement mContactIdAndMarkAggregatedUpdate;
    158     private SQLiteStatement mContactIdUpdate;
    159     private SQLiteStatement mMarkAggregatedUpdate;
    160     private SQLiteStatement mContactUpdate;
    161     private SQLiteStatement mContactInsert;
    162     private SQLiteStatement mResetPinnedForRawContact;
    163 
    164     private HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap();
    165 
    166     private String[] mSelectionArgs1 = new String[1];
    167     private String[] mSelectionArgs2 = new String[2];
    168     private String[] mSelectionArgs3 = new String[3];
    169     private long mMimeTypeIdIdentity;
    170     private long mMimeTypeIdEmail;
    171     private long mMimeTypeIdPhoto;
    172     private long mMimeTypeIdPhone;
    173     private String mRawContactsQueryByRawContactId;
    174     private String mRawContactsQueryByContactId;
    175     private StringBuilder mSb = new StringBuilder();
    176     private MatchCandidateList mCandidates = new MatchCandidateList();
    177     private ContactMatcher mMatcher = new ContactMatcher();
    178     private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
    179 
    180     /**
    181      * Parameter for the suggestion lookup query.
    182      */
    183     public static final class AggregationSuggestionParameter {
    184         public final String kind;
    185         public final String value;
    186 
    187         public AggregationSuggestionParameter(String kind, String value) {
    188             this.kind = kind;
    189             this.value = value;
    190         }
    191     }
    192 
    193     /**
    194      * Captures a potential match for a given name. The matching algorithm
    195      * constructs a bunch of NameMatchCandidate objects for various potential matches
    196      * and then executes the search in bulk.
    197      */
    198     private static class NameMatchCandidate {
    199         String mName;
    200         int mLookupType;
    201 
    202         public NameMatchCandidate(String name, int nameLookupType) {
    203             mName = name;
    204             mLookupType = nameLookupType;
    205         }
    206     }
    207 
    208     /**
    209      * A list of {@link NameMatchCandidate} that keeps its elements even when the list is
    210      * truncated. This is done for optimization purposes to avoid excessive object allocation.
    211      */
    212     private static class MatchCandidateList {
    213         private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>();
    214         private int mCount;
    215 
    216         /**
    217          * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists.
    218          */
    219         public void add(String name, int nameLookupType) {
    220             if (mCount >= mList.size()) {
    221                 mList.add(new NameMatchCandidate(name, nameLookupType));
    222             } else {
    223                 NameMatchCandidate candidate = mList.get(mCount);
    224                 candidate.mName = name;
    225                 candidate.mLookupType = nameLookupType;
    226             }
    227             mCount++;
    228         }
    229 
    230         public void clear() {
    231             mCount = 0;
    232         }
    233 
    234         public boolean isEmpty() {
    235             return mCount == 0;
    236         }
    237     }
    238 
    239     /**
    240      * A convenience class used in the algorithm that figures out which of available
    241      * display names to use for an aggregate contact.
    242      */
    243     private static class DisplayNameCandidate {
    244         long rawContactId;
    245         String displayName;
    246         int displayNameSource;
    247         boolean verified;
    248         boolean writableAccount;
    249 
    250         public DisplayNameCandidate() {
    251             clear();
    252         }
    253 
    254         public void clear() {
    255             rawContactId = -1;
    256             displayName = null;
    257             displayNameSource = DisplayNameSources.UNDEFINED;
    258             verified = false;
    259             writableAccount = false;
    260         }
    261     }
    262 
    263     /**
    264      * Constructor.
    265      */
    266     public ContactAggregator(ContactsProvider2 contactsProvider,
    267             ContactsDatabaseHelper contactsDatabaseHelper,
    268             PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
    269             CommonNicknameCache commonNicknameCache) {
    270         mContactsProvider = contactsProvider;
    271         mDbHelper = contactsDatabaseHelper;
    272         mPhotoPriorityResolver = photoPriorityResolver;
    273         mNameSplitter = nameSplitter;
    274         mCommonNicknameCache = commonNicknameCache;
    275 
    276         SQLiteDatabase db = mDbHelper.getReadableDatabase();
    277 
    278         // Since we have no way of determining which custom status was set last,
    279         // we'll just pick one randomly.  We are using MAX as an approximation of randomness
    280         final String replaceAggregatePresenceSql =
    281                 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
    282                 + AggregatedPresenceColumns.CONTACT_ID + ", "
    283                 + StatusUpdates.PRESENCE + ", "
    284                 + StatusUpdates.CHAT_CAPABILITY + ")"
    285                 + " SELECT " + PresenceColumns.CONTACT_ID + ","
    286                 + StatusUpdates.PRESENCE + ","
    287                 + StatusUpdates.CHAT_CAPABILITY
    288                 + " FROM " + Tables.PRESENCE
    289                 + " WHERE "
    290                 + " (" + StatusUpdates.PRESENCE
    291                 +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
    292                 + " = (SELECT "
    293                 + "MAX (" + StatusUpdates.PRESENCE
    294                 +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
    295                 + " FROM " + Tables.PRESENCE
    296                 + " WHERE " + PresenceColumns.CONTACT_ID
    297                 + "=?)"
    298                 + " AND " + PresenceColumns.CONTACT_ID
    299                 + "=?;";
    300         mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql);
    301 
    302         mRawContactCountQuery = db.compileStatement(
    303                 "SELECT COUNT(" + RawContacts._ID + ")" +
    304                 " FROM " + Tables.RAW_CONTACTS +
    305                 " WHERE " + RawContacts.CONTACT_ID + "=?"
    306                         + " AND " + RawContacts._ID + "<>?");
    307 
    308         mAggregatedPresenceDelete = db.compileStatement(
    309                 "DELETE FROM " + Tables.AGGREGATED_PRESENCE +
    310                 " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?");
    311 
    312         mMarkForAggregation = db.compileStatement(
    313                 "UPDATE " + Tables.RAW_CONTACTS +
    314                 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
    315                 " WHERE " + RawContacts._ID + "=?"
    316                         + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0");
    317 
    318         mPhotoIdUpdate = db.compileStatement(
    319                 "UPDATE " + Tables.CONTACTS +
    320                 " SET " + Contacts.PHOTO_ID + "=?," + Contacts.PHOTO_FILE_ID + "=? " +
    321                 " WHERE " + Contacts._ID + "=?");
    322 
    323         mDisplayNameUpdate = db.compileStatement(
    324                 "UPDATE " + Tables.CONTACTS +
    325                 " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
    326                 " WHERE " + Contacts._ID + "=?");
    327 
    328         mLookupKeyUpdate = db.compileStatement(
    329                 "UPDATE " + Tables.CONTACTS +
    330                 " SET " + Contacts.LOOKUP_KEY + "=? " +
    331                 " WHERE " + Contacts._ID + "=?");
    332 
    333         mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
    334                 + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED
    335                 + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE "
    336                 + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND "
    337                 + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?");
    338 
    339         mPinnedUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
    340                 + Contacts.PINNED + " = IFNULL((SELECT MIN(" + RawContacts.PINNED + ") FROM "
    341                 + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
    342                 + ContactsColumns.CONCRETE_ID + " AND " + RawContacts.PINNED + ">"
    343                 + PinnedPositions.UNPINNED + ")," + PinnedPositions.UNPINNED + ") "
    344                 + "WHERE " + Contacts._ID + "=?");
    345 
    346         mContactIdAndMarkAggregatedUpdate = db.compileStatement(
    347                 "UPDATE " + Tables.RAW_CONTACTS +
    348                 " SET " + RawContacts.CONTACT_ID + "=?, "
    349                         + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
    350                 " WHERE " + RawContacts._ID + "=?");
    351 
    352         mContactIdUpdate = db.compileStatement(
    353                 "UPDATE " + Tables.RAW_CONTACTS +
    354                 " SET " + RawContacts.CONTACT_ID + "=?" +
    355                 " WHERE " + RawContacts._ID + "=?");
    356 
    357         mMarkAggregatedUpdate = db.compileStatement(
    358                 "UPDATE " + Tables.RAW_CONTACTS +
    359                 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
    360                 " WHERE " + RawContacts._ID + "=?");
    361 
    362         mPresenceContactIdUpdate = db.compileStatement(
    363                 "UPDATE " + Tables.PRESENCE +
    364                 " SET " + PresenceColumns.CONTACT_ID + "=?" +
    365                 " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
    366 
    367         mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
    368         mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
    369 
    370         mResetPinnedForRawContact = db.compileStatement(
    371                 "UPDATE " + Tables.RAW_CONTACTS +
    372                 " SET " + RawContacts.PINNED + "=" + PinnedPositions.UNPINNED +
    373                 " WHERE " + RawContacts._ID + "=?");
    374 
    375         mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
    376         mMimeTypeIdIdentity = mDbHelper.getMimeTypeId(Identity.CONTENT_ITEM_TYPE);
    377         mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
    378         mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
    379 
    380         // Query used to retrieve data from raw contacts to populate the corresponding aggregate
    381         mRawContactsQueryByRawContactId = String.format(Locale.US,
    382                 RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
    383                 mMimeTypeIdPhoto, mMimeTypeIdPhone);
    384 
    385         mRawContactsQueryByContactId = String.format(Locale.US,
    386                 RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
    387                 mMimeTypeIdPhoto, mMimeTypeIdPhone);
    388     }
    389 
    390     public void setEnabled(boolean enabled) {
    391         mEnabled = enabled;
    392     }
    393 
    394     public boolean isEnabled() {
    395         return mEnabled;
    396     }
    397 
    398     private interface AggregationQuery {
    399         String SQL =
    400                 "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
    401                         ", " + RawContactsColumns.ACCOUNT_ID +
    402                 " FROM " + Tables.RAW_CONTACTS +
    403                 " WHERE " + RawContacts._ID + " IN(";
    404 
    405         int _ID = 0;
    406         int CONTACT_ID = 1;
    407         int ACCOUNT_ID = 2;
    408     }
    409 
    410     /**
    411      * Aggregate all raw contacts that were marked for aggregation in the current transaction.
    412      * Call just before committing the transaction.
    413      */
    414     public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) {
    415         final int markedCount = mRawContactsMarkedForAggregation.size();
    416         if (markedCount == 0) {
    417             return;
    418         }
    419 
    420         final long start = System.currentTimeMillis();
    421         if (DEBUG_LOGGING) {
    422             Log.d(TAG, "aggregateInTransaction for " + markedCount + " contacts");
    423         }
    424 
    425         EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -markedCount);
    426 
    427         int index = 0;
    428 
    429         // We don't use the cached string builder (namely mSb)  here, as this string can be very
    430         // long when upgrading (where we re-aggregate all visible contacts) and StringBuilder won't
    431         // shrink the internal storage.
    432         // Note: don't use selection args here.  We just include all IDs directly in the selection,
    433         // because there's a limit for the number of parameters in a query.
    434         final StringBuilder sbQuery = new StringBuilder();
    435         sbQuery.append(AggregationQuery.SQL);
    436         for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) {
    437             if (index > 0) {
    438                 sbQuery.append(',');
    439             }
    440             sbQuery.append(rawContactId);
    441             index++;
    442         }
    443 
    444         sbQuery.append(')');
    445 
    446         final long[] rawContactIds;
    447         final long[] contactIds;
    448         final long[] accountIds;
    449         final int actualCount;
    450         final Cursor c = db.rawQuery(sbQuery.toString(), null);
    451         try {
    452             actualCount = c.getCount();
    453             rawContactIds = new long[actualCount];
    454             contactIds = new long[actualCount];
    455             accountIds = new long[actualCount];
    456 
    457             index = 0;
    458             while (c.moveToNext()) {
    459                 rawContactIds[index] = c.getLong(AggregationQuery._ID);
    460                 contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
    461                 accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID);
    462                 index++;
    463             }
    464         } finally {
    465             c.close();
    466         }
    467 
    468         if (DEBUG_LOGGING) {
    469             Log.d(TAG, "aggregateInTransaction: initial query done.");
    470         }
    471 
    472         for (int i = 0; i < actualCount; i++) {
    473             aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i],
    474                     mCandidates, mMatcher);
    475         }
    476 
    477         long elapsedTime = System.currentTimeMillis() - start;
    478         EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, actualCount);
    479 
    480         if (DEBUG_LOGGING) {
    481             Log.d(TAG, "Contact aggregation complete: " + actualCount +
    482                     (actualCount == 0 ? "" : ", " + (elapsedTime / actualCount)
    483                             + " ms per raw contact"));
    484         }
    485     }
    486 
    487     @SuppressWarnings("deprecation")
    488     public void triggerAggregation(TransactionContext txContext, long rawContactId) {
    489         if (!mEnabled) {
    490             return;
    491         }
    492 
    493         int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
    494         switch (aggregationMode) {
    495             case RawContacts.AGGREGATION_MODE_DISABLED:
    496                 break;
    497 
    498             case RawContacts.AGGREGATION_MODE_DEFAULT: {
    499                 markForAggregation(rawContactId, aggregationMode, false);
    500                 break;
    501             }
    502 
    503             case RawContacts.AGGREGATION_MODE_SUSPENDED: {
    504                 long contactId = mDbHelper.getContactId(rawContactId);
    505 
    506                 if (contactId != 0) {
    507                     updateAggregateData(txContext, contactId);
    508                 }
    509                 break;
    510             }
    511 
    512             case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
    513                 aggregateContact(txContext, mDbHelper.getWritableDatabase(), rawContactId);
    514                 break;
    515             }
    516         }
    517     }
    518 
    519     public void clearPendingAggregations() {
    520         // HashMap woulnd't shrink the internal table once expands it, so let's just re-create
    521         // a new one instead of clear()ing it.
    522         mRawContactsMarkedForAggregation = Maps.newHashMap();
    523     }
    524 
    525     public void markNewForAggregation(long rawContactId, int aggregationMode) {
    526         mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
    527     }
    528 
    529     public void markForAggregation(long rawContactId, int aggregationMode, boolean force) {
    530         final int effectiveAggregationMode;
    531         if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) {
    532             // As per ContactsContract documentation, default aggregation mode
    533             // does not override a previously set mode
    534             if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
    535                 effectiveAggregationMode = mRawContactsMarkedForAggregation.get(rawContactId);
    536             } else {
    537                 effectiveAggregationMode = aggregationMode;
    538             }
    539         } else {
    540             mMarkForAggregation.bindLong(1, rawContactId);
    541             mMarkForAggregation.execute();
    542             effectiveAggregationMode = aggregationMode;
    543         }
    544 
    545         mRawContactsMarkedForAggregation.put(rawContactId, effectiveAggregationMode);
    546     }
    547 
    548     private static class RawContactIdAndAggregationModeQuery {
    549         public static final String TABLE = Tables.RAW_CONTACTS;
    550 
    551         public static final String[] COLUMNS = { RawContacts._ID, RawContacts.AGGREGATION_MODE };
    552 
    553         public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
    554 
    555         public static final int _ID = 0;
    556         public static final int AGGREGATION_MODE = 1;
    557     }
    558 
    559     /**
    560      * Marks all constituent raw contacts of an aggregated contact for re-aggregation.
    561      */
    562     private void markContactForAggregation(SQLiteDatabase db, long contactId) {
    563         mSelectionArgs1[0] = String.valueOf(contactId);
    564         Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE,
    565                 RawContactIdAndAggregationModeQuery.COLUMNS,
    566                 RawContactIdAndAggregationModeQuery.SELECTION, mSelectionArgs1, null, null, null);
    567         try {
    568             if (cursor.moveToFirst()) {
    569                 long rawContactId = cursor.getLong(RawContactIdAndAggregationModeQuery._ID);
    570                 int aggregationMode = cursor.getInt(
    571                         RawContactIdAndAggregationModeQuery.AGGREGATION_MODE);
    572                 // Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED.
    573                 // (Also just ignore deprecated AGGREGATION_MODE_IMMEDIATE)
    574                 if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
    575                     markForAggregation(rawContactId, aggregationMode, true);
    576                 }
    577             }
    578         } finally {
    579             cursor.close();
    580         }
    581     }
    582 
    583     /**
    584      * Mark all visible contacts for re-aggregation.
    585      *
    586      * - Set {@link RawContactsColumns#AGGREGATION_NEEDED} For all visible raw_contacts with
    587      *   {@link RawContacts#AGGREGATION_MODE_DEFAULT}.
    588      * - Also put them into {@link #mRawContactsMarkedForAggregation}.
    589      */
    590     public int markAllVisibleForAggregation(SQLiteDatabase db) {
    591         final long start = System.currentTimeMillis();
    592 
    593         // Set AGGREGATION_NEEDED for all visible raw_cotnacts with AGGREGATION_MODE_DEFAULT.
    594         // (Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED)
    595         db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
    596                 RawContactsColumns.AGGREGATION_NEEDED + "=1" +
    597                 " WHERE " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY +
    598                 " AND " + RawContacts.AGGREGATION_MODE + "=" + RawContacts.AGGREGATION_MODE_DEFAULT
    599                 );
    600 
    601         final int count;
    602         final Cursor cursor = db.rawQuery("SELECT " + RawContacts._ID +
    603                 " FROM " + Tables.RAW_CONTACTS +
    604                 " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1", null);
    605         try {
    606             count = cursor.getCount();
    607             cursor.moveToPosition(-1);
    608             while (cursor.moveToNext()) {
    609                 final long rawContactId = cursor.getLong(0);
    610                 mRawContactsMarkedForAggregation.put(rawContactId,
    611                         RawContacts.AGGREGATION_MODE_DEFAULT);
    612             }
    613         } finally {
    614             cursor.close();
    615         }
    616 
    617         final long end = System.currentTimeMillis();
    618         Log.i(TAG, "Marked all visible contacts for aggregation: " + count + " raw contacts, " +
    619                 (end - start) + " ms");
    620         return count;
    621     }
    622 
    623     /**
    624      * Creates a new contact based on the given raw contact.  Does not perform aggregation.  Returns
    625      * the ID of the contact that was created.
    626      */
    627     public long onRawContactInsert(
    628             TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
    629         long contactId = insertContact(db, rawContactId);
    630         setContactId(rawContactId, contactId);
    631         mDbHelper.updateContactVisible(txContext, contactId);
    632         return contactId;
    633     }
    634 
    635     protected long insertContact(SQLiteDatabase db, long rawContactId) {
    636         mSelectionArgs1[0] = String.valueOf(rawContactId);
    637         computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
    638         return mContactInsert.executeInsert();
    639     }
    640 
    641     private static final class RawContactIdAndAccountQuery {
    642         public static final String TABLE = Tables.RAW_CONTACTS;
    643 
    644         public static final String[] COLUMNS = {
    645                 RawContacts.CONTACT_ID,
    646                 RawContactsColumns.ACCOUNT_ID
    647         };
    648 
    649         public static final String SELECTION = RawContacts._ID + "=?";
    650 
    651         public static final int CONTACT_ID = 0;
    652         public static final int ACCOUNT_ID = 1;
    653     }
    654 
    655     public void aggregateContact(
    656             TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
    657         if (!mEnabled) {
    658             return;
    659         }
    660 
    661         MatchCandidateList candidates = new MatchCandidateList();
    662         ContactMatcher matcher = new ContactMatcher();
    663 
    664         long contactId = 0;
    665         long accountId = 0;
    666         mSelectionArgs1[0] = String.valueOf(rawContactId);
    667         Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE,
    668                 RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION,
    669                 mSelectionArgs1, null, null, null);
    670         try {
    671             if (cursor.moveToFirst()) {
    672                 contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID);
    673                 accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID);
    674             }
    675         } finally {
    676             cursor.close();
    677         }
    678 
    679         aggregateContact(txContext, db, rawContactId, accountId, contactId,
    680                 candidates, matcher);
    681     }
    682 
    683     public void updateAggregateData(TransactionContext txContext, long contactId) {
    684         if (!mEnabled) {
    685             return;
    686         }
    687 
    688         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    689         computeAggregateData(db, contactId, mContactUpdate);
    690         mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
    691         mContactUpdate.execute();
    692 
    693         mDbHelper.updateContactVisible(txContext, contactId);
    694         updateAggregatedStatusUpdate(contactId);
    695     }
    696 
    697     private void updateAggregatedStatusUpdate(long contactId) {
    698         mAggregatedPresenceReplace.bindLong(1, contactId);
    699         mAggregatedPresenceReplace.bindLong(2, contactId);
    700         mAggregatedPresenceReplace.execute();
    701         updateLastStatusUpdateId(contactId);
    702     }
    703 
    704     /**
    705      * Adjusts the reference to the latest status update for the specified contact.
    706      */
    707     public void updateLastStatusUpdateId(long contactId) {
    708         String contactIdString = String.valueOf(contactId);
    709         mDbHelper.getWritableDatabase().execSQL(UPDATE_LAST_STATUS_UPDATE_ID_SQL,
    710                 new String[]{contactIdString, contactIdString});
    711     }
    712 
    713     /**
    714      * Given a specific raw contact, finds all matching aggregate contacts and chooses the one
    715      * with the highest match score.  If no such contact is found, creates a new contact.
    716      */
    717     private synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
    718             long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates,
    719             ContactMatcher matcher) {
    720 
    721         if (VERBOSE_LOGGING) {
    722             Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId);
    723         }
    724 
    725         int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
    726 
    727         Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId);
    728         if (aggModeObject != null) {
    729             aggregationMode = aggModeObject;
    730         }
    731 
    732         long contactId = -1; // Best matching contact ID.
    733         long contactIdToSplit = -1;
    734 
    735         if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
    736             candidates.clear();
    737             matcher.clear();
    738 
    739             contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher);
    740             if (contactId == -1) {
    741 
    742                 // If this is a newly inserted contact or a visible contact, look for
    743                 // data matches.
    744                 if (currentContactId == 0
    745                         || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) {
    746                     contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher);
    747                 }
    748 
    749                 // If we found an aggregate to join, but it already contains raw contacts from
    750                 // the same account, not only will we not join it, but also we will split
    751                 // that other aggregate
    752                 if (contactId != -1 && contactId != currentContactId &&
    753                         !canJoinIntoContact(db, contactId, rawContactId, accountId)) {
    754                     contactIdToSplit = contactId;
    755                     contactId = -1;
    756                 }
    757             }
    758         } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
    759             return;
    760         }
    761 
    762         // # of raw_contacts in the [currentContactId] contact excluding the [rawContactId]
    763         // raw_contact.
    764         long currentContactContentsCount = 0;
    765 
    766         if (currentContactId != 0) {
    767             mRawContactCountQuery.bindLong(1, currentContactId);
    768             mRawContactCountQuery.bindLong(2, rawContactId);
    769             currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong();
    770         }
    771 
    772         // If there are no other raw contacts in the current aggregate, we might as well reuse it.
    773         // Also, if the aggregation mode is SUSPENDED, we must reuse the same aggregate.
    774         if (contactId == -1
    775                 && currentContactId != 0
    776                 && (currentContactContentsCount == 0
    777                         || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) {
    778             contactId = currentContactId;
    779         }
    780 
    781         if (contactId == currentContactId) {
    782             // Aggregation unchanged
    783             markAggregated(rawContactId);
    784         } else if (contactId == -1) {
    785             // Splitting an aggregate
    786             createNewContactForRawContact(txContext, db, rawContactId);
    787             if (currentContactContentsCount > 0) {
    788                 updateAggregateData(txContext, currentContactId);
    789             }
    790         } else {
    791             // Joining with an existing aggregate
    792             if (currentContactContentsCount == 0) {
    793                 // Delete a previous aggregate if it only contained this raw contact
    794                 ContactsTableUtil.deleteContact(db, currentContactId);
    795 
    796                 mAggregatedPresenceDelete.bindLong(1, currentContactId);
    797                 mAggregatedPresenceDelete.execute();
    798             }
    799 
    800             setContactIdAndMarkAggregated(rawContactId, contactId);
    801             computeAggregateData(db, contactId, mContactUpdate);
    802             mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
    803             mContactUpdate.execute();
    804             mDbHelper.updateContactVisible(txContext, contactId);
    805             updateAggregatedStatusUpdate(contactId);
    806             // Make sure the raw contact does not contribute to the current contact
    807             if (currentContactId != 0) {
    808                 updateAggregateData(txContext, currentContactId);
    809             }
    810         }
    811 
    812         if (contactIdToSplit != -1) {
    813             splitAutomaticallyAggregatedRawContacts(txContext, db, contactIdToSplit);
    814         }
    815     }
    816 
    817     /**
    818      * @return true if the raw contact of {@code rawContactId} can be joined into the existing
    819      * contact of {@code of contactId}.
    820      *
    821      * Now a raw contact can be merged into a contact containing raw contacts from
    822      * the same account if there's at least one raw contact in those raw contacts
    823      * that shares at least one email address, phone number, or identity.
    824      */
    825     private boolean canJoinIntoContact(SQLiteDatabase db, long contactId,
    826             long rawContactId, long rawContactAccountId) {
    827         // First, list all raw contact IDs in contact [contactId] on account [rawContactAccountId],
    828         // excluding raw_contact [rawContactId].
    829 
    830         // Append all found raw contact IDs into this SB to create a comma separated list of
    831         // the IDs.
    832         // We don't always need it, so lazily initialize it.
    833         StringBuilder rawContactIdsBuilder;
    834 
    835         mSelectionArgs3[0] = String.valueOf(contactId);
    836         mSelectionArgs3[1] = String.valueOf(rawContactId);
    837         mSelectionArgs3[2] = String.valueOf(rawContactAccountId);
    838         final Cursor duplicatesCursor = db.rawQuery(
    839                 "SELECT " + RawContacts._ID +
    840                 " FROM " + Tables.RAW_CONTACTS +
    841                 " WHERE " + RawContacts.CONTACT_ID + "=?" +
    842                 " AND " + RawContacts._ID + "!=?" +
    843                 " AND " + RawContactsColumns.ACCOUNT_ID +"=?",
    844                 mSelectionArgs3);
    845         try {
    846             final int duplicateCount = duplicatesCursor.getCount();
    847             if (duplicateCount == 0) {
    848                 return true; // No duplicates -- common case -- bail early.
    849             }
    850             if (VERBOSE_LOGGING) {
    851                 Log.v(TAG, "canJoinIntoContact: " + duplicateCount + " duplicate(s) found");
    852             }
    853 
    854             rawContactIdsBuilder = new StringBuilder();
    855 
    856             duplicatesCursor.moveToPosition(-1);
    857             while (duplicatesCursor.moveToNext()) {
    858                 if (rawContactIdsBuilder.length() > 0) {
    859                     rawContactIdsBuilder.append(',');
    860                 }
    861                 rawContactIdsBuilder.append(duplicatesCursor.getLong(0));
    862             }
    863         } finally {
    864             duplicatesCursor.close();
    865         }
    866 
    867         // Comma separated raw_contacts IDs.
    868         final String rawContactIds = rawContactIdsBuilder.toString();
    869 
    870         // See if there's any raw_contacts that share an email address, a phone number, or
    871         // an identity with raw_contact [rawContactId].
    872 
    873         // First, check for the email address.
    874         mSelectionArgs2[0] = String.valueOf(mMimeTypeIdEmail);
    875         mSelectionArgs2[1] = String.valueOf(rawContactId);
    876         if (isFirstColumnGreaterThanZero(db,
    877                 "SELECT count(*)" +
    878                 " FROM " + Tables.DATA + " AS d1" +
    879                 " JOIN " + Tables.DATA + " AS d2"
    880                     + " ON (d1." + Email.ADDRESS + " = d2." + Email.ADDRESS + ")" +
    881                 " WHERE d1." + DataColumns.MIMETYPE_ID + " = ?1" +
    882                 " AND d2." + DataColumns.MIMETYPE_ID + " = ?1" +
    883                 " AND d1." + Data.RAW_CONTACT_ID + " = ?2" +
    884                 " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")",
    885                 mSelectionArgs2)) {
    886             if (VERBOSE_LOGGING) {
    887                 Log.v(TAG, "Relaxing rule SA: email match found for rid=" + rawContactId);
    888             }
    889             return true;
    890         }
    891 
    892         // Next, check for the identity.
    893         mSelectionArgs2[0] = String.valueOf(mMimeTypeIdIdentity);
    894         mSelectionArgs2[1] = String.valueOf(rawContactId);
    895         if (isFirstColumnGreaterThanZero(db,
    896                 "SELECT count(*)" +
    897                 " FROM " + Tables.DATA + " AS d1" +
    898                 " JOIN " + Tables.DATA + " AS d2"
    899                     + " ON (d1." + Identity.IDENTITY + " = d2." + Identity.IDENTITY + " AND" +
    900                            " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" +
    901                 " WHERE d1." + DataColumns.MIMETYPE_ID + " = ?1" +
    902                 " AND d2." + DataColumns.MIMETYPE_ID + " = ?1" +
    903                 " AND d1." + Data.RAW_CONTACT_ID + " = ?2" +
    904                 " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")",
    905                 mSelectionArgs2)) {
    906             if (VERBOSE_LOGGING) {
    907                 Log.v(TAG, "Relaxing rule SA: identity match found for rid=" + rawContactId);
    908             }
    909             return true;
    910         }
    911 
    912         // Lastly, the phone number.
    913         // It's a bit tricker because it has to be consistent with
    914         // updateMatchScoresBasedOnPhoneMatches().
    915         mSelectionArgs3[0] = String.valueOf(mMimeTypeIdPhone);
    916         mSelectionArgs3[1] = String.valueOf(rawContactId);
    917         mSelectionArgs3[2] = String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter());
    918 
    919         if (isFirstColumnGreaterThanZero(db,
    920                 "SELECT count(*)" +
    921                 " FROM " + Tables.PHONE_LOOKUP + " AS p1" +
    922                 " JOIN " + Tables.DATA + " AS d1 ON " +
    923                     "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" +
    924                 " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH +
    925                     "=p2." + PhoneLookupColumns.MIN_MATCH + ")" +
    926                 " JOIN " + Tables.DATA + " AS d2 ON " +
    927                     "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" +
    928                 " WHERE d1." + DataColumns.MIMETYPE_ID + " = ?1" +
    929                 " AND d2." + DataColumns.MIMETYPE_ID + " = ?1" +
    930                 " AND d1." + Data.RAW_CONTACT_ID + " = ?2" +
    931                 " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")" +
    932                 " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + ",?3)",
    933                 mSelectionArgs3)) {
    934             if (VERBOSE_LOGGING) {
    935                 Log.v(TAG, "Relaxing rule SA: phone match found for rid=" + rawContactId);
    936             }
    937             return true;
    938         }
    939         if (VERBOSE_LOGGING) {
    940             Log.v(TAG, "Rule SA splitting up cid=" + contactId + " for rid=" + rawContactId);
    941         }
    942         return false;
    943     }
    944 
    945     private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query,
    946             String[] selectionArgs) {
    947         final Cursor cursor = db.rawQuery(query, selectionArgs);
    948         try {
    949             return cursor.moveToFirst() && (cursor.getInt(0) > 0);
    950         } finally {
    951             cursor.close();
    952         }
    953     }
    954 
    955     /**
    956      * Breaks up an existing aggregate when a new raw contact is inserted that has
    957      * come from the same account as one of the raw contacts in this aggregate.
    958      */
    959     private void splitAutomaticallyAggregatedRawContacts(
    960             TransactionContext txContext, SQLiteDatabase db, long contactId) {
    961         mSelectionArgs1[0] = String.valueOf(contactId);
    962         int count = (int) DatabaseUtils.longForQuery(db,
    963                 "SELECT COUNT(" + RawContacts._ID + ")" +
    964                 " FROM " + Tables.RAW_CONTACTS +
    965                 " WHERE " + RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
    966         if (count < 2) {
    967             // A single-raw-contact aggregate does not need to be split up
    968             return;
    969         }
    970 
    971         // Find all constituent raw contacts that are not held together by
    972         // an explicit aggregation exception
    973         String query =
    974                 "SELECT " + RawContacts._ID +
    975                 " FROM " + Tables.RAW_CONTACTS +
    976                 " WHERE " + RawContacts.CONTACT_ID + "=?" +
    977                 "   AND " + RawContacts._ID + " NOT IN " +
    978                         "(SELECT " + AggregationExceptions.RAW_CONTACT_ID1 +
    979                         " FROM " + Tables.AGGREGATION_EXCEPTIONS +
    980                         " WHERE " + AggregationExceptions.TYPE + "="
    981                                 + AggregationExceptions.TYPE_KEEP_TOGETHER +
    982                         " UNION SELECT " + AggregationExceptions.RAW_CONTACT_ID2 +
    983                         " FROM " + Tables.AGGREGATION_EXCEPTIONS +
    984                         " WHERE " + AggregationExceptions.TYPE + "="
    985                                 + AggregationExceptions.TYPE_KEEP_TOGETHER +
    986                         ")";
    987 
    988         Cursor cursor = db.rawQuery(query, mSelectionArgs1);
    989         try {
    990             // Process up to count-1 raw contact, leaving the last one alone.
    991             for (int i = 0; i < count - 1; i++) {
    992                 if (!cursor.moveToNext()) {
    993                     break;
    994                 }
    995                 long rawContactId = cursor.getLong(0);
    996                 createNewContactForRawContact(txContext, db, rawContactId);
    997             }
    998         } finally {
    999             cursor.close();
   1000         }
   1001         if (contactId > 0) {
   1002             updateAggregateData(txContext, contactId);
   1003         }
   1004     }
   1005 
   1006     /**
   1007      * Creates a stand-alone Contact for the given raw contact ID. This is only called
   1008      * when splitting an existing merged contact into separate raw contacts.
   1009      */
   1010     private void createNewContactForRawContact(
   1011             TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
   1012         // All split contacts should automatically be unpinned.
   1013         unpinRawContact(rawContactId);
   1014         mSelectionArgs1[0] = String.valueOf(rawContactId);
   1015         computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
   1016                 mContactInsert);
   1017         long contactId = mContactInsert.executeInsert();
   1018         setContactIdAndMarkAggregated(rawContactId, contactId);
   1019         mDbHelper.updateContactVisible(txContext, contactId);
   1020         setPresenceContactId(rawContactId, contactId);
   1021         updateAggregatedStatusUpdate(contactId);
   1022     }
   1023 
   1024     private static class RawContactIdQuery {
   1025         public static final String TABLE = Tables.RAW_CONTACTS;
   1026         public static final String[] COLUMNS = { RawContacts._ID };
   1027         public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
   1028         public static final int RAW_CONTACT_ID = 0;
   1029     }
   1030 
   1031     /**
   1032      * Ensures that automatic aggregation rules are followed after a contact
   1033      * becomes visible or invisible. Specifically, consider this case: there are
   1034      * three contacts named Foo. Two of them come from account A1 and one comes
   1035      * from account A2. The aggregation rules say that in this case none of the
   1036      * three Foo's should be aggregated: two of them are in the same account, so
   1037      * they don't get aggregated; the third has two affinities, so it does not
   1038      * join either of them.
   1039      * <p>
   1040      * Consider what happens if one of the "Foo"s from account A1 becomes
   1041      * invisible. Nothing stands in the way of aggregating the other two
   1042      * anymore, so they should get joined.
   1043      * <p>
   1044      * What if the invisible "Foo" becomes visible after that? We should split the
   1045      * aggregate between the other two.
   1046      */
   1047     public void updateAggregationAfterVisibilityChange(long contactId) {
   1048         SQLiteDatabase db = mDbHelper.getWritableDatabase();
   1049         boolean visible = mDbHelper.isContactInDefaultDirectory(db, contactId);
   1050         if (visible) {
   1051             markContactForAggregation(db, contactId);
   1052         } else {
   1053             // Find all contacts that _could be_ aggregated with this one and
   1054             // rerun aggregation for all of them
   1055             mSelectionArgs1[0] = String.valueOf(contactId);
   1056             Cursor cursor = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
   1057                     RawContactIdQuery.SELECTION, mSelectionArgs1, null, null, null);
   1058             try {
   1059                 while (cursor.moveToNext()) {
   1060                     long rawContactId = cursor.getLong(RawContactIdQuery.RAW_CONTACT_ID);
   1061                     mMatcher.clear();
   1062 
   1063                     updateMatchScoresBasedOnIdentityMatch(db, rawContactId, mMatcher);
   1064                     updateMatchScoresBasedOnNameMatches(db, rawContactId, mMatcher);
   1065                     List<MatchScore> bestMatches =
   1066                             mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
   1067                     for (MatchScore matchScore : bestMatches) {
   1068                         markContactForAggregation(db, matchScore.getContactId());
   1069                     }
   1070 
   1071                     mMatcher.clear();
   1072                     updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher);
   1073                     updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher);
   1074                     bestMatches =
   1075                             mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
   1076                     for (MatchScore matchScore : bestMatches) {
   1077                         markContactForAggregation(db, matchScore.getContactId());
   1078                     }
   1079                 }
   1080             } finally {
   1081                 cursor.close();
   1082             }
   1083         }
   1084     }
   1085 
   1086     /**
   1087      * Updates the contact ID for the specified contact.
   1088      */
   1089     protected void setContactId(long rawContactId, long contactId) {
   1090         mContactIdUpdate.bindLong(1, contactId);
   1091         mContactIdUpdate.bindLong(2, rawContactId);
   1092         mContactIdUpdate.execute();
   1093     }
   1094 
   1095     /**
   1096      * Marks the specified raw contact ID as aggregated
   1097      */
   1098     private void markAggregated(long rawContactId) {
   1099         mMarkAggregatedUpdate.bindLong(1, rawContactId);
   1100         mMarkAggregatedUpdate.execute();
   1101     }
   1102 
   1103     /**
   1104      * Updates the contact ID for the specified contact and marks the raw contact as aggregated.
   1105      */
   1106     private void setContactIdAndMarkAggregated(long rawContactId, long contactId) {
   1107         mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId);
   1108         mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId);
   1109         mContactIdAndMarkAggregatedUpdate.execute();
   1110     }
   1111 
   1112     private void setPresenceContactId(long rawContactId, long contactId) {
   1113         mPresenceContactIdUpdate.bindLong(1, contactId);
   1114         mPresenceContactIdUpdate.bindLong(2, rawContactId);
   1115         mPresenceContactIdUpdate.execute();
   1116     }
   1117 
   1118     private void unpinRawContact(long rawContactId) {
   1119         mResetPinnedForRawContact.bindLong(1, rawContactId);
   1120         mResetPinnedForRawContact.execute();
   1121     }
   1122 
   1123     interface AggregateExceptionPrefetchQuery {
   1124         String TABLE = Tables.AGGREGATION_EXCEPTIONS;
   1125 
   1126         String[] COLUMNS = {
   1127             AggregationExceptions.RAW_CONTACT_ID1,
   1128             AggregationExceptions.RAW_CONTACT_ID2,
   1129         };
   1130 
   1131         int RAW_CONTACT_ID1 = 0;
   1132         int RAW_CONTACT_ID2 = 1;
   1133     }
   1134 
   1135     // A set of raw contact IDs for which there are aggregation exceptions
   1136     private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
   1137     private boolean mAggregationExceptionIdsValid;
   1138 
   1139     public void invalidateAggregationExceptionCache() {
   1140         mAggregationExceptionIdsValid = false;
   1141     }
   1142 
   1143     /**
   1144      * Finds all raw contact IDs for which there are aggregation exceptions. The list of
   1145      * ids is used as an optimization in aggregation: there is no point to run a query against
   1146      * the agg_exceptions table if it is known that there are no records there for a given
   1147      * raw contact ID.
   1148      */
   1149     private void prefetchAggregationExceptionIds(SQLiteDatabase db) {
   1150         mAggregationExceptionIds.clear();
   1151         final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
   1152                 AggregateExceptionPrefetchQuery.COLUMNS,
   1153                 null, null, null, null, null);
   1154 
   1155         try {
   1156             while (c.moveToNext()) {
   1157                 long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
   1158                 long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
   1159                 mAggregationExceptionIds.add(rawContactId1);
   1160                 mAggregationExceptionIds.add(rawContactId2);
   1161             }
   1162         } finally {
   1163             c.close();
   1164         }
   1165 
   1166         mAggregationExceptionIdsValid = true;
   1167     }
   1168 
   1169     interface AggregateExceptionQuery {
   1170         String TABLE = Tables.AGGREGATION_EXCEPTIONS
   1171             + " JOIN raw_contacts raw_contacts1 "
   1172                     + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) "
   1173             + " JOIN raw_contacts raw_contacts2 "
   1174                     + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) ";
   1175 
   1176         String[] COLUMNS = {
   1177             AggregationExceptions.TYPE,
   1178             AggregationExceptions.RAW_CONTACT_ID1,
   1179             "raw_contacts1." + RawContacts.CONTACT_ID,
   1180             "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED,
   1181             "raw_contacts2." + RawContacts.CONTACT_ID,
   1182             "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED,
   1183         };
   1184 
   1185         int TYPE = 0;
   1186         int RAW_CONTACT_ID1 = 1;
   1187         int CONTACT_ID1 = 2;
   1188         int AGGREGATION_NEEDED_1 = 3;
   1189         int CONTACT_ID2 = 4;
   1190         int AGGREGATION_NEEDED_2 = 5;
   1191     }
   1192 
   1193     /**
   1194      * Computes match scores based on exceptions entered by the user: always match and never match.
   1195      * Returns the aggregate contact with the always match exception if any.
   1196      */
   1197     private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId,
   1198             ContactMatcher matcher) {
   1199         if (!mAggregationExceptionIdsValid) {
   1200             prefetchAggregationExceptionIds(db);
   1201         }
   1202 
   1203         // If there are no aggregation exceptions involving this raw contact, there is no need to
   1204         // run a query and we can just return -1, which stands for "nothing found"
   1205         if (!mAggregationExceptionIds.contains(rawContactId)) {
   1206             return -1;
   1207         }
   1208 
   1209         final Cursor c = db.query(AggregateExceptionQuery.TABLE,
   1210                 AggregateExceptionQuery.COLUMNS,
   1211                 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
   1212                         + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId,
   1213                 null, null, null, null);
   1214 
   1215         try {
   1216             while (c.moveToNext()) {
   1217                 int type = c.getInt(AggregateExceptionQuery.TYPE);
   1218                 long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
   1219                 long contactId = -1;
   1220                 if (rawContactId == rawContactId1) {
   1221                     if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0
   1222                             && !c.isNull(AggregateExceptionQuery.CONTACT_ID2)) {
   1223                         contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2);
   1224                     }
   1225                 } else {
   1226                     if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0
   1227                             && !c.isNull(AggregateExceptionQuery.CONTACT_ID1)) {
   1228                         contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1);
   1229                     }
   1230                 }
   1231                 if (contactId != -1) {
   1232                     if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) {
   1233                         matcher.keepIn(contactId);
   1234                     } else {
   1235                         matcher.keepOut(contactId);
   1236                     }
   1237                 }
   1238             }
   1239         } finally {
   1240             c.close();
   1241         }
   1242 
   1243         return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true);
   1244     }
   1245 
   1246     /**
   1247      * Picks the best matching contact based on matches between data elements.  It considers
   1248      * name match to be primary and phone, email etc matches to be secondary.  A good primary
   1249      * match triggers aggregation, while a good secondary match only triggers aggregation in
   1250      * the absence of a strong primary mismatch.
   1251      * <p>
   1252      * Consider these examples:
   1253      * <p>
   1254      * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should
   1255      * be aggregated (same number, similar names).
   1256      * <p>
   1257      * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should
   1258      * not be aggregated (same number, different names).
   1259      */
   1260     private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
   1261             MatchCandidateList candidates, ContactMatcher matcher) {
   1262 
   1263         // Find good matches based on name alone
   1264         long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, matcher);
   1265         if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
   1266             // We found multiple matches on the name - do not aggregate because of the ambiguity
   1267             return -1;
   1268         } else if (bestMatch == -1) {
   1269             // We haven't found a good match on name, see if we have any matches on phone, email etc
   1270             bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher);
   1271             if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
   1272                 return -1;
   1273             }
   1274         }
   1275 
   1276         return bestMatch;
   1277     }
   1278 
   1279 
   1280     /**
   1281      * Picks the best matching contact based on secondary data matches.  The method loads
   1282      * structured names for all candidate contacts and recomputes match scores using approximate
   1283      * matching.
   1284      */
   1285     private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db,
   1286             long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
   1287         List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates(
   1288                 ContactMatcher.SCORE_THRESHOLD_PRIMARY);
   1289         if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) {
   1290             return -1;
   1291         }
   1292 
   1293         loadNameMatchCandidates(db, rawContactId, candidates, true);
   1294 
   1295         mSb.setLength(0);
   1296         mSb.append(RawContacts.CONTACT_ID).append(" IN (");
   1297         for (int i = 0; i < secondaryContactIds.size(); i++) {
   1298             if (i != 0) {
   1299                 mSb.append(',');
   1300             }
   1301             mSb.append(secondaryContactIds.get(i));
   1302         }
   1303 
   1304         // We only want to compare structured names to structured names
   1305         // at this stage, we need to ignore all other sources of name lookup data.
   1306         mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL);
   1307 
   1308         matchAllCandidates(db, mSb.toString(), candidates, matcher,
   1309                 ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
   1310 
   1311         return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false);
   1312     }
   1313 
   1314     private interface NameLookupQuery {
   1315         String TABLE = Tables.NAME_LOOKUP;
   1316 
   1317         String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
   1318         String SELECTION_STRUCTURED_NAME_BASED =
   1319                 SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
   1320 
   1321         String[] COLUMNS = new String[] {
   1322                 NameLookupColumns.NORMALIZED_NAME,
   1323                 NameLookupColumns.NAME_TYPE
   1324         };
   1325 
   1326         int NORMALIZED_NAME = 0;
   1327         int NAME_TYPE = 1;
   1328     }
   1329 
   1330     private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
   1331             MatchCandidateList candidates, boolean structuredNameBased) {
   1332         candidates.clear();
   1333         mSelectionArgs1[0] = String.valueOf(rawContactId);
   1334         Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
   1335                 structuredNameBased
   1336                         ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
   1337                         : NameLookupQuery.SELECTION,
   1338                 mSelectionArgs1, null, null, null);
   1339         try {
   1340             while (c.moveToNext()) {
   1341                 String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
   1342                 int type = c.getInt(NameLookupQuery.NAME_TYPE);
   1343                 candidates.add(normalizedName, type);
   1344             }
   1345         } finally {
   1346             c.close();
   1347         }
   1348     }
   1349 
   1350     /**
   1351      * Computes scores for contacts that have matching data rows.
   1352      */
   1353     private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
   1354             ContactMatcher matcher) {
   1355 
   1356         updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
   1357         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
   1358         long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY, false);
   1359         if (bestMatch != -1) {
   1360             return bestMatch;
   1361         }
   1362 
   1363         updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
   1364         updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
   1365 
   1366         return -1;
   1367     }
   1368 
   1369     private interface IdentityLookupMatchQuery {
   1370         final String TABLE = Tables.DATA + " dataA"
   1371                 + " JOIN " + Tables.DATA + " dataB" +
   1372                 " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE +
   1373                 " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")"
   1374                 + " JOIN " + Tables.RAW_CONTACTS +
   1375                 " ON (dataB." + Data.RAW_CONTACT_ID + " = "
   1376                 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
   1377 
   1378         final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
   1379                 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
   1380                 + " AND dataA." + Identity.NAMESPACE + " NOT NULL"
   1381                 + " AND dataA." + Identity.IDENTITY + " NOT NULL"
   1382                 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
   1383                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
   1384                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
   1385 
   1386         final String[] COLUMNS = new String[] {
   1387             RawContacts.CONTACT_ID
   1388         };
   1389 
   1390         int CONTACT_ID = 0;
   1391     }
   1392 
   1393     /**
   1394      * Finds contacts with exact identity matches to the the specified raw contact.
   1395      */
   1396     private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId,
   1397             ContactMatcher matcher) {
   1398         mSelectionArgs2[0] = String.valueOf(rawContactId);
   1399         mSelectionArgs2[1] = String.valueOf(mMimeTypeIdIdentity);
   1400         Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS,
   1401                 IdentityLookupMatchQuery.SELECTION,
   1402                 mSelectionArgs2, RawContacts.CONTACT_ID, null, null);
   1403         try {
   1404             while (c.moveToNext()) {
   1405                 final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID);
   1406                 matcher.matchIdentity(contactId);
   1407             }
   1408         } finally {
   1409             c.close();
   1410         }
   1411 
   1412     }
   1413 
   1414     private interface NameLookupMatchQuery {
   1415         String TABLE = Tables.NAME_LOOKUP + " nameA"
   1416                 + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
   1417                 " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
   1418                         + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
   1419                 + " JOIN " + Tables.RAW_CONTACTS +
   1420                 " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
   1421                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
   1422 
   1423         String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
   1424                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
   1425                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
   1426 
   1427         String[] COLUMNS = new String[] {
   1428             RawContacts.CONTACT_ID,
   1429             "nameA." + NameLookupColumns.NORMALIZED_NAME,
   1430             "nameA." + NameLookupColumns.NAME_TYPE,
   1431             "nameB." + NameLookupColumns.NAME_TYPE,
   1432         };
   1433 
   1434         int CONTACT_ID = 0;
   1435         int NAME = 1;
   1436         int NAME_TYPE_A = 2;
   1437         int NAME_TYPE_B = 3;
   1438     }
   1439 
   1440     /**
   1441      * Finds contacts with names matching the name of the specified raw contact.
   1442      */
   1443     private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
   1444             ContactMatcher matcher) {
   1445         mSelectionArgs1[0] = String.valueOf(rawContactId);
   1446         Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
   1447                 NameLookupMatchQuery.SELECTION,
   1448                 mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
   1449         try {
   1450             while (c.moveToNext()) {
   1451                 long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
   1452                 String name = c.getString(NameLookupMatchQuery.NAME);
   1453                 int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
   1454                 int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
   1455                 matcher.matchName(contactId, nameTypeA, name,
   1456                         nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT);
   1457                 if (nameTypeA == NameLookupType.NICKNAME &&
   1458                         nameTypeB == NameLookupType.NICKNAME) {
   1459                     matcher.updateScoreWithNicknameMatch(contactId);
   1460                 }
   1461             }
   1462         } finally {
   1463             c.close();
   1464         }
   1465     }
   1466 
   1467     private interface NameLookupMatchQueryWithParameter {
   1468         String TABLE = Tables.NAME_LOOKUP
   1469                 + " JOIN " + Tables.RAW_CONTACTS +
   1470                 " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = "
   1471                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
   1472 
   1473         String[] COLUMNS = new String[] {
   1474             RawContacts.CONTACT_ID,
   1475             NameLookupColumns.NORMALIZED_NAME,
   1476             NameLookupColumns.NAME_TYPE,
   1477         };
   1478 
   1479         int CONTACT_ID = 0;
   1480         int NAME = 1;
   1481         int NAME_TYPE = 2;
   1482     }
   1483 
   1484     private final class NameLookupSelectionBuilder extends NameLookupBuilder {
   1485 
   1486         private final MatchCandidateList mNameLookupCandidates;
   1487 
   1488         private StringBuilder mSelection = new StringBuilder(
   1489                 NameLookupColumns.NORMALIZED_NAME + " IN(");
   1490 
   1491 
   1492         public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) {
   1493             super(splitter);
   1494             this.mNameLookupCandidates = candidates;
   1495         }
   1496 
   1497         @Override
   1498         protected String[] getCommonNicknameClusters(String normalizedName) {
   1499             return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
   1500         }
   1501 
   1502         @Override
   1503         protected void insertNameLookup(
   1504                 long rawContactId, long dataId, int lookupType, String string) {
   1505             mNameLookupCandidates.add(string, lookupType);
   1506             DatabaseUtils.appendEscapedSQLString(mSelection, string);
   1507             mSelection.append(',');
   1508         }
   1509 
   1510         public boolean isEmpty() {
   1511             return mNameLookupCandidates.isEmpty();
   1512         }
   1513 
   1514         public String getSelection() {
   1515             mSelection.setLength(mSelection.length() - 1);      // Strip last comma
   1516             mSelection.append(')');
   1517             return mSelection.toString();
   1518         }
   1519 
   1520         public int getLookupType(String name) {
   1521             for (int i = 0; i < mNameLookupCandidates.mCount; i++) {
   1522                 if (mNameLookupCandidates.mList.get(i).mName.equals(name)) {
   1523                     return mNameLookupCandidates.mList.get(i).mLookupType;
   1524                 }
   1525             }
   1526             throw new IllegalStateException();
   1527         }
   1528     }
   1529 
   1530     /**
   1531      * Finds contacts with names matching the specified name.
   1532      */
   1533     private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
   1534             MatchCandidateList candidates, ContactMatcher matcher) {
   1535         candidates.clear();
   1536         NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
   1537                 mNameSplitter, candidates);
   1538         builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
   1539         if (builder.isEmpty()) {
   1540             return;
   1541         }
   1542 
   1543         Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
   1544                 NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
   1545                 null, PRIMARY_HIT_LIMIT_STRING);
   1546         try {
   1547             while (c.moveToNext()) {
   1548                 long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
   1549                 String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
   1550                 int nameTypeA = builder.getLookupType(name);
   1551                 int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
   1552                 matcher.matchName(contactId, nameTypeA, name, nameTypeB, name,
   1553                         ContactMatcher.MATCHING_ALGORITHM_EXACT);
   1554                 if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
   1555                     matcher.updateScoreWithNicknameMatch(contactId);
   1556                 }
   1557             }
   1558         } finally {
   1559             c.close();
   1560         }
   1561     }
   1562 
   1563     private interface EmailLookupQuery {
   1564         String TABLE = Tables.DATA + " dataA"
   1565                 + " JOIN " + Tables.DATA + " dataB" +
   1566                 " ON (" + "dataA." + Email.DATA + "=dataB." + Email.DATA + ")"
   1567                 + " JOIN " + Tables.RAW_CONTACTS +
   1568                 " ON (dataB." + Data.RAW_CONTACT_ID + " = "
   1569                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
   1570 
   1571         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
   1572                 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
   1573                 + " AND dataA." + Email.DATA + " NOT NULL"
   1574                 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
   1575                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
   1576                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
   1577 
   1578         String[] COLUMNS = new String[] {
   1579             RawContacts.CONTACT_ID
   1580         };
   1581 
   1582         int CONTACT_ID = 0;
   1583     }
   1584 
   1585     private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
   1586             ContactMatcher matcher) {
   1587         mSelectionArgs2[0] = String.valueOf(rawContactId);
   1588         mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail);
   1589         Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
   1590                 EmailLookupQuery.SELECTION,
   1591                 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
   1592         try {
   1593             while (c.moveToNext()) {
   1594                 long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
   1595                 matcher.updateScoreWithEmailMatch(contactId);
   1596             }
   1597         } finally {
   1598             c.close();
   1599         }
   1600     }
   1601 
   1602     private interface PhoneLookupQuery {
   1603         String TABLE = Tables.PHONE_LOOKUP + " phoneA"
   1604                 + " JOIN " + Tables.DATA + " dataA"
   1605                 + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
   1606                 + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
   1607                 + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
   1608                         + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
   1609                 + " JOIN " + Tables.DATA + " dataB"
   1610                 + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
   1611                 + " JOIN " + Tables.RAW_CONTACTS
   1612                 + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
   1613                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
   1614 
   1615         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
   1616                 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
   1617                         + "dataB." + Phone.NUMBER + ",?)"
   1618                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
   1619                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
   1620 
   1621         String[] COLUMNS = new String[] {
   1622             RawContacts.CONTACT_ID
   1623         };
   1624 
   1625         int CONTACT_ID = 0;
   1626     }
   1627 
   1628     private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
   1629             ContactMatcher matcher) {
   1630         mSelectionArgs2[0] = String.valueOf(rawContactId);
   1631         mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
   1632         Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
   1633                 PhoneLookupQuery.SELECTION,
   1634                 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
   1635         try {
   1636             while (c.moveToNext()) {
   1637                 long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
   1638                 matcher.updateScoreWithPhoneNumberMatch(contactId);
   1639             }
   1640         } finally {
   1641             c.close();
   1642         }
   1643     }
   1644 
   1645     /**
   1646      * Loads name lookup rows for approximate name matching and updates match scores based on that
   1647      * data.
   1648      */
   1649     private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
   1650             ContactMatcher matcher) {
   1651         HashSet<String> firstLetters = new HashSet<String>();
   1652         for (int i = 0; i < candidates.mCount; i++) {
   1653             final NameMatchCandidate candidate = candidates.mList.get(i);
   1654             if (candidate.mName.length() >= 2) {
   1655                 String firstLetter = candidate.mName.substring(0, 2);
   1656                 if (!firstLetters.contains(firstLetter)) {
   1657                     firstLetters.add(firstLetter);
   1658                     final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
   1659                             + firstLetter + "*') AND "
   1660                             + "(" + NameLookupColumns.NAME_TYPE + " IN("
   1661                                     + NameLookupType.NAME_COLLATION_KEY + ","
   1662                                     + NameLookupType.EMAIL_BASED_NICKNAME + ","
   1663                                     + NameLookupType.NICKNAME + ")) AND "
   1664                             + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
   1665                     matchAllCandidates(db, selection, candidates, matcher,
   1666                             ContactMatcher.MATCHING_ALGORITHM_APPROXIMATE,
   1667                             String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT));
   1668                 }
   1669             }
   1670         }
   1671     }
   1672 
   1673     private interface ContactNameLookupQuery {
   1674         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
   1675 
   1676         String[] COLUMNS = new String[] {
   1677                 RawContacts.CONTACT_ID,
   1678                 NameLookupColumns.NORMALIZED_NAME,
   1679                 NameLookupColumns.NAME_TYPE
   1680         };
   1681 
   1682         int CONTACT_ID = 0;
   1683         int NORMALIZED_NAME = 1;
   1684         int NAME_TYPE = 2;
   1685     }
   1686 
   1687     /**
   1688      * Loads all candidate rows from the name lookup table and updates match scores based
   1689      * on that data.
   1690      */
   1691     private void matchAllCandidates(SQLiteDatabase db, String selection,
   1692             MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) {
   1693         final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
   1694                 selection, null, null, null, null, limit);
   1695 
   1696         try {
   1697             while (c.moveToNext()) {
   1698                 Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
   1699                 String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
   1700                 int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
   1701 
   1702                 // Note the N^2 complexity of the following fragment. This is not a huge concern
   1703                 // since the number of candidates is very small and in general secondary hits
   1704                 // in the absence of primary hits are rare.
   1705                 for (int i = 0; i < candidates.mCount; i++) {
   1706                     NameMatchCandidate candidate = candidates.mList.get(i);
   1707                     matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
   1708                             nameType, name, algorithm);
   1709                 }
   1710             }
   1711         } finally {
   1712             c.close();
   1713         }
   1714     }
   1715 
   1716     private interface RawContactsQuery {
   1717         String SQL_FORMAT =
   1718                 "SELECT "
   1719                         + RawContactsColumns.CONCRETE_ID + ","
   1720                         + RawContactsColumns.DISPLAY_NAME + ","
   1721                         + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
   1722                         + AccountsColumns.CONCRETE_ACCOUNT_TYPE + ","
   1723                         + AccountsColumns.CONCRETE_ACCOUNT_NAME + ","
   1724                         + AccountsColumns.CONCRETE_DATA_SET + ","
   1725                         + RawContacts.SOURCE_ID + ","
   1726                         + RawContacts.CUSTOM_RINGTONE + ","
   1727                         + RawContacts.SEND_TO_VOICEMAIL + ","
   1728                         + RawContacts.LAST_TIME_CONTACTED + ","
   1729                         + RawContacts.TIMES_CONTACTED + ","
   1730                         + RawContacts.STARRED + ","
   1731                         + RawContacts.PINNED + ","
   1732                         + RawContacts.NAME_VERIFIED + ","
   1733                         + DataColumns.CONCRETE_ID + ","
   1734                         + DataColumns.CONCRETE_MIMETYPE_ID + ","
   1735                         + Data.IS_SUPER_PRIMARY + ","
   1736                         + Photo.PHOTO_FILE_ID +
   1737                 " FROM " + Tables.RAW_CONTACTS +
   1738                 " JOIN " + Tables.ACCOUNTS + " ON ("
   1739                     + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
   1740                     + ")" +
   1741                 " LEFT OUTER JOIN " + Tables.DATA +
   1742                 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
   1743                         + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
   1744                                 + " AND " + Photo.PHOTO + " NOT NULL)"
   1745                         + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
   1746                                 + " AND " + Phone.NUMBER + " NOT NULL)))";
   1747 
   1748         String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
   1749                 " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
   1750 
   1751         String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
   1752                 " WHERE " + RawContacts.CONTACT_ID + "=?"
   1753                 + " AND " + RawContacts.DELETED + "=0";
   1754 
   1755         int RAW_CONTACT_ID = 0;
   1756         int DISPLAY_NAME = 1;
   1757         int DISPLAY_NAME_SOURCE = 2;
   1758         int ACCOUNT_TYPE = 3;
   1759         int ACCOUNT_NAME = 4;
   1760         int DATA_SET = 5;
   1761         int SOURCE_ID = 6;
   1762         int CUSTOM_RINGTONE = 7;
   1763         int SEND_TO_VOICEMAIL = 8;
   1764         int LAST_TIME_CONTACTED = 9;
   1765         int TIMES_CONTACTED = 10;
   1766         int STARRED = 11;
   1767         int PINNED = 12;
   1768         int NAME_VERIFIED = 13;
   1769         int DATA_ID = 14;
   1770         int MIMETYPE_ID = 15;
   1771         int IS_SUPER_PRIMARY = 16;
   1772         int PHOTO_FILE_ID = 17;
   1773     }
   1774 
   1775     private interface ContactReplaceSqlStatement {
   1776         String UPDATE_SQL =
   1777                 "UPDATE " + Tables.CONTACTS +
   1778                 " SET "
   1779                         + Contacts.NAME_RAW_CONTACT_ID + "=?, "
   1780                         + Contacts.PHOTO_ID + "=?, "
   1781                         + Contacts.PHOTO_FILE_ID + "=?, "
   1782                         + Contacts.SEND_TO_VOICEMAIL + "=?, "
   1783                         + Contacts.CUSTOM_RINGTONE + "=?, "
   1784                         + Contacts.LAST_TIME_CONTACTED + "=?, "
   1785                         + Contacts.TIMES_CONTACTED + "=?, "
   1786                         + Contacts.STARRED + "=?, "
   1787                         + Contacts.PINNED + "=?, "
   1788                         + Contacts.HAS_PHONE_NUMBER + "=?, "
   1789                         + Contacts.LOOKUP_KEY + "=?, "
   1790                         + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=? " +
   1791                 " WHERE " + Contacts._ID + "=?";
   1792 
   1793         String INSERT_SQL =
   1794                 "INSERT INTO " + Tables.CONTACTS + " ("
   1795                         + Contacts.NAME_RAW_CONTACT_ID + ", "
   1796                         + Contacts.PHOTO_ID + ", "
   1797                         + Contacts.PHOTO_FILE_ID + ", "
   1798                         + Contacts.SEND_TO_VOICEMAIL + ", "
   1799                         + Contacts.CUSTOM_RINGTONE + ", "
   1800                         + Contacts.LAST_TIME_CONTACTED + ", "
   1801                         + Contacts.TIMES_CONTACTED + ", "
   1802                         + Contacts.STARRED + ", "
   1803                         + Contacts.PINNED + ", "
   1804                         + Contacts.HAS_PHONE_NUMBER + ", "
   1805                         + Contacts.LOOKUP_KEY + ", "
   1806                         + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
   1807                         + ") " +
   1808                 " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
   1809 
   1810         int NAME_RAW_CONTACT_ID = 1;
   1811         int PHOTO_ID = 2;
   1812         int PHOTO_FILE_ID = 3;
   1813         int SEND_TO_VOICEMAIL = 4;
   1814         int CUSTOM_RINGTONE = 5;
   1815         int LAST_TIME_CONTACTED = 6;
   1816         int TIMES_CONTACTED = 7;
   1817         int STARRED = 8;
   1818         int PINNED = 9;
   1819         int HAS_PHONE_NUMBER = 10;
   1820         int LOOKUP_KEY = 11;
   1821         int CONTACT_LAST_UPDATED_TIMESTAMP = 12;
   1822         int CONTACT_ID = 13;
   1823     }
   1824 
   1825     /**
   1826      * Computes aggregate-level data for the specified aggregate contact ID.
   1827      */
   1828     private void computeAggregateData(SQLiteDatabase db, long contactId,
   1829             SQLiteStatement statement) {
   1830         mSelectionArgs1[0] = String.valueOf(contactId);
   1831         computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
   1832     }
   1833 
   1834     /**
   1835      * Indicates whether the given photo entry and priority gives this photo a higher overall
   1836      * priority than the current best photo entry and priority.
   1837      */
   1838     private boolean hasHigherPhotoPriority(PhotoEntry photoEntry, int priority,
   1839             PhotoEntry bestPhotoEntry, int bestPriority) {
   1840         int photoComparison = photoEntry.compareTo(bestPhotoEntry);
   1841         return photoComparison < 0 || photoComparison == 0 && priority > bestPriority;
   1842     }
   1843 
   1844     /**
   1845      * Computes aggregate-level data from constituent raw contacts.
   1846      */
   1847     private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
   1848             SQLiteStatement statement) {
   1849         long currentRawContactId = -1;
   1850         long bestPhotoId = -1;
   1851         long bestPhotoFileId = 0;
   1852         PhotoEntry bestPhotoEntry = null;
   1853         boolean foundSuperPrimaryPhoto = false;
   1854         int photoPriority = -1;
   1855         int totalRowCount = 0;
   1856         int contactSendToVoicemail = 0;
   1857         String contactCustomRingtone = null;
   1858         long contactLastTimeContacted = 0;
   1859         int contactTimesContacted = 0;
   1860         int contactStarred = 0;
   1861         int contactPinned = Integer.MAX_VALUE;
   1862         int hasPhoneNumber = 0;
   1863         StringBuilder lookupKey = new StringBuilder();
   1864 
   1865         mDisplayNameCandidate.clear();
   1866 
   1867         Cursor c = db.rawQuery(sql, sqlArgs);
   1868         try {
   1869             while (c.moveToNext()) {
   1870                 long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
   1871                 if (rawContactId != currentRawContactId) {
   1872                     currentRawContactId = rawContactId;
   1873                     totalRowCount++;
   1874 
   1875                     // Assemble sub-account.
   1876                     String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
   1877                     String dataSet = c.getString(RawContactsQuery.DATA_SET);
   1878                     String accountWithDataSet = (!TextUtils.isEmpty(dataSet))
   1879                             ? accountType + "/" + dataSet
   1880                             : accountType;
   1881 
   1882                     // Display name
   1883                     String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
   1884                     int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
   1885                     int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED);
   1886                     processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
   1887                             mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet),
   1888                             nameVerified != 0);
   1889 
   1890                     // Contact options
   1891                     if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
   1892                         boolean sendToVoicemail =
   1893                                 (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0);
   1894                         if (sendToVoicemail) {
   1895                             contactSendToVoicemail++;
   1896                         }
   1897                     }
   1898 
   1899                     if (contactCustomRingtone == null
   1900                             && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) {
   1901                         contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
   1902                     }
   1903 
   1904                     long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
   1905                     if (lastTimeContacted > contactLastTimeContacted) {
   1906                         contactLastTimeContacted = lastTimeContacted;
   1907                     }
   1908 
   1909                     int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
   1910                     if (timesContacted > contactTimesContacted) {
   1911                         contactTimesContacted = timesContacted;
   1912                     }
   1913 
   1914                     if (c.getInt(RawContactsQuery.STARRED) != 0) {
   1915                         contactStarred = 1;
   1916                     }
   1917 
   1918                     // contactPinned should be the lowest value of its constituent raw contacts,
   1919                     // excluding negative integers
   1920                     final int rawContactPinned = c.getInt(RawContactsQuery.PINNED);
   1921                     if (rawContactPinned > PinnedPositions.UNPINNED) {
   1922                         contactPinned = Math.min(contactPinned, rawContactPinned);
   1923                     }
   1924 
   1925                     appendLookupKey(
   1926                             lookupKey,
   1927                             accountWithDataSet,
   1928                             c.getString(RawContactsQuery.ACCOUNT_NAME),
   1929                             rawContactId,
   1930                             c.getString(RawContactsQuery.SOURCE_ID),
   1931                             displayName);
   1932                 }
   1933 
   1934                 if (!c.isNull(RawContactsQuery.DATA_ID)) {
   1935                     long dataId = c.getLong(RawContactsQuery.DATA_ID);
   1936                     long photoFileId = c.getLong(RawContactsQuery.PHOTO_FILE_ID);
   1937                     int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
   1938                     boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
   1939                     if (mimetypeId == mMimeTypeIdPhoto) {
   1940                         if (!foundSuperPrimaryPhoto) {
   1941                             // Lookup the metadata for the photo, if available.  Note that data set
   1942                             // does not come into play here, since accounts are looked up in the
   1943                             // account manager in the priority resolver.
   1944                             PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
   1945                             String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
   1946                             int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
   1947                             if (superPrimary || hasHigherPhotoPriority(
   1948                                     photoEntry, priority, bestPhotoEntry, photoPriority)) {
   1949                                 bestPhotoEntry = photoEntry;
   1950                                 photoPriority = priority;
   1951                                 bestPhotoId = dataId;
   1952                                 bestPhotoFileId = photoFileId;
   1953                                 foundSuperPrimaryPhoto |= superPrimary;
   1954                             }
   1955                         }
   1956                     } else if (mimetypeId == mMimeTypeIdPhone) {
   1957                         hasPhoneNumber = 1;
   1958                     }
   1959                 }
   1960             }
   1961         } finally {
   1962             c.close();
   1963         }
   1964 
   1965         if (contactPinned == Integer.MAX_VALUE) {
   1966             contactPinned = PinnedPositions.UNPINNED;
   1967         }
   1968 
   1969         statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID,
   1970                 mDisplayNameCandidate.rawContactId);
   1971 
   1972         if (bestPhotoId != -1) {
   1973             statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
   1974         } else {
   1975             statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
   1976         }
   1977 
   1978         if (bestPhotoFileId != 0) {
   1979             statement.bindLong(ContactReplaceSqlStatement.PHOTO_FILE_ID, bestPhotoFileId);
   1980         } else {
   1981             statement.bindNull(ContactReplaceSqlStatement.PHOTO_FILE_ID);
   1982         }
   1983 
   1984         statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
   1985                 totalRowCount == contactSendToVoicemail ? 1 : 0);
   1986         DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
   1987                 contactCustomRingtone);
   1988         statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
   1989                 contactLastTimeContacted);
   1990         statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
   1991                 contactTimesContacted);
   1992         statement.bindLong(ContactReplaceSqlStatement.STARRED,
   1993                 contactStarred);
   1994         statement.bindLong(ContactReplaceSqlStatement.PINNED,
   1995                 contactPinned);
   1996         statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
   1997                 hasPhoneNumber);
   1998         statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
   1999                 Uri.encode(lookupKey.toString()));
   2000         statement.bindLong(ContactReplaceSqlStatement.CONTACT_LAST_UPDATED_TIMESTAMP,
   2001                 Clock.getInstance().currentTimeMillis());
   2002     }
   2003 
   2004     /**
   2005      * Builds a lookup key using the given data.
   2006      */
   2007     protected void appendLookupKey(StringBuilder sb, String accountTypeWithDataSet,
   2008             String accountName, long rawContactId, String sourceId, String displayName) {
   2009         ContactLookupKey.appendToLookupKey(sb, accountTypeWithDataSet, accountName, rawContactId,
   2010                 sourceId, displayName);
   2011     }
   2012 
   2013     /**
   2014      * Uses the supplied values to determine if they represent a "better" display name
   2015      * for the aggregate contact currently evaluated.  If so, it updates
   2016      * {@link #mDisplayNameCandidate} with the new values.
   2017      */
   2018     private void processDisplayNameCandidate(long rawContactId, String displayName,
   2019             int displayNameSource, boolean writableAccount, boolean verified) {
   2020 
   2021         boolean replace = false;
   2022         if (mDisplayNameCandidate.rawContactId == -1) {
   2023             // No previous values available
   2024             replace = true;
   2025         } else if (!TextUtils.isEmpty(displayName)) {
   2026             if (!mDisplayNameCandidate.verified && verified) {
   2027                 // A verified name is better than any other name
   2028                 replace = true;
   2029             } else if (mDisplayNameCandidate.verified == verified) {
   2030                 if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
   2031                     // New values come from an superior source, e.g. structured name vs phone number
   2032                     replace = true;
   2033                 } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) {
   2034                     if (!mDisplayNameCandidate.writableAccount && writableAccount) {
   2035                         replace = true;
   2036                     } else if (mDisplayNameCandidate.writableAccount == writableAccount) {
   2037                         if (NameNormalizer.compareComplexity(displayName,
   2038                                 mDisplayNameCandidate.displayName) > 0) {
   2039                             // New name is more complex than the previously found one
   2040                             replace = true;
   2041                         }
   2042                     }
   2043                 }
   2044             }
   2045         }
   2046 
   2047         if (replace) {
   2048             mDisplayNameCandidate.rawContactId = rawContactId;
   2049             mDisplayNameCandidate.displayName = displayName;
   2050             mDisplayNameCandidate.displayNameSource = displayNameSource;
   2051             mDisplayNameCandidate.verified = verified;
   2052             mDisplayNameCandidate.writableAccount = writableAccount;
   2053         }
   2054     }
   2055 
   2056     private interface PhotoIdQuery {
   2057         final String[] COLUMNS = new String[] {
   2058             AccountsColumns.CONCRETE_ACCOUNT_TYPE,
   2059             DataColumns.CONCRETE_ID,
   2060             Data.IS_SUPER_PRIMARY,
   2061             Photo.PHOTO_FILE_ID,
   2062         };
   2063 
   2064         int ACCOUNT_TYPE = 0;
   2065         int DATA_ID = 1;
   2066         int IS_SUPER_PRIMARY = 2;
   2067         int PHOTO_FILE_ID = 3;
   2068     }
   2069 
   2070     public void updatePhotoId(SQLiteDatabase db, long rawContactId) {
   2071 
   2072         long contactId = mDbHelper.getContactId(rawContactId);
   2073         if (contactId == 0) {
   2074             return;
   2075         }
   2076 
   2077         long bestPhotoId = -1;
   2078         long bestPhotoFileId = 0;
   2079         int photoPriority = -1;
   2080 
   2081         long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
   2082 
   2083         String tables = Tables.RAW_CONTACTS
   2084                 + " JOIN " + Tables.ACCOUNTS + " ON ("
   2085                     + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
   2086                     + ")"
   2087                 + " JOIN " + Tables.DATA + " ON("
   2088                 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
   2089                 + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
   2090                         + Photo.PHOTO + " NOT NULL))";
   2091 
   2092         mSelectionArgs1[0] = String.valueOf(contactId);
   2093         final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
   2094                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
   2095         try {
   2096             PhotoEntry bestPhotoEntry = null;
   2097             while (c.moveToNext()) {
   2098                 long dataId = c.getLong(PhotoIdQuery.DATA_ID);
   2099                 long photoFileId = c.getLong(PhotoIdQuery.PHOTO_FILE_ID);
   2100                 boolean superPrimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0;
   2101                 PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
   2102 
   2103                 // Note that data set does not come into play here, since accounts are looked up in
   2104                 // the account manager in the priority resolver.
   2105                 String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE);
   2106                 int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
   2107                 if (superPrimary || hasHigherPhotoPriority(
   2108                         photoEntry, priority, bestPhotoEntry, photoPriority)) {
   2109                     bestPhotoEntry = photoEntry;
   2110                     photoPriority = priority;
   2111                     bestPhotoId = dataId;
   2112                     bestPhotoFileId = photoFileId;
   2113                     if (superPrimary) {
   2114                         break;
   2115                     }
   2116                 }
   2117             }
   2118         } finally {
   2119             c.close();
   2120         }
   2121 
   2122         if (bestPhotoId == -1) {
   2123             mPhotoIdUpdate.bindNull(1);
   2124         } else {
   2125             mPhotoIdUpdate.bindLong(1, bestPhotoId);
   2126         }
   2127 
   2128         if (bestPhotoFileId == 0) {
   2129             mPhotoIdUpdate.bindNull(2);
   2130         } else {
   2131             mPhotoIdUpdate.bindLong(2, bestPhotoFileId);
   2132         }
   2133 
   2134         mPhotoIdUpdate.bindLong(3, contactId);
   2135         mPhotoIdUpdate.execute();
   2136     }
   2137 
   2138     private interface PhotoFileQuery {
   2139         final String[] COLUMNS = new String[] {
   2140                 PhotoFiles.HEIGHT,
   2141                 PhotoFiles.WIDTH,
   2142                 PhotoFiles.FILESIZE
   2143         };
   2144 
   2145         int HEIGHT = 0;
   2146         int WIDTH = 1;
   2147         int FILESIZE = 2;
   2148     }
   2149 
   2150     private class PhotoEntry implements Comparable<PhotoEntry> {
   2151         // Pixel count (width * height) for the image.
   2152         final int pixelCount;
   2153 
   2154         // File size (in bytes) of the image.  Not populated if the image is a thumbnail.
   2155         final int fileSize;
   2156 
   2157         private PhotoEntry(int pixelCount, int fileSize) {
   2158             this.pixelCount = pixelCount;
   2159             this.fileSize = fileSize;
   2160         }
   2161 
   2162         @Override
   2163         public int compareTo(PhotoEntry pe) {
   2164             if (pe == null) {
   2165                 return -1;
   2166             }
   2167             if (pixelCount == pe.pixelCount) {
   2168                 return pe.fileSize - fileSize;
   2169             } else {
   2170                 return pe.pixelCount - pixelCount;
   2171             }
   2172         }
   2173     }
   2174 
   2175     private PhotoEntry getPhotoMetadata(SQLiteDatabase db, long photoFileId) {
   2176         if (photoFileId == 0) {
   2177             // Assume standard thumbnail size.  Don't bother getting a file size for priority;
   2178             // we should fall back to photo priority resolver if all we have are thumbnails.
   2179             int thumbDim = mContactsProvider.getMaxThumbnailDim();
   2180             return new PhotoEntry(thumbDim * thumbDim, 0);
   2181         } else {
   2182             Cursor c = db.query(Tables.PHOTO_FILES, PhotoFileQuery.COLUMNS, PhotoFiles._ID + "=?",
   2183                     new String[]{String.valueOf(photoFileId)}, null, null, null);
   2184             try {
   2185                 if (c.getCount() == 1) {
   2186                     c.moveToFirst();
   2187                     int pixelCount =
   2188                             c.getInt(PhotoFileQuery.HEIGHT) * c.getInt(PhotoFileQuery.WIDTH);
   2189                     return new PhotoEntry(pixelCount, c.getInt(PhotoFileQuery.FILESIZE));
   2190                 }
   2191             } finally {
   2192                 c.close();
   2193             }
   2194         }
   2195         return new PhotoEntry(0, 0);
   2196     }
   2197 
   2198     private interface DisplayNameQuery {
   2199         String[] COLUMNS = new String[] {
   2200             RawContacts._ID,
   2201             RawContactsColumns.DISPLAY_NAME,
   2202             RawContactsColumns.DISPLAY_NAME_SOURCE,
   2203             RawContacts.NAME_VERIFIED,
   2204             RawContacts.SOURCE_ID,
   2205             RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
   2206         };
   2207 
   2208         int _ID = 0;
   2209         int DISPLAY_NAME = 1;
   2210         int DISPLAY_NAME_SOURCE = 2;
   2211         int NAME_VERIFIED = 3;
   2212         int SOURCE_ID = 4;
   2213         int ACCOUNT_TYPE_AND_DATA_SET = 5;
   2214     }
   2215 
   2216     public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
   2217         long contactId = mDbHelper.getContactId(rawContactId);
   2218         if (contactId == 0) {
   2219             return;
   2220         }
   2221 
   2222         updateDisplayNameForContact(db, contactId);
   2223     }
   2224 
   2225     public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
   2226         boolean lookupKeyUpdateNeeded = false;
   2227 
   2228         mDisplayNameCandidate.clear();
   2229 
   2230         mSelectionArgs1[0] = String.valueOf(contactId);
   2231         final Cursor c = db.query(Views.RAW_CONTACTS, DisplayNameQuery.COLUMNS,
   2232                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
   2233         try {
   2234             while (c.moveToNext()) {
   2235                 long rawContactId = c.getLong(DisplayNameQuery._ID);
   2236                 String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
   2237                 int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
   2238                 int nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED);
   2239                 String accountTypeAndDataSet = c.getString(
   2240                         DisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET);
   2241                 processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
   2242                         mContactsProvider.isWritableAccountWithDataSet(accountTypeAndDataSet),
   2243                         nameVerified != 0);
   2244 
   2245                 // If the raw contact has no source id, the lookup key is based on the display
   2246                 // name, so the lookup key needs to be updated.
   2247                 lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID);
   2248             }
   2249         } finally {
   2250             c.close();
   2251         }
   2252 
   2253         if (mDisplayNameCandidate.rawContactId != -1) {
   2254             mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
   2255             mDisplayNameUpdate.bindLong(2, contactId);
   2256             mDisplayNameUpdate.execute();
   2257         }
   2258 
   2259         if (lookupKeyUpdateNeeded) {
   2260             updateLookupKeyForContact(db, contactId);
   2261         }
   2262     }
   2263 
   2264 
   2265     /**
   2266      * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the
   2267      * specified raw contact.
   2268      */
   2269     public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) {
   2270 
   2271         long contactId = mDbHelper.getContactId(rawContactId);
   2272         if (contactId == 0) {
   2273             return;
   2274         }
   2275 
   2276         final SQLiteStatement hasPhoneNumberUpdate = db.compileStatement(
   2277                 "UPDATE " + Tables.CONTACTS +
   2278                 " SET " + Contacts.HAS_PHONE_NUMBER + "="
   2279                         + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)"
   2280                         + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS
   2281                         + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
   2282                                 + " AND " + Phone.NUMBER + " NOT NULL"
   2283                                 + " AND " + RawContacts.CONTACT_ID + "=?)" +
   2284                 " WHERE " + Contacts._ID + "=?");
   2285         try {
   2286             hasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE));
   2287             hasPhoneNumberUpdate.bindLong(2, contactId);
   2288             hasPhoneNumberUpdate.bindLong(3, contactId);
   2289             hasPhoneNumberUpdate.execute();
   2290         } finally {
   2291             hasPhoneNumberUpdate.close();
   2292         }
   2293     }
   2294 
   2295     private interface LookupKeyQuery {
   2296         String TABLE = Views.RAW_CONTACTS;
   2297         String[] COLUMNS = new String[] {
   2298             RawContacts._ID,
   2299             RawContactsColumns.DISPLAY_NAME,
   2300             RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
   2301             RawContacts.ACCOUNT_NAME,
   2302             RawContacts.SOURCE_ID,
   2303         };
   2304 
   2305         int ID = 0;
   2306         int DISPLAY_NAME = 1;
   2307         int ACCOUNT_TYPE_AND_DATA_SET = 2;
   2308         int ACCOUNT_NAME = 3;
   2309         int SOURCE_ID = 4;
   2310     }
   2311 
   2312     public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
   2313         long contactId = mDbHelper.getContactId(rawContactId);
   2314         if (contactId == 0) {
   2315             return;
   2316         }
   2317 
   2318         updateLookupKeyForContact(db, contactId);
   2319     }
   2320 
   2321     private void updateLookupKeyForContact(SQLiteDatabase db, long contactId) {
   2322         String lookupKey = computeLookupKeyForContact(db, contactId);
   2323 
   2324         if (lookupKey == null) {
   2325             mLookupKeyUpdate.bindNull(1);
   2326         } else {
   2327             mLookupKeyUpdate.bindString(1, Uri.encode(lookupKey));
   2328         }
   2329         mLookupKeyUpdate.bindLong(2, contactId);
   2330 
   2331         mLookupKeyUpdate.execute();
   2332     }
   2333 
   2334     protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) {
   2335         StringBuilder sb = new StringBuilder();
   2336         mSelectionArgs1[0] = String.valueOf(contactId);
   2337         final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS,
   2338                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID);
   2339         try {
   2340             while (c.moveToNext()) {
   2341                 ContactLookupKey.appendToLookupKey(sb,
   2342                         c.getString(LookupKeyQuery.ACCOUNT_TYPE_AND_DATA_SET),
   2343                         c.getString(LookupKeyQuery.ACCOUNT_NAME),
   2344                         c.getLong(LookupKeyQuery.ID),
   2345                         c.getString(LookupKeyQuery.SOURCE_ID),
   2346                         c.getString(LookupKeyQuery.DISPLAY_NAME));
   2347             }
   2348         } finally {
   2349             c.close();
   2350         }
   2351         return sb.length() == 0 ? null : sb.toString();
   2352     }
   2353 
   2354     /**
   2355      * Execute {@link SQLiteStatement} that will update the
   2356      * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}.
   2357      */
   2358     public void updateStarred(long rawContactId) {
   2359         long contactId = mDbHelper.getContactId(rawContactId);
   2360         if (contactId == 0) {
   2361             return;
   2362         }
   2363 
   2364         mStarredUpdate.bindLong(1, contactId);
   2365         mStarredUpdate.execute();
   2366     }
   2367 
   2368     /**
   2369      * Execute {@link SQLiteStatement} that will update the
   2370      * {@link Contacts#PINNED} flag for the given {@link RawContacts#_ID}.
   2371      */
   2372     public void updatePinned(long rawContactId) {
   2373         long contactId = mDbHelper.getContactId(rawContactId);
   2374         if (contactId == 0) {
   2375             return;
   2376         }
   2377         mPinnedUpdate.bindLong(1, contactId);
   2378         mPinnedUpdate.execute();
   2379     }
   2380 
   2381     /**
   2382      * Finds matching contacts and returns a cursor on those.
   2383      */
   2384     public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb,
   2385             String[] projection, long contactId, int maxSuggestions, String filter,
   2386             ArrayList<AggregationSuggestionParameter> parameters) {
   2387         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
   2388         db.beginTransaction();
   2389         try {
   2390             List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters);
   2391             return queryMatchingContacts(qb, db, projection, bestMatches, maxSuggestions, filter);
   2392         } finally {
   2393             db.endTransaction();
   2394         }
   2395     }
   2396 
   2397     private interface ContactIdQuery {
   2398         String[] COLUMNS = new String[] {
   2399             Contacts._ID
   2400         };
   2401 
   2402         int _ID = 0;
   2403     }
   2404 
   2405     /**
   2406      * Loads contacts with specified IDs and returns them in the order of IDs in the
   2407      * supplied list.
   2408      */
   2409     private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db,
   2410             String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) {
   2411         StringBuilder sb = new StringBuilder();
   2412         sb.append(Contacts._ID);
   2413         sb.append(" IN (");
   2414         for (int i = 0; i < bestMatches.size(); i++) {
   2415             MatchScore matchScore = bestMatches.get(i);
   2416             if (i != 0) {
   2417                 sb.append(",");
   2418             }
   2419             sb.append(matchScore.getContactId());
   2420         }
   2421         sb.append(")");
   2422 
   2423         if (!TextUtils.isEmpty(filter)) {
   2424             sb.append(" AND " + Contacts._ID + " IN ");
   2425             mContactsProvider.appendContactFilterAsNestedQuery(sb, filter);
   2426         }
   2427 
   2428         // Run a query and find ids of best matching contacts satisfying the filter (if any)
   2429         HashSet<Long> foundIds = new HashSet<Long>();
   2430         Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(),
   2431                 null, null, null, null);
   2432         try {
   2433             while(cursor.moveToNext()) {
   2434                 foundIds.add(cursor.getLong(ContactIdQuery._ID));
   2435             }
   2436         } finally {
   2437             cursor.close();
   2438         }
   2439 
   2440         // Exclude all contacts that did not match the filter
   2441         Iterator<MatchScore> iter = bestMatches.iterator();
   2442         while (iter.hasNext()) {
   2443             long id = iter.next().getContactId();
   2444             if (!foundIds.contains(id)) {
   2445                 iter.remove();
   2446             }
   2447         }
   2448 
   2449         // Limit the number of returned suggestions
   2450         final List<MatchScore> limitedMatches;
   2451         if (bestMatches.size() > maxSuggestions) {
   2452             limitedMatches = bestMatches.subList(0, maxSuggestions);
   2453         } else {
   2454             limitedMatches = bestMatches;
   2455         }
   2456 
   2457         // Build an in-clause with the remaining contact IDs
   2458         sb.setLength(0);
   2459         sb.append(Contacts._ID);
   2460         sb.append(" IN (");
   2461         for (int i = 0; i < limitedMatches.size(); i++) {
   2462             MatchScore matchScore = limitedMatches.get(i);
   2463             if (i != 0) {
   2464                 sb.append(",");
   2465             }
   2466             sb.append(matchScore.getContactId());
   2467         }
   2468         sb.append(")");
   2469 
   2470         // Run the final query with the required projection and contact IDs found by the first query
   2471         cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID);
   2472 
   2473         // Build a sorted list of discovered IDs
   2474         ArrayList<Long> sortedContactIds = new ArrayList<Long>(limitedMatches.size());
   2475         for (MatchScore matchScore : limitedMatches) {
   2476             sortedContactIds.add(matchScore.getContactId());
   2477         }
   2478 
   2479         Collections.sort(sortedContactIds);
   2480 
   2481         // Map cursor indexes according to the descending order of match scores
   2482         int[] positionMap = new int[limitedMatches.size()];
   2483         for (int i = 0; i < positionMap.length; i++) {
   2484             long id = limitedMatches.get(i).getContactId();
   2485             positionMap[i] = sortedContactIds.indexOf(id);
   2486         }
   2487 
   2488         return new ReorderingCursorWrapper(cursor, positionMap);
   2489     }
   2490 
   2491     /**
   2492      * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
   2493      * descending order of match score.
   2494      * @param parameters
   2495      */
   2496     private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
   2497             ArrayList<AggregationSuggestionParameter> parameters) {
   2498 
   2499         MatchCandidateList candidates = new MatchCandidateList();
   2500         ContactMatcher matcher = new ContactMatcher();
   2501 
   2502         // Don't aggregate a contact with itself
   2503         matcher.keepOut(contactId);
   2504 
   2505         if (parameters == null || parameters.size() == 0) {
   2506             final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
   2507                     RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
   2508             try {
   2509                 while (c.moveToNext()) {
   2510                     long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID);
   2511                     updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
   2512                             matcher);
   2513                 }
   2514             } finally {
   2515                 c.close();
   2516             }
   2517         } else {
   2518             updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates,
   2519                     matcher, parameters);
   2520         }
   2521 
   2522         return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST);
   2523     }
   2524 
   2525     /**
   2526      * Computes scores for contacts that have matching data rows.
   2527      */
   2528     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
   2529             long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
   2530 
   2531         updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
   2532         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
   2533         updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
   2534         updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
   2535         loadNameMatchCandidates(db, rawContactId, candidates, false);
   2536         lookupApproximateNameMatches(db, candidates, matcher);
   2537     }
   2538 
   2539     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
   2540             MatchCandidateList candidates, ContactMatcher matcher,
   2541             ArrayList<AggregationSuggestionParameter> parameters) {
   2542         for (AggregationSuggestionParameter parameter : parameters) {
   2543             if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) {
   2544                 updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher);
   2545             }
   2546 
   2547             // TODO: add support for other parameter kinds
   2548         }
   2549     }
   2550 }
   2551