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