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