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