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