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