Home | History | Annotate | Download | only in contacts
      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;
     18 
     19 import com.android.providers.contacts.ContactMatcher.MatchScore;
     20 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
     21 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
     22 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
     23 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
     24 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
     25 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
     26 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
     27 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
     28 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     29 
     30 import android.content.ContentValues;
     31 import android.database.Cursor;
     32 import android.database.DatabaseUtils;
     33 import android.database.sqlite.SQLiteDatabase;
     34 import android.database.sqlite.SQLiteQueryBuilder;
     35 import android.database.sqlite.SQLiteStatement;
     36 import android.net.Uri;
     37 import android.provider.ContactsContract.AggregationExceptions;
     38 import android.provider.ContactsContract.Contacts;
     39 import android.provider.ContactsContract.Data;
     40 import android.provider.ContactsContract.DisplayNameSources;
     41 import android.provider.ContactsContract.RawContacts;
     42 import android.provider.ContactsContract.StatusUpdates;
     43 import android.provider.ContactsContract.CommonDataKinds.Email;
     44 import android.provider.ContactsContract.CommonDataKinds.Phone;
     45 import android.provider.ContactsContract.CommonDataKinds.Photo;
     46 import android.text.TextUtils;
     47 import android.util.EventLog;
     48 import android.util.Log;
     49 
     50 import java.util.ArrayList;
     51 import java.util.Collections;
     52 import java.util.HashMap;
     53 import java.util.HashSet;
     54 import java.util.Iterator;
     55 import java.util.List;
     56 
     57 
     58 /**
     59  * ContactAggregator deals with aggregating contact information coming from different sources.
     60  * Two John Doe contacts from two disjoint sources are presumed to be the same
     61  * person unless the user declares otherwise.
     62  */
     63 public class ContactAggregator {
     64 
     65     private static final String TAG = "ContactAggregator";
     66 
     67     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
     68 
     69     private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
     70             NameLookupColumns.NAME_TYPE + " IN ("
     71                     + NameLookupType.NAME_EXACT + ","
     72                     + NameLookupType.NAME_VARIANT + ","
     73                     + NameLookupType.NAME_COLLATION_KEY + ")";
     74 
     75     // From system/core/logcat/event-log-tags
     76     // aggregator [time, count] will be logged for each aggregator cycle.
     77     // For the query (as opposed to the merge), count will be negative
     78     public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747;
     79 
     80     // If we encounter more than this many contacts with matching names, aggregate only this many
     81     private static final int PRIMARY_HIT_LIMIT = 15;
     82     private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT);
     83 
     84     // If we encounter more than this many contacts with matching phone number or email,
     85     // don't attempt to aggregate - this is likely an error or a shared corporate data element.
     86     private static final int SECONDARY_HIT_LIMIT = 20;
     87     private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
     88 
     89     // If we encounter more than this many contacts with matching name during aggregation
     90     // suggestion lookup, ignore the remaining results.
     91     private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100;
     92 
     93     private final ContactsProvider2 mContactsProvider;
     94     private final ContactsDatabaseHelper mDbHelper;
     95     private PhotoPriorityResolver mPhotoPriorityResolver;
     96     private boolean mEnabled = true;
     97 
     98     /** Precompiled sql statement for setting an aggregated presence */
     99     private SQLiteStatement mAggregatedPresenceReplace;
    100     private SQLiteStatement mPresenceContactIdUpdate;
    101     private SQLiteStatement mRawContactCountQuery;
    102     private SQLiteStatement mContactDelete;
    103     private SQLiteStatement mAggregatedPresenceDelete;
    104     private SQLiteStatement mMarkForAggregation;
    105     private SQLiteStatement mPhotoIdUpdate;
    106     private SQLiteStatement mDisplayNameUpdate;
    107     private SQLiteStatement mHasPhoneNumberUpdate;
    108     private SQLiteStatement mLookupKeyUpdate;
    109     private SQLiteStatement mStarredUpdate;
    110     private SQLiteStatement mContactIdAndMarkAggregatedUpdate;
    111     private SQLiteStatement mContactIdUpdate;
    112     private SQLiteStatement mMarkAggregatedUpdate;
    113     private SQLiteStatement mContactUpdate;
    114     private SQLiteStatement mContactInsert;
    115 
    116     private HashMap<Long, Integer> mRawContactsMarkedForAggregation = new HashMap<Long, Integer>();
    117 
    118     private String[] mSelectionArgs1 = new String[1];
    119     private String[] mSelectionArgs2 = new String[2];
    120     private String[] mSelectionArgs3 = new String[3];
    121     private long mMimeTypeIdEmail;
    122     private long mMimeTypeIdPhoto;
    123     private long mMimeTypeIdPhone;
    124     private String mRawContactsQueryByRawContactId;
    125     private String mRawContactsQueryByContactId;
    126     private StringBuilder mSb = new StringBuilder();
    127     private MatchCandidateList mCandidates = new MatchCandidateList();
    128     private ContactMatcher mMatcher = new ContactMatcher();
    129     private ContentValues mValues = new ContentValues();
    130     private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
    131 
    132     /**
    133      * Captures a potential match for a given name. The matching algorithm
    134      * constructs a bunch of NameMatchCandidate objects for various potential matches
    135      * and then executes the search in bulk.
    136      */
    137     private static class NameMatchCandidate {
    138         String mName;
    139         int mLookupType;
    140 
    141         public NameMatchCandidate(String name, int nameLookupType) {
    142             mName = name;
    143             mLookupType = nameLookupType;
    144         }
    145     }
    146 
    147     /**
    148      * A list of {@link NameMatchCandidate} that keeps its elements even when the list is
    149      * truncated. This is done for optimization purposes to avoid excessive object allocation.
    150      */
    151     private static class MatchCandidateList {
    152         private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>();
    153         private int mCount;
    154 
    155         /**
    156          * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists.
    157          */
    158         public void add(String name, int nameLookupType) {
    159             if (mCount >= mList.size()) {
    160                 mList.add(new NameMatchCandidate(name, nameLookupType));
    161             } else {
    162                 NameMatchCandidate candidate = mList.get(mCount);
    163                 candidate.mName = name;
    164                 candidate.mLookupType = nameLookupType;
    165             }
    166             mCount++;
    167         }
    168 
    169         public void clear() {
    170             mCount = 0;
    171         }
    172     }
    173 
    174     /**
    175      * A convenience class used in the algorithm that figures out which of available
    176      * display names to use for an aggregate contact.
    177      */
    178     private static class DisplayNameCandidate {
    179         long rawContactId;
    180         String displayName;
    181         int displayNameSource;
    182         boolean verified;
    183         boolean writableAccount;
    184 
    185         public DisplayNameCandidate() {
    186             clear();
    187         }
    188 
    189         public void clear() {
    190             rawContactId = -1;
    191             displayName = null;
    192             displayNameSource = DisplayNameSources.UNDEFINED;
    193             verified = false;
    194             writableAccount = false;
    195         }
    196     }
    197 
    198     /**
    199      * Constructor.
    200      */
    201     public ContactAggregator(ContactsProvider2 contactsProvider,
    202             ContactsDatabaseHelper contactsDatabaseHelper,
    203             PhotoPriorityResolver photoPriorityResolver) {
    204         mContactsProvider = contactsProvider;
    205         mDbHelper = contactsDatabaseHelper;
    206         mPhotoPriorityResolver = photoPriorityResolver;
    207 
    208         SQLiteDatabase db = mDbHelper.getReadableDatabase();
    209 
    210         // Since we have no way of determining which custom status was set last,
    211         // we'll just pick one randomly.  We are using MAX as an approximation of randomness
    212         mAggregatedPresenceReplace = db.compileStatement(
    213                 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
    214                         + AggregatedPresenceColumns.CONTACT_ID + ", "
    215                         + StatusUpdates.PRESENCE_STATUS
    216                 + ") SELECT ?, MAX(" + StatusUpdates.PRESENCE_STATUS + ") "
    217                         + " FROM " + Tables.PRESENCE
    218                         + " WHERE " + PresenceColumns.CONTACT_ID + "=?");
    219 
    220         mRawContactCountQuery = db.compileStatement(
    221                 "SELECT COUNT(" + RawContacts._ID + ")" +
    222                 " FROM " + Tables.RAW_CONTACTS +
    223                 " WHERE " + RawContacts.CONTACT_ID + "=?"
    224                         + " AND " + RawContacts._ID + "<>?");
    225 
    226         mContactDelete = db.compileStatement(
    227                 "DELETE FROM " + Tables.CONTACTS +
    228                 " WHERE " + Contacts._ID + "=?");
    229 
    230         mAggregatedPresenceDelete = db.compileStatement(
    231                 "DELETE FROM " + Tables.AGGREGATED_PRESENCE +
    232                 " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?");
    233 
    234         mMarkForAggregation = db.compileStatement(
    235                 "UPDATE " + Tables.RAW_CONTACTS +
    236                 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
    237                 " WHERE " + RawContacts._ID + "=?"
    238                         + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0");
    239 
    240         mPhotoIdUpdate = db.compileStatement(
    241                 "UPDATE " + Tables.CONTACTS +
    242                 " SET " + Contacts.PHOTO_ID + "=? " +
    243                 " WHERE " + Contacts._ID + "=?");
    244 
    245         mDisplayNameUpdate = db.compileStatement(
    246                 "UPDATE " + Tables.CONTACTS +
    247                 " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
    248                 " WHERE " + Contacts._ID + "=?");
    249 
    250         mLookupKeyUpdate = db.compileStatement(
    251                 "UPDATE " + Tables.CONTACTS +
    252                 " SET " + Contacts.LOOKUP_KEY + "=? " +
    253                 " WHERE " + Contacts._ID + "=?");
    254 
    255         mHasPhoneNumberUpdate = db.compileStatement(
    256                 "UPDATE " + Tables.CONTACTS +
    257                 " SET " + Contacts.HAS_PHONE_NUMBER + "="
    258                         + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)"
    259                         + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS
    260                         + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
    261                                 + " AND " + Phone.NUMBER + " NOT NULL"
    262                                 + " AND " + RawContacts.CONTACT_ID + "=?)" +
    263                 " WHERE " + Contacts._ID + "=?");
    264 
    265         mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
    266                 + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED
    267                 + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE "
    268                 + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND "
    269                 + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?");
    270 
    271         mContactIdAndMarkAggregatedUpdate = db.compileStatement(
    272                 "UPDATE " + Tables.RAW_CONTACTS +
    273                 " SET " + RawContacts.CONTACT_ID + "=?, "
    274                         + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
    275                 " WHERE " + RawContacts._ID + "=?");
    276 
    277         mContactIdUpdate = db.compileStatement(
    278                 "UPDATE " + Tables.RAW_CONTACTS +
    279                 " SET " + RawContacts.CONTACT_ID + "=?" +
    280                 " WHERE " + RawContacts._ID + "=?");
    281 
    282         mMarkAggregatedUpdate = db.compileStatement(
    283                 "UPDATE " + Tables.RAW_CONTACTS +
    284                 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
    285                 " WHERE " + RawContacts._ID + "=?");
    286 
    287         mPresenceContactIdUpdate = db.compileStatement(
    288                 "UPDATE " + Tables.PRESENCE +
    289                 " SET " + PresenceColumns.CONTACT_ID + "=?" +
    290                 " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
    291 
    292         mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
    293         mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
    294 
    295         mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
    296         mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
    297         mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
    298 
    299         // Query used to retrieve data from raw contacts to populate the corresponding aggregate
    300         mRawContactsQueryByRawContactId = String.format(
    301                 RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
    302                 mMimeTypeIdPhoto, mMimeTypeIdPhone);
    303 
    304         mRawContactsQueryByContactId = String.format(
    305                 RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
    306                 mMimeTypeIdPhoto, mMimeTypeIdPhone);
    307     }
    308 
    309     public void setEnabled(boolean enabled) {
    310         mEnabled = enabled;
    311     }
    312 
    313     public boolean isEnabled() {
    314         return mEnabled;
    315     }
    316 
    317     private interface AggregationQuery {
    318         String SQL =
    319                 "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
    320                 " FROM " + Tables.RAW_CONTACTS +
    321                 " WHERE " + RawContacts._ID + " IN(";
    322 
    323         int _ID = 0;
    324         int CONTACT_ID = 1;
    325     }
    326 
    327     /**
    328      * Aggregate all raw contacts that were marked for aggregation in the current transaction.
    329      * Call just before committing the transaction.
    330      */
    331     public void aggregateInTransaction(SQLiteDatabase db) {
    332         int count = mRawContactsMarkedForAggregation.size();
    333         if (count == 0) {
    334             return;
    335         }
    336 
    337         long start = System.currentTimeMillis();
    338         if (VERBOSE_LOGGING) {
    339             Log.v(TAG, "Contact aggregation: " + count);
    340         }
    341 
    342         EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -count);
    343 
    344         String selectionArgs[] = new String[count];
    345 
    346         int index = 0;
    347         mSb.setLength(0);
    348         mSb.append(AggregationQuery.SQL);
    349         for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) {
    350             if (index > 0) {
    351                 mSb.append(',');
    352             }
    353             mSb.append('?');
    354             selectionArgs[index++] = String.valueOf(rawContactId);
    355         }
    356 
    357         mSb.append(')');
    358 
    359         long rawContactIds[] = new long[count];
    360         long contactIds[] = new long[count];
    361         Cursor c = db.rawQuery(mSb.toString(), selectionArgs);
    362         try {
    363             count = c.getCount();
    364             index = 0;
    365             while (c.moveToNext()) {
    366                 rawContactIds[index] = c.getLong(AggregationQuery._ID);
    367                 contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
    368                 index++;
    369             }
    370         } finally {
    371             c.close();
    372         }
    373 
    374         for (int i = 0; i < count; i++) {
    375             aggregateContact(db, rawContactIds[i], contactIds[i], mCandidates, mMatcher, mValues);
    376         }
    377 
    378         long elapsedTime = System.currentTimeMillis() - start;
    379         EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, count);
    380 
    381         if (VERBOSE_LOGGING) {
    382             String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact";
    383             Log.i(TAG, "Contact aggregation complete: " + count + performance);
    384         }
    385     }
    386 
    387     public void clearPendingAggregations() {
    388         mRawContactsMarkedForAggregation.clear();
    389     }
    390 
    391     public void markNewForAggregation(long rawContactId, int aggregationMode) {
    392         mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
    393     }
    394 
    395     public void markForAggregation(long rawContactId, int aggregationMode, boolean force) {
    396         if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) {
    397             // As per ContactsContract documentation, default aggregation mode
    398             // does not override a previously set mode
    399             if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
    400                 aggregationMode = mRawContactsMarkedForAggregation.get(rawContactId);
    401             }
    402         } else {
    403             mMarkForAggregation.bindLong(1, rawContactId);
    404             mMarkForAggregation.execute();
    405         }
    406 
    407         mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
    408     }
    409 
    410     /**
    411      * Creates a new contact based on the given raw contact.  Does not perform aggregation.
    412      */
    413     public void onRawContactInsert(SQLiteDatabase db, long rawContactId) {
    414         mSelectionArgs1[0] = String.valueOf(rawContactId);
    415         computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
    416         long contactId = mContactInsert.executeInsert();
    417         setContactId(rawContactId, contactId);
    418         mDbHelper.updateContactVisible(contactId);
    419     }
    420 
    421     /**
    422      * Synchronously aggregate the specified contact assuming an open transaction.
    423      */
    424     public void aggregateContact(SQLiteDatabase db, long rawContactId, long currentContactId) {
    425         if (!mEnabled) {
    426             return;
    427         }
    428 
    429         MatchCandidateList candidates = new MatchCandidateList();
    430         ContactMatcher matcher = new ContactMatcher();
    431         ContentValues values = new ContentValues();
    432 
    433         aggregateContact(db, rawContactId, currentContactId, candidates, matcher, values);
    434     }
    435 
    436     public void updateAggregateData(long contactId) {
    437         if (!mEnabled) {
    438             return;
    439         }
    440 
    441         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    442         computeAggregateData(db, contactId, mContactUpdate);
    443         mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
    444         mContactUpdate.execute();
    445 
    446         mDbHelper.updateContactVisible(contactId);
    447         updateAggregatedPresence(contactId);
    448     }
    449 
    450     private void updateAggregatedPresence(long contactId) {
    451         mAggregatedPresenceReplace.bindLong(1, contactId);
    452         mAggregatedPresenceReplace.bindLong(2, contactId);
    453         mAggregatedPresenceReplace.execute();
    454     }
    455 
    456     /**
    457      * Given a specific raw contact, finds all matching aggregate contacts and chooses the one
    458      * with the highest match score.  If no such contact is found, creates a new contact.
    459      */
    460     private synchronized void aggregateContact(SQLiteDatabase db, long rawContactId,
    461             long currentContactId, MatchCandidateList candidates, ContactMatcher matcher,
    462             ContentValues values) {
    463 
    464         int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
    465 
    466         Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId);
    467         if (aggModeObject != null) {
    468             aggregationMode = aggModeObject;
    469         }
    470 
    471         long contactId = -1;
    472 
    473         if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
    474             candidates.clear();
    475             matcher.clear();
    476 
    477             contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher);
    478             if (contactId == -1) {
    479                 contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher);
    480             }
    481         } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
    482             return;
    483         }
    484 
    485         long currentContactContentsCount = 0;
    486 
    487         if (currentContactId != 0) {
    488             mRawContactCountQuery.bindLong(1, currentContactId);
    489             mRawContactCountQuery.bindLong(2, rawContactId);
    490             currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong();
    491         }
    492 
    493         // If there are no other raw contacts in the current aggregate, we might as well reuse it.
    494         // Also, if the aggregation mode is SUSPENDED, we must reuse the same aggregate.
    495         if (contactId == -1
    496                 && currentContactId != 0
    497                 && (currentContactContentsCount == 0
    498                         || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) {
    499             contactId = currentContactId;
    500         }
    501 
    502         if (contactId == currentContactId) {
    503             // Aggregation unchanged
    504             markAggregated(rawContactId);
    505         } else if (contactId == -1) {
    506             // Splitting an aggregate
    507             mSelectionArgs1[0] = String.valueOf(rawContactId);
    508             computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
    509                     mContactInsert);
    510             contactId = mContactInsert.executeInsert();
    511             setContactIdAndMarkAggregated(rawContactId, contactId);
    512             mDbHelper.updateContactVisible(contactId);
    513 
    514             setPresenceContactId(rawContactId, contactId);
    515 
    516             updateAggregatedPresence(contactId);
    517 
    518             if (currentContactContentsCount > 0) {
    519                 updateAggregateData(currentContactId);
    520             }
    521         } else {
    522             // Joining with an existing aggregate
    523             if (currentContactContentsCount == 0) {
    524                 // Delete a previous aggregate if it only contained this raw contact
    525                 mContactDelete.bindLong(1, currentContactId);
    526                 mContactDelete.execute();
    527 
    528                 mAggregatedPresenceDelete.bindLong(1, currentContactId);
    529                 mAggregatedPresenceDelete.execute();
    530             }
    531 
    532             setContactIdAndMarkAggregated(rawContactId, contactId);
    533             computeAggregateData(db, contactId, mContactUpdate);
    534             mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
    535             mContactUpdate.execute();
    536             mDbHelper.updateContactVisible(contactId);
    537             updateAggregatedPresence(contactId);
    538         }
    539     }
    540 
    541     /**
    542      * Updates the contact ID for the specified contact.
    543      */
    544     private void setContactId(long rawContactId, long contactId) {
    545         mContactIdUpdate.bindLong(1, contactId);
    546         mContactIdUpdate.bindLong(2, rawContactId);
    547         mContactIdUpdate.execute();
    548     }
    549 
    550     /**
    551      * Marks the specified raw contact ID as aggregated
    552      */
    553     private void markAggregated(long rawContactId) {
    554         mMarkAggregatedUpdate.bindLong(1, rawContactId);
    555         mMarkAggregatedUpdate.execute();
    556     }
    557 
    558     /**
    559      * Updates the contact ID for the specified contact and marks the raw contact as aggregated.
    560      */
    561     private void setContactIdAndMarkAggregated(long rawContactId, long contactId) {
    562         mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId);
    563         mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId);
    564         mContactIdAndMarkAggregatedUpdate.execute();
    565     }
    566 
    567     private void setPresenceContactId(long rawContactId, long contactId) {
    568         mPresenceContactIdUpdate.bindLong(1, contactId);
    569         mPresenceContactIdUpdate.bindLong(2, rawContactId);
    570         mPresenceContactIdUpdate.execute();
    571     }
    572 
    573     interface AggregateExceptionPrefetchQuery {
    574         String TABLE = Tables.AGGREGATION_EXCEPTIONS;
    575 
    576         String[] COLUMNS = {
    577             AggregationExceptions.RAW_CONTACT_ID1,
    578             AggregationExceptions.RAW_CONTACT_ID2,
    579         };
    580 
    581         int RAW_CONTACT_ID1 = 0;
    582         int RAW_CONTACT_ID2 = 1;
    583     }
    584 
    585     // A set of raw contact IDs for which there are aggregation exceptions
    586     private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
    587     private boolean mAggregationExceptionIdsValid;
    588 
    589     public void invalidateAggregationExceptionCache() {
    590         mAggregationExceptionIdsValid = false;
    591     }
    592 
    593     /**
    594      * Finds all raw contact IDs for which there are aggregation exceptions. The list of
    595      * ids is used as an optimization in aggregation: there is no point to run a query against
    596      * the agg_exceptions table if it is known that there are no records there for a given
    597      * raw contact ID.
    598      */
    599     private void prefetchAggregationExceptionIds(SQLiteDatabase db) {
    600         mAggregationExceptionIds.clear();
    601         final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
    602                 AggregateExceptionPrefetchQuery.COLUMNS,
    603                 null, null, null, null, null);
    604 
    605         try {
    606             while (c.moveToNext()) {
    607                 long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
    608                 long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
    609                 mAggregationExceptionIds.add(rawContactId1);
    610                 mAggregationExceptionIds.add(rawContactId2);
    611             }
    612         } finally {
    613             c.close();
    614         }
    615 
    616         mAggregationExceptionIdsValid = true;
    617     }
    618 
    619     interface AggregateExceptionQuery {
    620         String TABLE = Tables.AGGREGATION_EXCEPTIONS
    621             + " JOIN raw_contacts raw_contacts1 "
    622                     + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) "
    623             + " JOIN raw_contacts raw_contacts2 "
    624                     + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) ";
    625 
    626         String[] COLUMNS = {
    627             AggregationExceptions.TYPE,
    628             AggregationExceptions.RAW_CONTACT_ID1,
    629             "raw_contacts1." + RawContacts.CONTACT_ID,
    630             "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED,
    631             "raw_contacts2." + RawContacts.CONTACT_ID,
    632             "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED,
    633         };
    634 
    635         int TYPE = 0;
    636         int RAW_CONTACT_ID1 = 1;
    637         int CONTACT_ID1 = 2;
    638         int AGGREGATION_NEEDED_1 = 3;
    639         int CONTACT_ID2 = 4;
    640         int AGGREGATION_NEEDED_2 = 5;
    641     }
    642 
    643     /**
    644      * Computes match scores based on exceptions entered by the user: always match and never match.
    645      * Returns the aggregate contact with the always match exception if any.
    646      */
    647     private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId,
    648             ContactMatcher matcher) {
    649         if (!mAggregationExceptionIdsValid) {
    650             prefetchAggregationExceptionIds(db);
    651         }
    652 
    653         // If there are no aggregation exceptions involving this raw contact, there is no need to
    654         // run a query and we can just return -1, which stands for "nothing found"
    655         if (!mAggregationExceptionIds.contains(rawContactId)) {
    656             return -1;
    657         }
    658 
    659         final Cursor c = db.query(AggregateExceptionQuery.TABLE,
    660                 AggregateExceptionQuery.COLUMNS,
    661                 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
    662                         + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId,
    663                 null, null, null, null);
    664 
    665         try {
    666             while (c.moveToNext()) {
    667                 int type = c.getInt(AggregateExceptionQuery.TYPE);
    668                 long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
    669                 long contactId = -1;
    670                 if (rawContactId == rawContactId1) {
    671                     if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0
    672                             && !c.isNull(AggregateExceptionQuery.CONTACT_ID2)) {
    673                         contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2);
    674                     }
    675                 } else {
    676                     if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0
    677                             && !c.isNull(AggregateExceptionQuery.CONTACT_ID1)) {
    678                         contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1);
    679                     }
    680                 }
    681                 if (contactId != -1) {
    682                     if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) {
    683                         matcher.keepIn(contactId);
    684                     } else {
    685                         matcher.keepOut(contactId);
    686                     }
    687                 }
    688             }
    689         } finally {
    690             c.close();
    691         }
    692 
    693         return matcher.pickBestMatch(ContactMatcher.MAX_SCORE);
    694     }
    695 
    696     /**
    697      * Picks the best matching contact based on matches between data elements.  It considers
    698      * name match to be primary and phone, email etc matches to be secondary.  A good primary
    699      * match triggers aggregation, while a good secondary match only triggers aggregation in
    700      * the absence of a strong primary mismatch.
    701      * <p>
    702      * Consider these examples:
    703      * <p>
    704      * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should
    705      * be aggregated (same number, similar names).
    706      * <p>
    707      * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should
    708      * not be aggregated (same number, different names).
    709      */
    710     private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
    711             MatchCandidateList candidates, ContactMatcher matcher) {
    712 
    713         // Find good matches based on name alone
    714         long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, candidates, matcher);
    715         if (bestMatch == -1) {
    716             // We haven't found a good match on name, see if we have any matches on phone, email etc
    717             bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher);
    718         }
    719 
    720         return bestMatch;
    721     }
    722 
    723 
    724     /**
    725      * Picks the best matching contact based on secondary data matches.  The method loads
    726      * structured names for all candidate contacts and recomputes match scores using approximate
    727      * matching.
    728      */
    729     private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db,
    730             long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
    731         List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates(
    732                 ContactMatcher.SCORE_THRESHOLD_PRIMARY);
    733         if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) {
    734             return -1;
    735         }
    736 
    737         loadNameMatchCandidates(db, rawContactId, candidates, true);
    738 
    739         mSb.setLength(0);
    740         mSb.append(RawContacts.CONTACT_ID).append(" IN (");
    741         for (int i = 0; i < secondaryContactIds.size(); i++) {
    742             if (i != 0) {
    743                 mSb.append(',');
    744             }
    745             mSb.append(secondaryContactIds.get(i));
    746         }
    747 
    748         // We only want to compare structured names to structured names
    749         // at this stage, we need to ignore all other sources of name lookup data.
    750         mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL);
    751 
    752         matchAllCandidates(db, mSb.toString(), candidates, matcher,
    753                 ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
    754 
    755         return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
    756     }
    757 
    758     private interface NameLookupQuery {
    759         String TABLE = Tables.NAME_LOOKUP;
    760 
    761         String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
    762         String SELECTION_STRUCTURED_NAME_BASED =
    763                 SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
    764 
    765         String[] COLUMNS = new String[] {
    766                 NameLookupColumns.NORMALIZED_NAME,
    767                 NameLookupColumns.NAME_TYPE
    768         };
    769 
    770         int NORMALIZED_NAME = 0;
    771         int NAME_TYPE = 1;
    772     }
    773 
    774     private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
    775             MatchCandidateList candidates, boolean structuredNameBased) {
    776         candidates.clear();
    777         mSelectionArgs1[0] = String.valueOf(rawContactId);
    778         Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
    779                 structuredNameBased
    780                         ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
    781                         : NameLookupQuery.SELECTION,
    782                 mSelectionArgs1, null, null, null);
    783         try {
    784             while (c.moveToNext()) {
    785                 String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
    786                 int type = c.getInt(NameLookupQuery.NAME_TYPE);
    787                 candidates.add(normalizedName, type);
    788             }
    789         } finally {
    790             c.close();
    791         }
    792     }
    793 
    794     /**
    795      * Computes scores for contacts that have matching data rows.
    796      */
    797     private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
    798             MatchCandidateList candidates, ContactMatcher matcher) {
    799 
    800         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
    801         long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
    802         if (bestMatch != -1) {
    803             return bestMatch;
    804         }
    805 
    806         updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
    807         updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
    808 
    809         return -1;
    810     }
    811 
    812     private interface NameLookupMatchQuery {
    813         String TABLE = Tables.NAME_LOOKUP + " nameA"
    814                 + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
    815                 " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
    816                         + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
    817                 + " JOIN " + Tables.RAW_CONTACTS +
    818                 " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
    819                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
    820 
    821         String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
    822                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
    823 
    824         String[] COLUMNS = new String[] {
    825             RawContacts.CONTACT_ID,
    826             "nameA." + NameLookupColumns.NORMALIZED_NAME,
    827             "nameA." + NameLookupColumns.NAME_TYPE,
    828             "nameB." + NameLookupColumns.NAME_TYPE,
    829         };
    830 
    831         int CONTACT_ID = 0;
    832         int NAME = 1;
    833         int NAME_TYPE_A = 2;
    834         int NAME_TYPE_B = 3;
    835     }
    836 
    837     private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
    838             ContactMatcher matcher) {
    839         mSelectionArgs1[0] = String.valueOf(rawContactId);
    840         Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
    841                 NameLookupMatchQuery.SELECTION,
    842                 mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
    843         try {
    844             while (c.moveToNext()) {
    845                 long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
    846                 String name = c.getString(NameLookupMatchQuery.NAME);
    847                 int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
    848                 int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
    849                 matcher.matchName(contactId, nameTypeA, name,
    850                         nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT);
    851                 if (nameTypeA == NameLookupType.NICKNAME &&
    852                         nameTypeB == NameLookupType.NICKNAME) {
    853                     matcher.updateScoreWithNicknameMatch(contactId);
    854                 }
    855             }
    856         } finally {
    857             c.close();
    858         }
    859     }
    860 
    861     private interface EmailLookupQuery {
    862         String TABLE = Tables.DATA + " dataA"
    863                 + " JOIN " + Tables.DATA + " dataB" +
    864                 " ON (" + "dataA." + Email.DATA + "=dataB." + Email.DATA + ")"
    865                 + " JOIN " + Tables.RAW_CONTACTS +
    866                 " ON (dataB." + Data.RAW_CONTACT_ID + " = "
    867                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
    868 
    869         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
    870                 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?"
    871                 + " AND dataA." + Email.DATA + " NOT NULL"
    872                 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?"
    873                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
    874 
    875         String[] COLUMNS = new String[] {
    876             RawContacts.CONTACT_ID
    877         };
    878 
    879         int CONTACT_ID = 0;
    880     }
    881 
    882     private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
    883             ContactMatcher matcher) {
    884         mSelectionArgs3[0] = String.valueOf(rawContactId);
    885         mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdEmail);
    886         Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
    887                 EmailLookupQuery.SELECTION,
    888                 mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING);
    889         try {
    890             while (c.moveToNext()) {
    891                 long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
    892                 matcher.updateScoreWithEmailMatch(contactId);
    893             }
    894         } finally {
    895             c.close();
    896         }
    897     }
    898 
    899     private interface PhoneLookupQuery {
    900         String TABLE = Tables.PHONE_LOOKUP + " phoneA"
    901                 + " JOIN " + Tables.DATA + " dataA"
    902                 + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
    903                 + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
    904                 + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
    905                         + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
    906                 + " JOIN " + Tables.DATA + " dataB"
    907                 + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
    908                 + " JOIN " + Tables.RAW_CONTACTS
    909                 + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
    910                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
    911 
    912         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
    913                 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
    914                         + "dataB." + Phone.NUMBER + ",?)"
    915                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
    916 
    917         String[] COLUMNS = new String[] {
    918             RawContacts.CONTACT_ID
    919         };
    920 
    921         int CONTACT_ID = 0;
    922     }
    923 
    924     private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
    925             ContactMatcher matcher) {
    926         mSelectionArgs2[0] = String.valueOf(rawContactId);
    927         mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
    928         Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
    929                 PhoneLookupQuery.SELECTION,
    930                 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
    931         try {
    932             while (c.moveToNext()) {
    933                 long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
    934                 matcher.updateScoreWithPhoneNumberMatch(contactId);
    935             }
    936         } finally {
    937             c.close();
    938         }
    939 
    940     }
    941 
    942     /**
    943      * Loads name lookup rows for approximate name matching and updates match scores based on that
    944      * data.
    945      */
    946     private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
    947             ContactMatcher matcher) {
    948         HashSet<String> firstLetters = new HashSet<String>();
    949         for (int i = 0; i < candidates.mCount; i++) {
    950             final NameMatchCandidate candidate = candidates.mList.get(i);
    951             if (candidate.mName.length() >= 2) {
    952                 String firstLetter = candidate.mName.substring(0, 2);
    953                 if (!firstLetters.contains(firstLetter)) {
    954                     firstLetters.add(firstLetter);
    955                     final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
    956                             + firstLetter + "*') AND "
    957                             + NameLookupColumns.NAME_TYPE + " IN("
    958                                     + NameLookupType.NAME_COLLATION_KEY + ","
    959                                     + NameLookupType.EMAIL_BASED_NICKNAME + ","
    960                                     + NameLookupType.NICKNAME + ")";
    961                     matchAllCandidates(db, selection, candidates, matcher,
    962                             ContactMatcher.MATCHING_ALGORITHM_APPROXIMATE,
    963                             String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT));
    964                 }
    965             }
    966         }
    967     }
    968 
    969     private interface ContactNameLookupQuery {
    970         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
    971 
    972         String[] COLUMNS = new String[] {
    973                 RawContacts.CONTACT_ID,
    974                 NameLookupColumns.NORMALIZED_NAME,
    975                 NameLookupColumns.NAME_TYPE
    976         };
    977 
    978         int CONTACT_ID = 0;
    979         int NORMALIZED_NAME = 1;
    980         int NAME_TYPE = 2;
    981     }
    982 
    983     /**
    984      * Loads all candidate rows from the name lookup table and updates match scores based
    985      * on that data.
    986      */
    987     private void matchAllCandidates(SQLiteDatabase db, String selection,
    988             MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) {
    989         final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
    990                 selection, null, null, null, null, limit);
    991 
    992         try {
    993             while (c.moveToNext()) {
    994                 Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
    995                 String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
    996                 int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
    997 
    998                 // Note the N^2 complexity of the following fragment. This is not a huge concern
    999                 // since the number of candidates is very small and in general secondary hits
   1000                 // in the absence of primary hits are rare.
   1001                 for (int i = 0; i < candidates.mCount; i++) {
   1002                     NameMatchCandidate candidate = candidates.mList.get(i);
   1003                     matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
   1004                             nameType, name, algorithm);
   1005                 }
   1006             }
   1007         } finally {
   1008             c.close();
   1009         }
   1010     }
   1011 
   1012     private interface RawContactsQuery {
   1013         String SQL_FORMAT =
   1014                 "SELECT "
   1015                         + RawContactsColumns.CONCRETE_ID + ","
   1016                         + RawContactsColumns.DISPLAY_NAME + ","
   1017                         + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
   1018                         + RawContacts.ACCOUNT_TYPE + ","
   1019                         + RawContacts.ACCOUNT_NAME + ","
   1020                         + RawContacts.SOURCE_ID + ","
   1021                         + RawContacts.CUSTOM_RINGTONE + ","
   1022                         + RawContacts.SEND_TO_VOICEMAIL + ","
   1023                         + RawContacts.LAST_TIME_CONTACTED + ","
   1024                         + RawContacts.TIMES_CONTACTED + ","
   1025                         + RawContacts.STARRED + ","
   1026                         + RawContacts.IS_RESTRICTED + ","
   1027                         + RawContacts.NAME_VERIFIED + ","
   1028                         + DataColumns.CONCRETE_ID + ","
   1029                         + DataColumns.CONCRETE_MIMETYPE_ID + ","
   1030                         + Data.IS_SUPER_PRIMARY +
   1031                 " FROM " + Tables.RAW_CONTACTS +
   1032                 " LEFT OUTER JOIN " + Tables.DATA +
   1033                 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
   1034                         + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
   1035                                 + " AND " + Photo.PHOTO + " NOT NULL)"
   1036                         + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
   1037                                 + " AND " + Phone.NUMBER + " NOT NULL)))";
   1038 
   1039         String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
   1040                 " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
   1041 
   1042         String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
   1043                 " WHERE " + RawContacts.CONTACT_ID + "=?"
   1044                 + " AND " + RawContacts.DELETED + "=0";
   1045 
   1046         int RAW_CONTACT_ID = 0;
   1047         int DISPLAY_NAME = 1;
   1048         int DISPLAY_NAME_SOURCE = 2;
   1049         int ACCOUNT_TYPE = 3;
   1050         int ACCOUNT_NAME = 4;
   1051         int SOURCE_ID = 5;
   1052         int CUSTOM_RINGTONE = 6;
   1053         int SEND_TO_VOICEMAIL = 7;
   1054         int LAST_TIME_CONTACTED = 8;
   1055         int TIMES_CONTACTED = 9;
   1056         int STARRED = 10;
   1057         int IS_RESTRICTED = 11;
   1058         int NAME_VERIFIED = 12;
   1059         int DATA_ID = 13;
   1060         int MIMETYPE_ID = 14;
   1061         int IS_SUPER_PRIMARY = 15;
   1062     }
   1063 
   1064     private interface ContactReplaceSqlStatement {
   1065         String UPDATE_SQL =
   1066                 "UPDATE " + Tables.CONTACTS +
   1067                 " SET "
   1068                         + Contacts.NAME_RAW_CONTACT_ID + "=?, "
   1069                         + Contacts.PHOTO_ID + "=?, "
   1070                         + Contacts.SEND_TO_VOICEMAIL + "=?, "
   1071                         + Contacts.CUSTOM_RINGTONE + "=?, "
   1072                         + Contacts.LAST_TIME_CONTACTED + "=?, "
   1073                         + Contacts.TIMES_CONTACTED + "=?, "
   1074                         + Contacts.STARRED + "=?, "
   1075                         + Contacts.HAS_PHONE_NUMBER + "=?, "
   1076                         + ContactsColumns.SINGLE_IS_RESTRICTED + "=?, "
   1077                         + Contacts.LOOKUP_KEY + "=? " +
   1078                 " WHERE " + Contacts._ID + "=?";
   1079 
   1080         String INSERT_SQL =
   1081                 "INSERT INTO " + Tables.CONTACTS + " ("
   1082                         + Contacts.NAME_RAW_CONTACT_ID + ", "
   1083                         + Contacts.PHOTO_ID + ", "
   1084                         + Contacts.SEND_TO_VOICEMAIL + ", "
   1085                         + Contacts.CUSTOM_RINGTONE + ", "
   1086                         + Contacts.LAST_TIME_CONTACTED + ", "
   1087                         + Contacts.TIMES_CONTACTED + ", "
   1088                         + Contacts.STARRED + ", "
   1089                         + Contacts.HAS_PHONE_NUMBER + ", "
   1090                         + ContactsColumns.SINGLE_IS_RESTRICTED + ", "
   1091                         + Contacts.LOOKUP_KEY + ", "
   1092                         + Contacts.IN_VISIBLE_GROUP + ") " +
   1093                 " VALUES (?,?,?,?,?,?,?,?,?,?,0)";
   1094 
   1095         int NAME_RAW_CONTACT_ID = 1;
   1096         int PHOTO_ID = 2;
   1097         int SEND_TO_VOICEMAIL = 3;
   1098         int CUSTOM_RINGTONE = 4;
   1099         int LAST_TIME_CONTACTED = 5;
   1100         int TIMES_CONTACTED = 6;
   1101         int STARRED = 7;
   1102         int HAS_PHONE_NUMBER = 8;
   1103         int SINGLE_IS_RESTRICTED = 9;
   1104         int LOOKUP_KEY = 10;
   1105         int CONTACT_ID = 11;
   1106     }
   1107 
   1108     /**
   1109      * Computes aggregate-level data for the specified aggregate contact ID.
   1110      */
   1111     private void computeAggregateData(SQLiteDatabase db, long contactId,
   1112             SQLiteStatement statement) {
   1113         mSelectionArgs1[0] = String.valueOf(contactId);
   1114         computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
   1115     }
   1116 
   1117     /**
   1118      * Computes aggregate-level data from constituent raw contacts.
   1119      */
   1120     private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
   1121             SQLiteStatement statement) {
   1122         long currentRawContactId = -1;
   1123         long bestPhotoId = -1;
   1124         boolean foundSuperPrimaryPhoto = false;
   1125         int photoPriority = -1;
   1126         int totalRowCount = 0;
   1127         int contactSendToVoicemail = 0;
   1128         String contactCustomRingtone = null;
   1129         long contactLastTimeContacted = 0;
   1130         int contactTimesContacted = 0;
   1131         int contactStarred = 0;
   1132         int singleIsRestricted = 1;
   1133         int hasPhoneNumber = 0;
   1134 
   1135         mDisplayNameCandidate.clear();
   1136 
   1137         mSb.setLength(0);       // Lookup key
   1138         Cursor c = db.rawQuery(sql, sqlArgs);
   1139         try {
   1140             while (c.moveToNext()) {
   1141                 long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
   1142                 if (rawContactId != currentRawContactId) {
   1143                     currentRawContactId = rawContactId;
   1144                     totalRowCount++;
   1145 
   1146                     // Display name
   1147                     String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
   1148                     int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
   1149                     int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED);
   1150                     String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
   1151                     processDisplayNameCanditate(rawContactId, displayName, displayNameSource,
   1152                             mContactsProvider.isWritableAccount(accountType), nameVerified != 0);
   1153 
   1154 
   1155                     // Contact options
   1156                     if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
   1157                         boolean sendToVoicemail =
   1158                                 (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0);
   1159                         if (sendToVoicemail) {
   1160                             contactSendToVoicemail++;
   1161                         }
   1162                     }
   1163 
   1164                     if (contactCustomRingtone == null
   1165                             && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) {
   1166                         contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
   1167                     }
   1168 
   1169                     long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
   1170                     if (lastTimeContacted > contactLastTimeContacted) {
   1171                         contactLastTimeContacted = lastTimeContacted;
   1172                     }
   1173 
   1174                     int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
   1175                     if (timesContacted > contactTimesContacted) {
   1176                         contactTimesContacted = timesContacted;
   1177                     }
   1178 
   1179                     if (c.getInt(RawContactsQuery.STARRED) != 0) {
   1180                         contactStarred = 1;
   1181                     }
   1182 
   1183                     // Single restricted
   1184                     if (totalRowCount > 1) {
   1185                         // Not single
   1186                         singleIsRestricted = 0;
   1187                     } else {
   1188                         int isRestricted = c.getInt(RawContactsQuery.IS_RESTRICTED);
   1189 
   1190                         if (isRestricted == 0) {
   1191                             // Not restricted
   1192                             singleIsRestricted = 0;
   1193                         }
   1194                     }
   1195 
   1196                     ContactLookupKey.appendToLookupKey(mSb,
   1197                             c.getString(RawContactsQuery.ACCOUNT_TYPE),
   1198                             c.getString(RawContactsQuery.ACCOUNT_NAME),
   1199                             rawContactId,
   1200                             c.getString(RawContactsQuery.SOURCE_ID),
   1201                             displayName);
   1202                 }
   1203 
   1204                 if (!c.isNull(RawContactsQuery.DATA_ID)) {
   1205                     long dataId = c.getLong(RawContactsQuery.DATA_ID);
   1206                     int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
   1207                     boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
   1208                     if (mimetypeId == mMimeTypeIdPhoto) {
   1209                         if (!foundSuperPrimaryPhoto) {
   1210                             String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
   1211                             int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
   1212                             if (superPrimary || priority > photoPriority) {
   1213                                 photoPriority = priority;
   1214                                 bestPhotoId = dataId;
   1215                                 foundSuperPrimaryPhoto |= superPrimary;
   1216                             }
   1217                         }
   1218                     } else if (mimetypeId == mMimeTypeIdPhone) {
   1219                         hasPhoneNumber = 1;
   1220                     }
   1221                 }
   1222             }
   1223         } finally {
   1224             c.close();
   1225         }
   1226 
   1227         statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID,
   1228                 mDisplayNameCandidate.rawContactId);
   1229 
   1230         if (bestPhotoId != -1) {
   1231             statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
   1232         } else {
   1233             statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
   1234         }
   1235 
   1236         statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
   1237                 totalRowCount == contactSendToVoicemail ? 1 : 0);
   1238         DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
   1239                 contactCustomRingtone);
   1240         statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
   1241                 contactLastTimeContacted);
   1242         statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
   1243                 contactTimesContacted);
   1244         statement.bindLong(ContactReplaceSqlStatement.STARRED,
   1245                 contactStarred);
   1246         statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
   1247                 hasPhoneNumber);
   1248         statement.bindLong(ContactReplaceSqlStatement.SINGLE_IS_RESTRICTED,
   1249                 singleIsRestricted);
   1250         statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
   1251                 Uri.encode(mSb.toString()));
   1252     }
   1253 
   1254     /**
   1255      * Uses the supplied values to determine if they represent a "better" display name
   1256      * for the aggregate contact currently evaluated.  If so, it updates
   1257      * {@link #mDisplayNameCandidate} with the new values.
   1258      */
   1259     private void processDisplayNameCanditate(long rawContactId, String displayName,
   1260             int displayNameSource, boolean writableAccount, boolean verified) {
   1261 
   1262         boolean replace = false;
   1263         if (mDisplayNameCandidate.rawContactId == -1) {
   1264             // No previous values available
   1265             replace = true;
   1266         } else if (!TextUtils.isEmpty(displayName)) {
   1267             if (!mDisplayNameCandidate.verified && verified) {
   1268                 // A verified name is better than any other name
   1269                 replace = true;
   1270             } else if (mDisplayNameCandidate.verified == verified) {
   1271                 if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
   1272                     // New values come from an superior source, e.g. structured name vs phone number
   1273                     replace = true;
   1274                 } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) {
   1275                     if (!mDisplayNameCandidate.writableAccount && writableAccount) {
   1276                         replace = true;
   1277                     } else if (mDisplayNameCandidate.writableAccount == writableAccount) {
   1278                         if (NameNormalizer.compareComplexity(displayName,
   1279                                 mDisplayNameCandidate.displayName) > 0) {
   1280                             // New name is more complex than the previously found one
   1281                             replace = true;
   1282                         }
   1283                     }
   1284                 }
   1285             }
   1286         }
   1287 
   1288         if (replace) {
   1289             mDisplayNameCandidate.rawContactId = rawContactId;
   1290             mDisplayNameCandidate.displayName = displayName;
   1291             mDisplayNameCandidate.displayNameSource = displayNameSource;
   1292             mDisplayNameCandidate.verified = verified;
   1293             mDisplayNameCandidate.writableAccount = writableAccount;
   1294         }
   1295     }
   1296 
   1297     private interface PhotoIdQuery {
   1298         String[] COLUMNS = new String[] {
   1299             RawContacts.ACCOUNT_TYPE,
   1300             DataColumns.CONCRETE_ID,
   1301             Data.IS_SUPER_PRIMARY,
   1302         };
   1303 
   1304         int ACCOUNT_TYPE = 0;
   1305         int DATA_ID = 1;
   1306         int IS_SUPER_PRIMARY = 2;
   1307     }
   1308 
   1309     public void updatePhotoId(SQLiteDatabase db, long rawContactId) {
   1310 
   1311         long contactId = mDbHelper.getContactId(rawContactId);
   1312         if (contactId == 0) {
   1313             return;
   1314         }
   1315 
   1316         long bestPhotoId = -1;
   1317         int photoPriority = -1;
   1318 
   1319         long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
   1320 
   1321         String tables = Tables.RAW_CONTACTS + " JOIN " + Tables.DATA + " ON("
   1322                 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
   1323                 + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
   1324                         + Photo.PHOTO + " NOT NULL))";
   1325 
   1326         mSelectionArgs1[0] = String.valueOf(contactId);
   1327         final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
   1328                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
   1329         try {
   1330             while (c.moveToNext()) {
   1331                 long dataId = c.getLong(PhotoIdQuery.DATA_ID);
   1332                 boolean superprimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0;
   1333                 if (superprimary) {
   1334                     bestPhotoId = dataId;
   1335                     break;
   1336                 }
   1337 
   1338                 String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE);
   1339                 int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
   1340                 if (priority > photoPriority) {
   1341                     photoPriority = priority;
   1342                     bestPhotoId = dataId;
   1343                 }
   1344             }
   1345         } finally {
   1346             c.close();
   1347         }
   1348 
   1349         if (bestPhotoId == -1) {
   1350             mPhotoIdUpdate.bindNull(1);
   1351         } else {
   1352             mPhotoIdUpdate.bindLong(1, bestPhotoId);
   1353         }
   1354         mPhotoIdUpdate.bindLong(2, contactId);
   1355         mPhotoIdUpdate.execute();
   1356     }
   1357 
   1358     private interface DisplayNameQuery {
   1359         String[] COLUMNS = new String[] {
   1360             RawContacts._ID,
   1361             RawContactsColumns.DISPLAY_NAME,
   1362             RawContactsColumns.DISPLAY_NAME_SOURCE,
   1363             RawContacts.NAME_VERIFIED,
   1364             RawContacts.SOURCE_ID,
   1365             RawContacts.ACCOUNT_TYPE,
   1366         };
   1367 
   1368         int _ID = 0;
   1369         int DISPLAY_NAME = 1;
   1370         int DISPLAY_NAME_SOURCE = 2;
   1371         int NAME_VERIFIED = 3;
   1372         int SOURCE_ID = 4;
   1373         int ACCOUNT_TYPE = 5;
   1374     }
   1375 
   1376     public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
   1377         long contactId = mDbHelper.getContactId(rawContactId);
   1378         if (contactId == 0) {
   1379             return;
   1380         }
   1381 
   1382         updateDisplayNameForContact(db, contactId);
   1383     }
   1384 
   1385     public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
   1386         boolean lookupKeyUpdateNeeded = false;
   1387 
   1388         mDisplayNameCandidate.clear();
   1389 
   1390         mSelectionArgs1[0] = String.valueOf(contactId);
   1391         final Cursor c = db.query(Tables.RAW_CONTACTS, DisplayNameQuery.COLUMNS,
   1392                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
   1393         try {
   1394             while (c.moveToNext()) {
   1395                 long rawContactId = c.getLong(DisplayNameQuery._ID);
   1396                 String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
   1397                 int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
   1398                 int nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED);
   1399                 String accountType = c.getString(DisplayNameQuery.ACCOUNT_TYPE);
   1400 
   1401                 processDisplayNameCanditate(rawContactId, displayName, displayNameSource,
   1402                         mContactsProvider.isWritableAccount(accountType), nameVerified != 0);
   1403 
   1404                 // If the raw contact has no source id, the lookup key is based on the display
   1405                 // name, so the lookup key needs to be updated.
   1406                 lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID);
   1407             }
   1408         } finally {
   1409             c.close();
   1410         }
   1411 
   1412         if (mDisplayNameCandidate.rawContactId != -1) {
   1413             mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
   1414             mDisplayNameUpdate.bindLong(2, contactId);
   1415             mDisplayNameUpdate.execute();
   1416         }
   1417 
   1418         if (lookupKeyUpdateNeeded) {
   1419             updateLookupKeyForContact(db, contactId);
   1420         }
   1421     }
   1422 
   1423     /**
   1424      * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the
   1425      * specified raw contact.
   1426      */
   1427     public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) {
   1428 
   1429         long contactId = mDbHelper.getContactId(rawContactId);
   1430         if (contactId == 0) {
   1431             return;
   1432         }
   1433 
   1434         mHasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE));
   1435         mHasPhoneNumberUpdate.bindLong(2, contactId);
   1436         mHasPhoneNumberUpdate.bindLong(3, contactId);
   1437         mHasPhoneNumberUpdate.execute();
   1438     }
   1439 
   1440     private interface LookupKeyQuery {
   1441         String[] COLUMNS = new String[] {
   1442             RawContacts._ID,
   1443             RawContactsColumns.DISPLAY_NAME,
   1444             RawContacts.ACCOUNT_TYPE,
   1445             RawContacts.ACCOUNT_NAME,
   1446             RawContacts.SOURCE_ID,
   1447         };
   1448 
   1449         int ID = 0;
   1450         int DISPLAY_NAME = 1;
   1451         int ACCOUNT_TYPE = 2;
   1452         int ACCOUNT_NAME = 3;
   1453         int SOURCE_ID = 4;
   1454     }
   1455 
   1456     public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
   1457         long contactId = mDbHelper.getContactId(rawContactId);
   1458         if (contactId == 0) {
   1459             return;
   1460         }
   1461 
   1462         updateLookupKeyForContact(db, contactId);
   1463     }
   1464 
   1465     public void updateLookupKeyForContact(SQLiteDatabase db, long contactId) {
   1466         mSb.setLength(0);
   1467         mSelectionArgs1[0] = String.valueOf(contactId);
   1468         final Cursor c = db.query(Tables.RAW_CONTACTS, LookupKeyQuery.COLUMNS,
   1469                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID);
   1470         try {
   1471             while (c.moveToNext()) {
   1472                 ContactLookupKey.appendToLookupKey(mSb,
   1473                         c.getString(LookupKeyQuery.ACCOUNT_TYPE),
   1474                         c.getString(LookupKeyQuery.ACCOUNT_NAME),
   1475                         c.getLong(LookupKeyQuery.ID),
   1476                         c.getString(LookupKeyQuery.SOURCE_ID),
   1477                         c.getString(LookupKeyQuery.DISPLAY_NAME));
   1478             }
   1479         } finally {
   1480             c.close();
   1481         }
   1482 
   1483         if (mSb.length() == 0) {
   1484             mLookupKeyUpdate.bindNull(1);
   1485         } else {
   1486             mLookupKeyUpdate.bindString(1, Uri.encode(mSb.toString()));
   1487         }
   1488         mLookupKeyUpdate.bindLong(2, contactId);
   1489 
   1490         mLookupKeyUpdate.execute();
   1491     }
   1492 
   1493     /**
   1494      * Execute {@link SQLiteStatement} that will update the
   1495      * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}.
   1496      */
   1497     protected void updateStarred(long rawContactId) {
   1498         long contactId = mDbHelper.getContactId(rawContactId);
   1499         if (contactId == 0) {
   1500             return;
   1501         }
   1502 
   1503         mStarredUpdate.bindLong(1, contactId);
   1504         mStarredUpdate.execute();
   1505     }
   1506 
   1507     /**
   1508      * Finds matching contacts and returns a cursor on those.
   1509      */
   1510     public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb, String[] projection,
   1511             long contactId, int maxSuggestions, String filter) {
   1512         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
   1513 
   1514         List<MatchScore> bestMatches = findMatchingContacts(db, contactId);
   1515         return queryMatchingContacts(qb, db, contactId, projection, bestMatches, maxSuggestions,
   1516                 filter);
   1517     }
   1518 
   1519     private interface ContactIdQuery {
   1520         String[] COLUMNS = new String[] {
   1521             Contacts._ID
   1522         };
   1523 
   1524         int _ID = 0;
   1525     }
   1526 
   1527     /**
   1528      * Loads contacts with specified IDs and returns them in the order of IDs in the
   1529      * supplied list.
   1530      */
   1531     private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db, long contactId,
   1532             String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) {
   1533 
   1534         StringBuilder sb = new StringBuilder();
   1535         sb.append(Contacts._ID);
   1536         sb.append(" IN (");
   1537         for (int i = 0; i < bestMatches.size(); i++) {
   1538             MatchScore matchScore = bestMatches.get(i);
   1539             if (i != 0) {
   1540                 sb.append(",");
   1541             }
   1542             sb.append(matchScore.getContactId());
   1543         }
   1544         sb.append(")");
   1545 
   1546         if (!TextUtils.isEmpty(filter)) {
   1547             sb.append(" AND " + Contacts._ID + " IN ");
   1548             mContactsProvider.appendContactFilterAsNestedQuery(sb, filter);
   1549         }
   1550 
   1551         // Run a query and find ids of best matching contacts satisfying the filter (if any)
   1552         HashSet<Long> foundIds = new HashSet<Long>();
   1553         Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(),
   1554                 null, null, null, null);
   1555         try {
   1556             while(cursor.moveToNext()) {
   1557                 foundIds.add(cursor.getLong(ContactIdQuery._ID));
   1558             }
   1559         } finally {
   1560             cursor.close();
   1561         }
   1562 
   1563         // Exclude all contacts that did not match the filter
   1564         Iterator<MatchScore> iter = bestMatches.iterator();
   1565         while (iter.hasNext()) {
   1566             long id = iter.next().getContactId();
   1567             if (!foundIds.contains(id)) {
   1568                 iter.remove();
   1569             }
   1570         }
   1571 
   1572         // Limit the number of returned suggestions
   1573         if (bestMatches.size() > maxSuggestions) {
   1574             bestMatches = bestMatches.subList(0, maxSuggestions);
   1575         }
   1576 
   1577         // Build an in-clause with the remaining contact IDs
   1578         sb.setLength(0);
   1579         sb.append(Contacts._ID);
   1580         sb.append(" IN (");
   1581         for (int i = 0; i < bestMatches.size(); i++) {
   1582             MatchScore matchScore = bestMatches.get(i);
   1583             if (i != 0) {
   1584                 sb.append(",");
   1585             }
   1586             sb.append(matchScore.getContactId());
   1587         }
   1588         sb.append(")");
   1589 
   1590         // Run the final query with the required projection and contact IDs found by the first query
   1591         cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID);
   1592 
   1593         // Build a sorted list of discovered IDs
   1594         ArrayList<Long> sortedContactIds = new ArrayList<Long>(bestMatches.size());
   1595         for (MatchScore matchScore : bestMatches) {
   1596             sortedContactIds.add(matchScore.getContactId());
   1597         }
   1598 
   1599         Collections.sort(sortedContactIds);
   1600 
   1601         // Map cursor indexes according to the descending order of match scores
   1602         int[] positionMap = new int[bestMatches.size()];
   1603         for (int i = 0; i < positionMap.length; i++) {
   1604             long id = bestMatches.get(i).getContactId();
   1605             positionMap[i] = sortedContactIds.indexOf(id);
   1606         }
   1607 
   1608         return new ReorderingCursorWrapper(cursor, positionMap);
   1609     }
   1610 
   1611     private interface RawContactIdQuery {
   1612         String TABLE = Tables.RAW_CONTACTS;
   1613 
   1614         String[] COLUMNS = new String[] {
   1615             RawContacts._ID
   1616         };
   1617 
   1618         int _ID = 0;
   1619     }
   1620 
   1621     /**
   1622      * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
   1623      * descending order of match score.
   1624      */
   1625     private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId) {
   1626 
   1627         MatchCandidateList candidates = new MatchCandidateList();
   1628         ContactMatcher matcher = new ContactMatcher();
   1629 
   1630         // Don't aggregate a contact with itself
   1631         matcher.keepOut(contactId);
   1632 
   1633         final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
   1634                 RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
   1635         try {
   1636             while (c.moveToNext()) {
   1637                 long rawContactId = c.getLong(RawContactIdQuery._ID);
   1638                 updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
   1639                         matcher);
   1640             }
   1641         } finally {
   1642             c.close();
   1643         }
   1644 
   1645         return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST);
   1646     }
   1647 
   1648     /**
   1649      * Computes scores for contacts that have matching data rows.
   1650      */
   1651     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
   1652             long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
   1653 
   1654         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
   1655         updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
   1656         updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
   1657         loadNameMatchCandidates(db, rawContactId, candidates, false);
   1658         lookupApproximateNameMatches(db, candidates, matcher);
   1659     }
   1660 }
   1661