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