Fork ContactAggregator to AbstractContactAggregator to keep file hsitory.

Bug:20055573
Change-Id: I42c85df36f039b59707101660441243fb90baca2
diff --git a/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
new file mode 100644
index 0000000..5b407a6
--- /dev/null
+++ b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
@@ -0,0 +1,2805 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.aggregation;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.database.sqlite.SQLiteStatement;
+import android.net.Uri;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhotoFiles;
+import android.provider.ContactsContract.PinnedPositions;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.providers.contacts.ContactLookupKey;
+import com.android.providers.contacts.ContactsDatabaseHelper;
+import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.ContactsDatabaseHelper.Views;
+import com.android.providers.contacts.ContactsProvider2;
+import com.android.providers.contacts.NameLookupBuilder;
+import com.android.providers.contacts.NameNormalizer;
+import com.android.providers.contacts.NameSplitter;
+import com.android.providers.contacts.PhotoPriorityResolver;
+import com.android.providers.contacts.ReorderingCursorWrapper;
+import com.android.providers.contacts.TransactionContext;
+import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
+import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper;
+import com.android.providers.contacts.aggregation.util.ContactMatcher;
+import com.android.providers.contacts.aggregation.util.ContactMatcher.MatchScore;
+import com.android.providers.contacts.database.ContactsTableUtil;
+import com.android.providers.contacts.util.Clock;
+
+import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.HashMultimap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * ContactAggregator deals with aggregating contact information coming from different sources.
+ * Two John Doe contacts from two disjoint sources are presumed to be the same
+ * person unless the user declares otherwise.
+ */
+public class AbstractContactAggregator {
+
+    private static final String TAG = "ContactAggregator";
+
+    private static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
+
+    private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
+            NameLookupColumns.NAME_TYPE + " IN ("
+                    + NameLookupType.NAME_EXACT + ","
+                    + NameLookupType.NAME_VARIANT + ","
+                    + NameLookupType.NAME_COLLATION_KEY + ")";
+
+
+    /**
+     * SQL statement that sets the {@link ContactsColumns#LAST_STATUS_UPDATE_ID} column
+     * on the contact to point to the latest social status update.
+     */
+    private static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL =
+            "UPDATE " + Tables.CONTACTS +
+            " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
+                    "(SELECT " + DataColumns.CONCRETE_ID +
+                    " FROM " + Tables.STATUS_UPDATES +
+                    " JOIN " + Tables.DATA +
+                    "   ON (" + StatusUpdatesColumns.DATA_ID + "="
+                            + DataColumns.CONCRETE_ID + ")" +
+                    " JOIN " + Tables.RAW_CONTACTS +
+                    "   ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
+                            + RawContactsColumns.CONCRETE_ID + ")" +
+                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
+                    " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
+                            + StatusUpdates.STATUS +
+                    " LIMIT 1)" +
+            " WHERE " + ContactsColumns.CONCRETE_ID + "=?";
+
+    // From system/core/logcat/event-log-tags
+    // aggregator [time, count] will be logged for each aggregator cycle.
+    // For the query (as opposed to the merge), count will be negative
+    public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747;
+
+    // If we encounter more than this many contacts with matching names, aggregate only this many
+    private static final int PRIMARY_HIT_LIMIT = 15;
+    private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT);
+
+    // If we encounter more than this many contacts with matching phone number or email,
+    // don't attempt to aggregate - this is likely an error or a shared corporate data element.
+    private static final int SECONDARY_HIT_LIMIT = 20;
+    private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
+
+    // If we encounter no less than this many raw contacts in the best matching contact during
+    // aggregation, don't attempt to aggregate - this is likely an error or a shared corporate
+    // data element.
+    @VisibleForTesting
+    static final int AGGREGATION_CONTACT_SIZE_LIMIT = 50;
+
+    // If we encounter more than this many contacts with matching name during aggregation
+    // suggestion lookup, ignore the remaining results.
+    private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100;
+
+    // Return code for the canJoinIntoContact method.
+    private static final int JOIN = 1;
+    private static final int KEEP_SEPARATE = 0;
+    private static final int RE_AGGREGATE = -1;
+
+    private final ContactsProvider2 mContactsProvider;
+    private final ContactsDatabaseHelper mDbHelper;
+    private PhotoPriorityResolver mPhotoPriorityResolver;
+    private final NameSplitter mNameSplitter;
+    private final CommonNicknameCache mCommonNicknameCache;
+
+    private boolean mEnabled = true;
+
+    /** Precompiled sql statement for setting an aggregated presence */
+    private SQLiteStatement mAggregatedPresenceReplace;
+    private SQLiteStatement mPresenceContactIdUpdate;
+    private SQLiteStatement mRawContactCountQuery;
+    private SQLiteStatement mAggregatedPresenceDelete;
+    private SQLiteStatement mMarkForAggregation;
+    private SQLiteStatement mPhotoIdUpdate;
+    private SQLiteStatement mDisplayNameUpdate;
+    private SQLiteStatement mLookupKeyUpdate;
+    private SQLiteStatement mStarredUpdate;
+    private SQLiteStatement mPinnedUpdate;
+    private SQLiteStatement mContactIdAndMarkAggregatedUpdate;
+    private SQLiteStatement mContactIdUpdate;
+    private SQLiteStatement mMarkAggregatedUpdate;
+    private SQLiteStatement mContactUpdate;
+    private SQLiteStatement mContactInsert;
+    private SQLiteStatement mResetPinnedForRawContact;
+
+    private HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap();
+
+    private String[] mSelectionArgs1 = new String[1];
+    private String[] mSelectionArgs2 = new String[2];
+
+    private long mMimeTypeIdIdentity;
+    private long mMimeTypeIdEmail;
+    private long mMimeTypeIdPhoto;
+    private long mMimeTypeIdPhone;
+    private String mRawContactsQueryByRawContactId;
+    private String mRawContactsQueryByContactId;
+    private StringBuilder mSb = new StringBuilder();
+    private MatchCandidateList mCandidates = new MatchCandidateList();
+    private ContactMatcher mMatcher = new ContactMatcher();
+    private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
+
+    /**
+     * Parameter for the suggestion lookup query.
+     */
+    public static final class AggregationSuggestionParameter {
+        public final String kind;
+        public final String value;
+
+        public AggregationSuggestionParameter(String kind, String value) {
+            this.kind = kind;
+            this.value = value;
+        }
+    }
+
+    /**
+     * Captures a potential match for a given name. The matching algorithm
+     * constructs a bunch of NameMatchCandidate objects for various potential matches
+     * and then executes the search in bulk.
+     */
+    private static class NameMatchCandidate {
+        String mName;
+        int mLookupType;
+
+        public NameMatchCandidate(String name, int nameLookupType) {
+            mName = name;
+            mLookupType = nameLookupType;
+        }
+    }
+
+    /**
+     * A list of {@link NameMatchCandidate} that keeps its elements even when the list is
+     * truncated. This is done for optimization purposes to avoid excessive object allocation.
+     */
+    private static class MatchCandidateList {
+        private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>();
+        private int mCount;
+
+        /**
+         * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists.
+         */
+        public void add(String name, int nameLookupType) {
+            if (mCount >= mList.size()) {
+                mList.add(new NameMatchCandidate(name, nameLookupType));
+            } else {
+                NameMatchCandidate candidate = mList.get(mCount);
+                candidate.mName = name;
+                candidate.mLookupType = nameLookupType;
+            }
+            mCount++;
+        }
+
+        public void clear() {
+            mCount = 0;
+        }
+
+        public boolean isEmpty() {
+            return mCount == 0;
+        }
+    }
+
+    /**
+     * A convenience class used in the algorithm that figures out which of available
+     * display names to use for an aggregate contact.
+     */
+    private static class DisplayNameCandidate {
+        long rawContactId;
+        String displayName;
+        int displayNameSource;
+        boolean isNameSuperPrimary;
+        boolean writableAccount;
+
+        public DisplayNameCandidate() {
+            clear();
+        }
+
+        public void clear() {
+            rawContactId = -1;
+            displayName = null;
+            displayNameSource = DisplayNameSources.UNDEFINED;
+            isNameSuperPrimary = false;
+            writableAccount = false;
+        }
+    }
+
+    /**
+     * Constructor.
+     */
+    public ContactAggregator(ContactsProvider2 contactsProvider,
+            ContactsDatabaseHelper contactsDatabaseHelper,
+            PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
+            CommonNicknameCache commonNicknameCache) {
+        mContactsProvider = contactsProvider;
+        mDbHelper = contactsDatabaseHelper;
+        mPhotoPriorityResolver = photoPriorityResolver;
+        mNameSplitter = nameSplitter;
+        mCommonNicknameCache = commonNicknameCache;
+
+        SQLiteDatabase db = mDbHelper.getReadableDatabase();
+
+        // Since we have no way of determining which custom status was set last,
+        // we'll just pick one randomly.  We are using MAX as an approximation of randomness
+        final String replaceAggregatePresenceSql =
+                "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
+                + AggregatedPresenceColumns.CONTACT_ID + ", "
+                + StatusUpdates.PRESENCE + ", "
+                + StatusUpdates.CHAT_CAPABILITY + ")"
+                + " SELECT " + PresenceColumns.CONTACT_ID + ","
+                + StatusUpdates.PRESENCE + ","
+                + StatusUpdates.CHAT_CAPABILITY
+                + " FROM " + Tables.PRESENCE
+                + " WHERE "
+                + " (" + StatusUpdates.PRESENCE
+                +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
+                + " = (SELECT "
+                + "MAX (" + StatusUpdates.PRESENCE
+                +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
+                + " FROM " + Tables.PRESENCE
+                + " WHERE " + PresenceColumns.CONTACT_ID
+                + "=?)"
+                + " AND " + PresenceColumns.CONTACT_ID
+                + "=?;";
+        mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql);
+
+        mRawContactCountQuery = db.compileStatement(
+                "SELECT COUNT(" + RawContacts._ID + ")" +
+                " FROM " + Tables.RAW_CONTACTS +
+                " WHERE " + RawContacts.CONTACT_ID + "=?"
+                        + " AND " + RawContacts._ID + "<>?");
+
+        mAggregatedPresenceDelete = db.compileStatement(
+                "DELETE FROM " + Tables.AGGREGATED_PRESENCE +
+                " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?");
+
+        mMarkForAggregation = db.compileStatement(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
+                " WHERE " + RawContacts._ID + "=?"
+                        + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0");
+
+        mPhotoIdUpdate = db.compileStatement(
+                "UPDATE " + Tables.CONTACTS +
+                " SET " + Contacts.PHOTO_ID + "=?," + Contacts.PHOTO_FILE_ID + "=? " +
+                " WHERE " + Contacts._ID + "=?");
+
+        mDisplayNameUpdate = db.compileStatement(
+                "UPDATE " + Tables.CONTACTS +
+                " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
+                " WHERE " + Contacts._ID + "=?");
+
+        mLookupKeyUpdate = db.compileStatement(
+                "UPDATE " + Tables.CONTACTS +
+                " SET " + Contacts.LOOKUP_KEY + "=? " +
+                " WHERE " + Contacts._ID + "=?");
+
+        mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
+                + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED
+                + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE "
+                + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND "
+                + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?");
+
+        mPinnedUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
+                + Contacts.PINNED + " = IFNULL((SELECT MIN(" + RawContacts.PINNED + ") FROM "
+                + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
+                + ContactsColumns.CONCRETE_ID + " AND " + RawContacts.PINNED + ">"
+                + PinnedPositions.UNPINNED + ")," + PinnedPositions.UNPINNED + ") "
+                + "WHERE " + Contacts._ID + "=?");
+
+        mContactIdAndMarkAggregatedUpdate = db.compileStatement(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContacts.CONTACT_ID + "=?, "
+                        + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
+                " WHERE " + RawContacts._ID + "=?");
+
+        mContactIdUpdate = db.compileStatement(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContacts.CONTACT_ID + "=?" +
+                " WHERE " + RawContacts._ID + "=?");
+
+        mMarkAggregatedUpdate = db.compileStatement(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
+                " WHERE " + RawContacts._ID + "=?");
+
+        mPresenceContactIdUpdate = db.compileStatement(
+                "UPDATE " + Tables.PRESENCE +
+                " SET " + PresenceColumns.CONTACT_ID + "=?" +
+                " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
+
+        mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
+        mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
+
+        mResetPinnedForRawContact = db.compileStatement(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContacts.PINNED + "=" + PinnedPositions.UNPINNED +
+                " WHERE " + RawContacts._ID + "=?");
+
+        mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
+        mMimeTypeIdIdentity = mDbHelper.getMimeTypeId(Identity.CONTENT_ITEM_TYPE);
+        mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
+        mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+
+        // Query used to retrieve data from raw contacts to populate the corresponding aggregate
+        mRawContactsQueryByRawContactId = String.format(Locale.US,
+                RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
+                mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone);
+
+        mRawContactsQueryByContactId = String.format(Locale.US,
+                RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
+                mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone);
+    }
+
+    public void setEnabled(boolean enabled) {
+        mEnabled = enabled;
+    }
+
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    private interface AggregationQuery {
+        String SQL =
+                "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
+                        ", " + RawContactsColumns.ACCOUNT_ID +
+                " FROM " + Tables.RAW_CONTACTS +
+                " WHERE " + RawContacts._ID + " IN(";
+
+        int _ID = 0;
+        int CONTACT_ID = 1;
+        int ACCOUNT_ID = 2;
+    }
+
+    /**
+     * Aggregate all raw contacts that were marked for aggregation in the current transaction.
+     * Call just before committing the transaction.
+     */
+    public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) {
+        final int markedCount = mRawContactsMarkedForAggregation.size();
+        if (markedCount == 0) {
+            return;
+        }
+
+        final long start = System.currentTimeMillis();
+        if (DEBUG_LOGGING) {
+            Log.d(TAG, "aggregateInTransaction for " + markedCount + " contacts");
+        }
+
+        EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -markedCount);
+
+        int index = 0;
+
+        // We don't use the cached string builder (namely mSb)  here, as this string can be very
+        // long when upgrading (where we re-aggregate all visible contacts) and StringBuilder won't
+        // shrink the internal storage.
+        // Note: don't use selection args here.  We just include all IDs directly in the selection,
+        // because there's a limit for the number of parameters in a query.
+        final StringBuilder sbQuery = new StringBuilder();
+        sbQuery.append(AggregationQuery.SQL);
+        for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) {
+            if (index > 0) {
+                sbQuery.append(',');
+            }
+            sbQuery.append(rawContactId);
+            index++;
+        }
+
+        sbQuery.append(')');
+
+        final long[] rawContactIds;
+        final long[] contactIds;
+        final long[] accountIds;
+        final int actualCount;
+        final Cursor c = db.rawQuery(sbQuery.toString(), null);
+        try {
+            actualCount = c.getCount();
+            rawContactIds = new long[actualCount];
+            contactIds = new long[actualCount];
+            accountIds = new long[actualCount];
+
+            index = 0;
+            while (c.moveToNext()) {
+                rawContactIds[index] = c.getLong(AggregationQuery._ID);
+                contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
+                accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID);
+                index++;
+            }
+        } finally {
+            c.close();
+        }
+
+        if (DEBUG_LOGGING) {
+            Log.d(TAG, "aggregateInTransaction: initial query done.");
+        }
+
+        for (int i = 0; i < actualCount; i++) {
+            aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i],
+                    mCandidates, mMatcher);
+        }
+
+        long elapsedTime = System.currentTimeMillis() - start;
+        EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, actualCount);
+
+        if (DEBUG_LOGGING) {
+            Log.d(TAG, "Contact aggregation complete: " + actualCount +
+                    (actualCount == 0 ? "" : ", " + (elapsedTime / actualCount)
+                            + " ms per raw contact"));
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    public void triggerAggregation(TransactionContext txContext, long rawContactId) {
+        if (!mEnabled) {
+            return;
+        }
+
+        int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
+        switch (aggregationMode) {
+            case RawContacts.AGGREGATION_MODE_DISABLED:
+                break;
+
+            case RawContacts.AGGREGATION_MODE_DEFAULT: {
+                markForAggregation(rawContactId, aggregationMode, false);
+                break;
+            }
+
+            case RawContacts.AGGREGATION_MODE_SUSPENDED: {
+                long contactId = mDbHelper.getContactId(rawContactId);
+
+                if (contactId != 0) {
+                    updateAggregateData(txContext, contactId);
+                }
+                break;
+            }
+
+            case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
+                aggregateContact(txContext, mDbHelper.getWritableDatabase(), rawContactId);
+                break;
+            }
+        }
+    }
+
+    public void clearPendingAggregations() {
+        // HashMap woulnd't shrink the internal table once expands it, so let's just re-create
+        // a new one instead of clear()ing it.
+        mRawContactsMarkedForAggregation = Maps.newHashMap();
+    }
+
+    public void markNewForAggregation(long rawContactId, int aggregationMode) {
+        mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
+    }
+
+    public void markForAggregation(long rawContactId, int aggregationMode, boolean force) {
+        final int effectiveAggregationMode;
+        if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) {
+            // As per ContactsContract documentation, default aggregation mode
+            // does not override a previously set mode
+            if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
+                effectiveAggregationMode = mRawContactsMarkedForAggregation.get(rawContactId);
+            } else {
+                effectiveAggregationMode = aggregationMode;
+            }
+        } else {
+            mMarkForAggregation.bindLong(1, rawContactId);
+            mMarkForAggregation.execute();
+            effectiveAggregationMode = aggregationMode;
+        }
+
+        mRawContactsMarkedForAggregation.put(rawContactId, effectiveAggregationMode);
+    }
+
+    private static class RawContactIdAndAggregationModeQuery {
+        public static final String TABLE = Tables.RAW_CONTACTS;
+
+        public static final String[] COLUMNS = { RawContacts._ID, RawContacts.AGGREGATION_MODE };
+
+        public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
+
+        public static final int _ID = 0;
+        public static final int AGGREGATION_MODE = 1;
+    }
+
+    /**
+     * Marks all constituent raw contacts of an aggregated contact for re-aggregation.
+     */
+    private void markContactForAggregation(SQLiteDatabase db, long contactId) {
+        mSelectionArgs1[0] = String.valueOf(contactId);
+        Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE,
+                RawContactIdAndAggregationModeQuery.COLUMNS,
+                RawContactIdAndAggregationModeQuery.SELECTION, mSelectionArgs1, null, null, null);
+        try {
+            if (cursor.moveToFirst()) {
+                long rawContactId = cursor.getLong(RawContactIdAndAggregationModeQuery._ID);
+                int aggregationMode = cursor.getInt(
+                        RawContactIdAndAggregationModeQuery.AGGREGATION_MODE);
+                // Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED.
+                // (Also just ignore deprecated AGGREGATION_MODE_IMMEDIATE)
+                if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
+                    markForAggregation(rawContactId, aggregationMode, true);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Mark all visible contacts for re-aggregation.
+     *
+     * - Set {@link RawContactsColumns#AGGREGATION_NEEDED} For all visible raw_contacts with
+     *   {@link RawContacts#AGGREGATION_MODE_DEFAULT}.
+     * - Also put them into {@link #mRawContactsMarkedForAggregation}.
+     */
+    public int markAllVisibleForAggregation(SQLiteDatabase db) {
+        final long start = System.currentTimeMillis();
+
+        // Set AGGREGATION_NEEDED for all visible raw_cotnacts with AGGREGATION_MODE_DEFAULT.
+        // (Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED)
+        db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
+                RawContactsColumns.AGGREGATION_NEEDED + "=1" +
+                " WHERE " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY +
+                " AND " + RawContacts.AGGREGATION_MODE + "=" + RawContacts.AGGREGATION_MODE_DEFAULT
+                );
+
+        final int count;
+        final Cursor cursor = db.rawQuery("SELECT " + RawContacts._ID +
+                " FROM " + Tables.RAW_CONTACTS +
+                " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1", null);
+        try {
+            count = cursor.getCount();
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                final long rawContactId = cursor.getLong(0);
+                mRawContactsMarkedForAggregation.put(rawContactId,
+                        RawContacts.AGGREGATION_MODE_DEFAULT);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        final long end = System.currentTimeMillis();
+        Log.i(TAG, "Marked all visible contacts for aggregation: " + count + " raw contacts, " +
+                (end - start) + " ms");
+        return count;
+    }
+
+    /**
+     * Creates a new contact based on the given raw contact.  Does not perform aggregation.  Returns
+     * the ID of the contact that was created.
+     */
+    public long onRawContactInsert(
+            TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
+        long contactId = insertContact(db, rawContactId);
+        setContactId(rawContactId, contactId);
+        mDbHelper.updateContactVisible(txContext, contactId);
+        return contactId;
+    }
+
+    protected long insertContact(SQLiteDatabase db, long rawContactId) {
+        mSelectionArgs1[0] = String.valueOf(rawContactId);
+        computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
+        return mContactInsert.executeInsert();
+    }
+
+    private static final class RawContactIdAndAccountQuery {
+        public static final String TABLE = Tables.RAW_CONTACTS;
+
+        public static final String[] COLUMNS = {
+                RawContacts.CONTACT_ID,
+                RawContactsColumns.ACCOUNT_ID
+        };
+
+        public static final String SELECTION = RawContacts._ID + "=?";
+
+        public static final int CONTACT_ID = 0;
+        public static final int ACCOUNT_ID = 1;
+    }
+
+    public void aggregateContact(
+            TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
+        if (!mEnabled) {
+            return;
+        }
+
+        MatchCandidateList candidates = new MatchCandidateList();
+        ContactMatcher matcher = new ContactMatcher();
+
+        long contactId = 0;
+        long accountId = 0;
+        mSelectionArgs1[0] = String.valueOf(rawContactId);
+        Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE,
+                RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION,
+                mSelectionArgs1, null, null, null);
+        try {
+            if (cursor.moveToFirst()) {
+                contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID);
+                accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        aggregateContact(txContext, db, rawContactId, accountId, contactId,
+                candidates, matcher);
+    }
+
+    public void updateAggregateData(TransactionContext txContext, long contactId) {
+        if (!mEnabled) {
+            return;
+        }
+
+        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        computeAggregateData(db, contactId, mContactUpdate);
+        mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+        mContactUpdate.execute();
+
+        mDbHelper.updateContactVisible(txContext, contactId);
+        updateAggregatedStatusUpdate(contactId);
+    }
+
+    private void updateAggregatedStatusUpdate(long contactId) {
+        mAggregatedPresenceReplace.bindLong(1, contactId);
+        mAggregatedPresenceReplace.bindLong(2, contactId);
+        mAggregatedPresenceReplace.execute();
+        updateLastStatusUpdateId(contactId);
+    }
+
+    /**
+     * Adjusts the reference to the latest status update for the specified contact.
+     */
+    public void updateLastStatusUpdateId(long contactId) {
+        String contactIdString = String.valueOf(contactId);
+        mDbHelper.getWritableDatabase().execSQL(UPDATE_LAST_STATUS_UPDATE_ID_SQL,
+                new String[]{contactIdString, contactIdString});
+    }
+
+    /**
+     * Given a specific raw contact, finds all matching aggregate contacts and chooses the one
+     * with the highest match score.  If no such contact is found, creates a new contact.
+     */
+    private synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
+            long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates,
+            ContactMatcher matcher) {
+
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId);
+        }
+
+        int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
+
+        Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId);
+        if (aggModeObject != null) {
+            aggregationMode = aggModeObject;
+        }
+
+        long contactId = -1; // Best matching contact ID.
+        boolean needReaggregate = false;
+
+        final Set<Long> rawContactIdsInSameAccount = new HashSet<Long>();
+        final Set<Long> rawContactIdsInOtherAccount = new HashSet<Long>();
+        if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
+            candidates.clear();
+            matcher.clear();
+
+            contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher);
+            if (contactId == -1) {
+
+                // If this is a newly inserted contact or a visible contact, look for
+                // data matches.
+                if (currentContactId == 0
+                        || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) {
+                    contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher);
+                }
+
+                // If we found an best matched contact, find out if the raw contact can be joined
+                // into it
+                if (contactId != -1 && contactId != currentContactId) {
+                    // List all raw contact ID and their account ID mappings in contact
+                    // [contactId] excluding raw_contact [rawContactId].
+
+                    // Based on the mapping, create two sets of raw contact IDs in
+                    // [rawContactAccountId] and not in [rawContactAccountId]. We don't always
+                    // need them, so lazily initialize them.
+                    mSelectionArgs2[0] = String.valueOf(contactId);
+                    mSelectionArgs2[1] = String.valueOf(rawContactId);
+                    final Cursor rawContactsToAccountsCursor = db.rawQuery(
+                            "SELECT " + RawContacts._ID + ", " + RawContactsColumns.ACCOUNT_ID +
+                                    " FROM " + Tables.RAW_CONTACTS +
+                                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
+                                    " AND " + RawContacts._ID + "!=?",
+                            mSelectionArgs2);
+                    try {
+                        rawContactsToAccountsCursor.moveToPosition(-1);
+                        while (rawContactsToAccountsCursor.moveToNext()) {
+                            final long rcId = rawContactsToAccountsCursor.getLong(0);
+                            final long rc_accountId = rawContactsToAccountsCursor.getLong(1);
+                            if (rc_accountId == accountId) {
+                                rawContactIdsInSameAccount.add(rcId);
+                            } else {
+                                rawContactIdsInOtherAccount.add(rcId);
+                            }
+                        }
+                    } finally {
+                        rawContactsToAccountsCursor.close();
+                    }
+                    final int actionCode;
+                    final int totalNumOfRawContactsInCandidate = rawContactIdsInSameAccount.size()
+                            + rawContactIdsInOtherAccount.size();
+                    if (totalNumOfRawContactsInCandidate >= AGGREGATION_CONTACT_SIZE_LIMIT) {
+                        if (VERBOSE_LOGGING) {
+                            Log.v(TAG, "Too many raw contacts (" + totalNumOfRawContactsInCandidate
+                                    + ") in the best matching contact, so skip aggregation");
+                        }
+                        actionCode = KEEP_SEPARATE;
+                    } else {
+                        actionCode = canJoinIntoContact(db, rawContactId,
+                                rawContactIdsInSameAccount, rawContactIdsInOtherAccount);
+                    }
+                    if (actionCode == KEEP_SEPARATE) {
+                        contactId = -1;
+                    } else if (actionCode == RE_AGGREGATE) {
+                        needReaggregate = true;
+                    }
+                }
+            }
+        } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
+            return;
+        }
+
+        // # of raw_contacts in the [currentContactId] contact excluding the [rawContactId]
+        // raw_contact.
+        long currentContactContentsCount = 0;
+
+        if (currentContactId != 0) {
+            mRawContactCountQuery.bindLong(1, currentContactId);
+            mRawContactCountQuery.bindLong(2, rawContactId);
+            currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong();
+        }
+
+        // If there are no other raw contacts in the current aggregate, we might as well reuse it.
+        // Also, if the aggregation mode is SUSPENDED, we must reuse the same aggregate.
+        if (contactId == -1
+                && currentContactId != 0
+                && (currentContactContentsCount == 0
+                        || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) {
+            contactId = currentContactId;
+        }
+
+        if (contactId == currentContactId) {
+            // Aggregation unchanged
+            markAggregated(rawContactId);
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "Aggregation unchanged");
+            }
+        } else if (contactId == -1) {
+            // create new contact for [rawContactId]
+            createContactForRawContacts(db, txContext, Sets.newHashSet(rawContactId), null);
+            if (currentContactContentsCount > 0) {
+                updateAggregateData(txContext, currentContactId);
+            }
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "create new contact for rid=" + rawContactId);
+            }
+        } else if (needReaggregate) {
+            // re-aggregate
+            final Set<Long> allRawContactIdSet = new HashSet<Long>();
+            allRawContactIdSet.addAll(rawContactIdsInSameAccount);
+            allRawContactIdSet.addAll(rawContactIdsInOtherAccount);
+            // If there is no other raw contacts aggregated with the given raw contact currently,
+            // we might as well reuse it.
+            currentContactId = (currentContactId != 0 && currentContactContentsCount == 0)
+                    ? currentContactId : 0;
+            reAggregateRawContacts(txContext, db, contactId, currentContactId, rawContactId,
+                    allRawContactIdSet);
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "Re-aggregating rid=" + rawContactId + " and cid=" + contactId);
+            }
+        } else {
+            // Joining with an existing aggregate
+            if (currentContactContentsCount == 0) {
+                // Delete a previous aggregate if it only contained this raw contact
+                ContactsTableUtil.deleteContact(db, currentContactId);
+
+                mAggregatedPresenceDelete.bindLong(1, currentContactId);
+                mAggregatedPresenceDelete.execute();
+            }
+
+            clearSuperPrimarySetting(db, contactId, rawContactId);
+            setContactIdAndMarkAggregated(rawContactId, contactId);
+            computeAggregateData(db, contactId, mContactUpdate);
+            mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+            mContactUpdate.execute();
+            mDbHelper.updateContactVisible(txContext, contactId);
+            updateAggregatedStatusUpdate(contactId);
+            // Make sure the raw contact does not contribute to the current contact
+            if (currentContactId != 0) {
+                updateAggregateData(txContext, currentContactId);
+            }
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "Join rid=" + rawContactId + " with cid=" + contactId);
+            }
+        }
+    }
+
+    /**
+     * Find out which mime-types are shared by raw contact of {@code rawContactId} and raw contacts
+     * of {@code contactId}. Clear the is_super_primary settings for these mime-types.
+     */
+    private void clearSuperPrimarySetting(SQLiteDatabase db, long contactId, long rawContactId) {
+        final String[] args = {String.valueOf(contactId), String.valueOf(rawContactId)};
+
+        // Find out which mime-types exist with is_super_primary=true on both the raw contact of
+        // rawContactId and raw contacts of contactId
+        int index = 0;
+        final StringBuilder mimeTypeCondition = new StringBuilder();
+        mimeTypeCondition.append(" AND " + DataColumns.MIMETYPE_ID + " IN (");
+
+        final Cursor c = db.rawQuery(
+                "SELECT DISTINCT(a." + DataColumns.MIMETYPE_ID + ")" +
+                " FROM (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE " +
+                        Data.IS_SUPER_PRIMARY + " =1 AND " +
+                        Data.RAW_CONTACT_ID + " IN (SELECT " + RawContacts._ID + " FROM " +
+                        Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=?1)) AS a" +
+                " JOIN  (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE " +
+                        Data.IS_SUPER_PRIMARY + " =1 AND " +
+                        Data.RAW_CONTACT_ID + "=?2) AS b" +
+                " ON a." + DataColumns.MIMETYPE_ID + "=b." + DataColumns.MIMETYPE_ID,
+                args);
+        try {
+            c.moveToPosition(-1);
+            while (c.moveToNext()) {
+                if (index > 0) {
+                    mimeTypeCondition.append(',');
+                }
+                mimeTypeCondition.append(c.getLong((0)));
+                index++;
+            }
+        } finally {
+            c.close();
+        }
+
+        if (index == 0) {
+            return;
+        }
+
+        // Clear is_super_primary setting for all the mime-types with is_super_primary=true
+        // in both raw contact of rawContactId and raw contacts of contactId
+        String superPrimaryUpdateSql = "UPDATE " + Tables.DATA +
+                " SET " + Data.IS_SUPER_PRIMARY + "=0" +
+                " WHERE (" +  Data.RAW_CONTACT_ID +
+                        " IN (SELECT " + RawContacts._ID +  " FROM " + Tables.RAW_CONTACTS +
+                        " WHERE " + RawContacts.CONTACT_ID + "=?1)" +
+                        " OR " +  Data.RAW_CONTACT_ID + "=?2)";
+
+        mimeTypeCondition.append(')');
+        superPrimaryUpdateSql += mimeTypeCondition.toString();
+        db.execSQL(superPrimaryUpdateSql, args);
+    }
+
+    /**
+     * @return JOIN if the raw contact of {@code rawContactId} can be joined into the existing
+     * contact of {@code contactId}. KEEP_SEPARATE if the raw contact of {@code rawContactId}
+     * cannot be joined into the existing contact of {@code contactId}. RE_AGGREGATE if raw contact
+     * of {@code rawContactId} and all the raw contacts of contact of {@code contactId} need to be
+     * re-aggregated.
+     *
+     * If contact of {@code contactId} doesn't contain any raw contacts from the same account as
+     * raw contact of {@code rawContactId}, join raw contact with contact if there is no identity
+     * mismatch between them on the same namespace, otherwise, keep them separate.
+     *
+     * If contact of {@code contactId} contains raw contacts from the same account as raw contact of
+     * {@code rawContactId}, join raw contact with contact if there's at least one raw contact in
+     * those raw contacts that shares at least one email address, phone number, or identity;
+     * otherwise, re-aggregate raw contact and all the raw contacts of contact.
+     */
+    private int canJoinIntoContact(SQLiteDatabase db, long rawContactId,
+            Set<Long> rawContactIdsInSameAccount, Set<Long> rawContactIdsInOtherAccount ) {
+
+        if (rawContactIdsInSameAccount.isEmpty()) {
+            final String rid = String.valueOf(rawContactId);
+            final String ridsInOtherAccts = TextUtils.join(",", rawContactIdsInOtherAccount);
+            // If there is no identity match between raw contact of [rawContactId] and
+            // any raw contact in other accounts on the same namespace, and there is at least
+            // one identity mismatch exist, keep raw contact separate from contact.
+            if (DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts,
+                    /* isIdentityMatching =*/ true, /* countOnly =*/ true), null) == 0 &&
+                    DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts,
+                            /* isIdentityMatching =*/ false, /* countOnly =*/ true), null) > 0) {
+                if (VERBOSE_LOGGING) {
+                    Log.v(TAG, "canJoinIntoContact: no duplicates, but has no matching identity " +
+                            "and has mis-matching identity on the same namespace between rid=" +
+                            rid + " and ridsInOtherAccts=" + ridsInOtherAccts);
+                }
+                return KEEP_SEPARATE; // has identity and identity doesn't match
+            } else {
+                if (VERBOSE_LOGGING) {
+                    Log.v(TAG, "canJoinIntoContact: can join the first raw contact from the same " +
+                            "account without any identity mismatch.");
+                }
+                return JOIN; // no identity or identity match
+            }
+        }
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "canJoinIntoContact: " + rawContactIdsInSameAccount.size() +
+                    " duplicate(s) found");
+        }
+
+
+        final Set<Long> rawContactIdSet = new HashSet<Long>();
+        rawContactIdSet.add(rawContactId);
+        if (rawContactIdsInSameAccount.size() > 0 &&
+                isDataMaching(db, rawContactIdSet, rawContactIdsInSameAccount)) {
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "canJoinIntoContact: join if there is a data matching found in the " +
+                        "same account");
+            }
+            return JOIN;
+        } else {
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "canJoinIntoContact: re-aggregate rid=" + rawContactId +
+                        " with its best matching contact to connected component");
+            }
+            return RE_AGGREGATE;
+        }
+    }
+
+    private interface RawContactMatchingSelectionStatement {
+        String SELECT_COUNT =  "SELECT count(*) " ;
+        String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2."  + Data.RAW_CONTACT_ID ;
+    }
+
+    /**
+     * Build sql to check if there is any identity match/mis-match between two sets of raw contact
+     * ids on the same namespace.
+     */
+    private String buildIdentityMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
+            boolean isIdentityMatching, boolean countOnly) {
+        final String identityType = String.valueOf(mMimeTypeIdIdentity);
+        final String matchingOperator = (isIdentityMatching) ? "=" : "!=";
+        final String sql =
+                " FROM " + Tables.DATA + " AS d1" +
+                " JOIN " + Tables.DATA + " AS d2" +
+                        " ON (d1." + Identity.IDENTITY + matchingOperator +
+                        " d2." + Identity.IDENTITY + " AND" +
+                        " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" +
+                " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + identityType +
+                " AND d2." + DataColumns.MIMETYPE_ID + " = " + identityType +
+                " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
+                " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
+        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+                RawContactMatchingSelectionStatement.SELECT_ID + sql;
+    }
+
+    private String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
+            boolean countOnly) {
+        final String emailType = String.valueOf(mMimeTypeIdEmail);
+        final String sql =
+                " FROM " + Tables.DATA + " AS d1" +
+                " JOIN " + Tables.DATA + " AS d2" +
+                        " ON lower(d1." + Email.ADDRESS + ")= lower(d2." + Email.ADDRESS + ")" +
+                " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + emailType +
+                " AND d2." + DataColumns.MIMETYPE_ID + " = " + emailType +
+                " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
+                " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
+        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+                RawContactMatchingSelectionStatement.SELECT_ID + sql;
+    }
+
+    private String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
+            boolean countOnly) {
+        // It's a bit tricker because it has to be consistent with
+        // updateMatchScoresBasedOnPhoneMatches().
+        final String phoneType = String.valueOf(mMimeTypeIdPhone);
+        final String sql =
+                " FROM " + Tables.PHONE_LOOKUP + " AS p1" +
+                " JOIN " + Tables.DATA + " AS d1 ON " +
+                        "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" +
+                " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH +
+                        "=p2." + PhoneLookupColumns.MIN_MATCH + ")" +
+                " JOIN " + Tables.DATA + " AS d2 ON " +
+                        "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" +
+                " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + phoneType +
+                " AND d2." + DataColumns.MIMETYPE_ID + " = " + phoneType +
+                " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
+                " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")" +
+                " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + "," +
+                        String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter()) +
+                        ")";
+        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+                RawContactMatchingSelectionStatement.SELECT_ID + sql;
+    }
+
+    private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2) {
+        return "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " +
+                AggregationExceptions.RAW_CONTACT_ID2 +
+                " FROM " + Tables.AGGREGATION_EXCEPTIONS +
+                " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" +
+                        rawContactIdSet1 + ")" +
+                " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" +
+                " AND " + AggregationExceptions.TYPE + "=" +
+                        AggregationExceptions.TYPE_KEEP_TOGETHER ;
+    }
+
+    private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) {
+        return DatabaseUtils.longForQuery(db, query, null) > 0;
+    }
+
+    /**
+     * If there's any identity, email address or a phone number matching between two raw contact
+     * sets.
+     */
+    private boolean isDataMaching(SQLiteDatabase db, Set<Long> rawContactIdSet1,
+            Set<Long> rawContactIdSet2) {
+        final String rawContactIds1 = TextUtils.join(",", rawContactIdSet1);
+        final String rawContactIds2 = TextUtils.join(",", rawContactIdSet2);
+        // First, check for the identity
+        if (isFirstColumnGreaterThanZero(db, buildIdentityMatchingSql(
+                rawContactIds1, rawContactIds2,  /* isIdentityMatching =*/ true,
+                /* countOnly =*/true))) {
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "canJoinIntoContact: identity match found between " + rawContactIds1 +
+                        " and " + rawContactIds2);
+            }
+            return true;
+        }
+
+        // Next, check for the email address.
+        if (isFirstColumnGreaterThanZero(db,
+                buildEmailMatchingSql(rawContactIds1, rawContactIds2, true))) {
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "canJoinIntoContact: email match found between " + rawContactIds1 +
+                        " and " + rawContactIds2);
+            }
+            return true;
+        }
+
+        // Lastly, the phone number.
+        if (isFirstColumnGreaterThanZero(db,
+                buildPhoneMatchingSql(rawContactIds1, rawContactIds2, true))) {
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "canJoinIntoContact: phone match found between " + rawContactIds1 +
+                        " and " + rawContactIds2);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of
+     * {@code existingRawContactIds} into connected components. This only happens when a given
+     * raw contacts cannot be joined with its best matching contacts directly.
+     *
+     *  Two raw contacts are considered connected if they share at least one email address, phone
+     *  number or identity. Create new contact for each connected component except the very first
+     *  one that doesn't contain rawContactId of {@code rawContactId}.
+     */
+    private void reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db,
+            long contactId, long currentContactId, long rawContactId,
+            Set<Long> existingRawContactIds) {
+        // Find the connected component based on the aggregation exceptions or
+        // identity/email/phone matching for all the raw contacts of [contactId] and the give
+        // raw contact.
+        final Set<Long> allIds = new HashSet<Long>();
+        allIds.add(rawContactId);
+        allIds.addAll(existingRawContactIds);
+        final Set<Set<Long>> connectedRawContactSets = findConnectedRawContacts(db, allIds);
+
+        if (connectedRawContactSets.size() == 1) {
+            // If everything is connected, create one contact with [contactId]
+            createContactForRawContacts(db, txContext, connectedRawContactSets.iterator().next(),
+                    contactId);
+        } else {
+            for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
+                if (connectedRawContactIds.contains(rawContactId)) {
+                    // crate contact for connect component containing [rawContactId], reuse
+                    // [currentContactId] if possible.
+                    createContactForRawContacts(db, txContext, connectedRawContactIds,
+                            currentContactId == 0 ? null : currentContactId);
+                    connectedRawContactSets.remove(connectedRawContactIds);
+                    break;
+                }
+            }
+            // Create new contact for each connected component except the last one. The last one
+            // will reuse [contactId]. Only the last one can reuse [contactId] when all other raw
+            // contacts has already been assigned new contact Id, so that the contact aggregation
+            // stats could be updated correctly.
+            int index = connectedRawContactSets.size();
+            for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
+                if (index > 1) {
+                    createContactForRawContacts(db, txContext, connectedRawContactIds, null);
+                    index--;
+                } else {
+                    createContactForRawContacts(db, txContext, connectedRawContactIds, contactId);
+                }
+            }
+        }
+    }
+
+    /**
+     * Partition the given raw contact Ids to connected component based on aggregation exception,
+     * identity matching, email matching or phone matching.
+     */
+    private Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long> rawContactIdSet) {
+        // Connections between two raw contacts
+       final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create();
+        String rawContactIds = TextUtils.join(",", rawContactIdSet);
+        findIdPairs(db, buildExceptionMatchingSql(rawContactIds, rawContactIds),
+                matchingRawIdPairs);
+        findIdPairs(db, buildIdentityMatchingSql(rawContactIds, rawContactIds,
+                /* isIdentityMatching =*/ true, /* countOnly =*/false), matchingRawIdPairs);
+        findIdPairs(db, buildEmailMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false),
+                matchingRawIdPairs);
+        findIdPairs(db, buildPhoneMatchingSql(rawContactIds, rawContactIds,  /* countOnly =*/false),
+                matchingRawIdPairs);
+
+        return ContactAggregatorHelper.findConnectedComponents(rawContactIdSet, matchingRawIdPairs);
+    }
+
+    /**
+     * Given a query which will return two non-null IDs in the first two columns as results, this
+     * method will put two entries into the given result map for each pair of different IDs, one
+     * keyed by each ID.
+     */
+    private void findIdPairs(SQLiteDatabase db, String query, Multimap<Long, Long> results) {
+        Cursor cursor = db.rawQuery(query, null);
+        try {
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                long idA = cursor.getLong(0);
+                long idB = cursor.getLong(1);
+                if (idA != idB) {
+                    results.put(idA, idB);
+                    results.put(idB, idA);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Creates a new Contact for a given set of the raw contacts of {@code rawContactIds} if the
+     * given contactId is null. Otherwise, regroup them into contact with {@code contactId}.
+     */
+    private void createContactForRawContacts(SQLiteDatabase db, TransactionContext txContext,
+            Set<Long> rawContactIds, Long contactId) {
+        if (rawContactIds.isEmpty()) {
+            // No raw contact id is provided.
+            return;
+        }
+
+        // If contactId is not provided, generates a new one.
+        if (contactId == null) {
+            mSelectionArgs1[0]= String.valueOf(rawContactIds.iterator().next());
+            computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
+                    mContactInsert);
+            contactId = mContactInsert.executeInsert();
+        }
+        for (Long rawContactId : rawContactIds) {
+            // Regrouped contacts should automatically be unpinned.
+            unpinRawContact(rawContactId);
+            setContactIdAndMarkAggregated(rawContactId, contactId);
+            setPresenceContactId(rawContactId, contactId);
+        }
+        updateAggregateData(txContext, contactId);
+    }
+
+    private static class RawContactIdQuery {
+        public static final String TABLE = Tables.RAW_CONTACTS;
+        public static final String[] COLUMNS = { RawContacts._ID };
+        public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
+        public static final int RAW_CONTACT_ID = 0;
+    }
+
+    /**
+     * Ensures that automatic aggregation rules are followed after a contact
+     * becomes visible or invisible. Specifically, consider this case: there are
+     * three contacts named Foo. Two of them come from account A1 and one comes
+     * from account A2. The aggregation rules say that in this case none of the
+     * three Foo's should be aggregated: two of them are in the same account, so
+     * they don't get aggregated; the third has two affinities, so it does not
+     * join either of them.
+     * <p>
+     * Consider what happens if one of the "Foo"s from account A1 becomes
+     * invisible. Nothing stands in the way of aggregating the other two
+     * anymore, so they should get joined.
+     * <p>
+     * What if the invisible "Foo" becomes visible after that? We should split the
+     * aggregate between the other two.
+     */
+    public void updateAggregationAfterVisibilityChange(long contactId) {
+        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        boolean visible = mDbHelper.isContactInDefaultDirectory(db, contactId);
+        if (visible) {
+            markContactForAggregation(db, contactId);
+        } else {
+            // Find all contacts that _could be_ aggregated with this one and
+            // rerun aggregation for all of them
+            mSelectionArgs1[0] = String.valueOf(contactId);
+            Cursor cursor = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
+                    RawContactIdQuery.SELECTION, mSelectionArgs1, null, null, null);
+            try {
+                while (cursor.moveToNext()) {
+                    long rawContactId = cursor.getLong(RawContactIdQuery.RAW_CONTACT_ID);
+                    mMatcher.clear();
+
+                    updateMatchScoresBasedOnIdentityMatch(db, rawContactId, mMatcher);
+                    updateMatchScoresBasedOnNameMatches(db, rawContactId, mMatcher);
+                    List<MatchScore> bestMatches =
+                            mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+                    for (MatchScore matchScore : bestMatches) {
+                        markContactForAggregation(db, matchScore.getContactId());
+                    }
+
+                    mMatcher.clear();
+                    updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher);
+                    updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher);
+                    bestMatches =
+                            mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
+                    for (MatchScore matchScore : bestMatches) {
+                        markContactForAggregation(db, matchScore.getContactId());
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    /**
+     * Updates the contact ID for the specified contact.
+     */
+    protected void setContactId(long rawContactId, long contactId) {
+        mContactIdUpdate.bindLong(1, contactId);
+        mContactIdUpdate.bindLong(2, rawContactId);
+        mContactIdUpdate.execute();
+    }
+
+    /**
+     * Marks the specified raw contact ID as aggregated
+     */
+    private void markAggregated(long rawContactId) {
+        mMarkAggregatedUpdate.bindLong(1, rawContactId);
+        mMarkAggregatedUpdate.execute();
+    }
+
+    /**
+     * Updates the contact ID for the specified contact and marks the raw contact as aggregated.
+     */
+    private void setContactIdAndMarkAggregated(long rawContactId, long contactId) {
+        mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId);
+        mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId);
+        mContactIdAndMarkAggregatedUpdate.execute();
+    }
+
+    private void setPresenceContactId(long rawContactId, long contactId) {
+        mPresenceContactIdUpdate.bindLong(1, contactId);
+        mPresenceContactIdUpdate.bindLong(2, rawContactId);
+        mPresenceContactIdUpdate.execute();
+    }
+
+    private void unpinRawContact(long rawContactId) {
+        mResetPinnedForRawContact.bindLong(1, rawContactId);
+        mResetPinnedForRawContact.execute();
+    }
+
+    interface AggregateExceptionPrefetchQuery {
+        String TABLE = Tables.AGGREGATION_EXCEPTIONS;
+
+        String[] COLUMNS = {
+            AggregationExceptions.RAW_CONTACT_ID1,
+            AggregationExceptions.RAW_CONTACT_ID2,
+        };
+
+        int RAW_CONTACT_ID1 = 0;
+        int RAW_CONTACT_ID2 = 1;
+    }
+
+    // A set of raw contact IDs for which there are aggregation exceptions
+    private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
+    private boolean mAggregationExceptionIdsValid;
+
+    public void invalidateAggregationExceptionCache() {
+        mAggregationExceptionIdsValid = false;
+    }
+
+    /**
+     * Finds all raw contact IDs for which there are aggregation exceptions. The list of
+     * ids is used as an optimization in aggregation: there is no point to run a query against
+     * the agg_exceptions table if it is known that there are no records there for a given
+     * raw contact ID.
+     */
+    private void prefetchAggregationExceptionIds(SQLiteDatabase db) {
+        mAggregationExceptionIds.clear();
+        final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
+                AggregateExceptionPrefetchQuery.COLUMNS,
+                null, null, null, null, null);
+
+        try {
+            while (c.moveToNext()) {
+                long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
+                long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
+                mAggregationExceptionIds.add(rawContactId1);
+                mAggregationExceptionIds.add(rawContactId2);
+            }
+        } finally {
+            c.close();
+        }
+
+        mAggregationExceptionIdsValid = true;
+    }
+
+    interface AggregateExceptionQuery {
+        String TABLE = Tables.AGGREGATION_EXCEPTIONS
+            + " JOIN raw_contacts raw_contacts1 "
+                    + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) "
+            + " JOIN raw_contacts raw_contacts2 "
+                    + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) ";
+
+        String[] COLUMNS = {
+            AggregationExceptions.TYPE,
+            AggregationExceptions.RAW_CONTACT_ID1,
+            "raw_contacts1." + RawContacts.CONTACT_ID,
+            "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED,
+            "raw_contacts2." + RawContacts.CONTACT_ID,
+            "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED,
+        };
+
+        int TYPE = 0;
+        int RAW_CONTACT_ID1 = 1;
+        int CONTACT_ID1 = 2;
+        int AGGREGATION_NEEDED_1 = 3;
+        int CONTACT_ID2 = 4;
+        int AGGREGATION_NEEDED_2 = 5;
+    }
+
+    /**
+     * Computes match scores based on exceptions entered by the user: always match and never match.
+     * Returns the aggregate contact with the always match exception if any.
+     */
+    private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId,
+            ContactMatcher matcher) {
+        if (!mAggregationExceptionIdsValid) {
+            prefetchAggregationExceptionIds(db);
+        }
+
+        // If there are no aggregation exceptions involving this raw contact, there is no need to
+        // run a query and we can just return -1, which stands for "nothing found"
+        if (!mAggregationExceptionIds.contains(rawContactId)) {
+            return -1;
+        }
+
+        final Cursor c = db.query(AggregateExceptionQuery.TABLE,
+                AggregateExceptionQuery.COLUMNS,
+                AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
+                        + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId,
+                null, null, null, null);
+
+        try {
+            while (c.moveToNext()) {
+                int type = c.getInt(AggregateExceptionQuery.TYPE);
+                long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
+                long contactId = -1;
+                if (rawContactId == rawContactId1) {
+                    if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0
+                            && !c.isNull(AggregateExceptionQuery.CONTACT_ID2)) {
+                        contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2);
+                    }
+                } else {
+                    if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0
+                            && !c.isNull(AggregateExceptionQuery.CONTACT_ID1)) {
+                        contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1);
+                    }
+                }
+                if (contactId != -1) {
+                    if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) {
+                        matcher.keepIn(contactId);
+                    } else {
+                        matcher.keepOut(contactId);
+                    }
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true);
+    }
+
+    /**
+     * Picks the best matching contact based on matches between data elements.  It considers
+     * name match to be primary and phone, email etc matches to be secondary.  A good primary
+     * match triggers aggregation, while a good secondary match only triggers aggregation in
+     * the absence of a strong primary mismatch.
+     * <p>
+     * Consider these examples:
+     * <p>
+     * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should
+     * be aggregated (same number, similar names).
+     * <p>
+     * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should
+     * not be aggregated (same number, different names).
+     */
+    private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
+            MatchCandidateList candidates, ContactMatcher matcher) {
+
+        // Find good matches based on name alone
+        long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, matcher);
+        if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
+            // We found multiple matches on the name - do not aggregate because of the ambiguity
+            return -1;
+        } else if (bestMatch == -1) {
+            // We haven't found a good match on name, see if we have any matches on phone, email etc
+            bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher);
+            if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
+                return -1;
+            }
+        }
+
+        return bestMatch;
+    }
+
+
+    /**
+     * Picks the best matching contact based on secondary data matches.  The method loads
+     * structured names for all candidate contacts and recomputes match scores using approximate
+     * matching.
+     */
+    private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db,
+            long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
+        List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates(
+                ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+        if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) {
+            return -1;
+        }
+
+        loadNameMatchCandidates(db, rawContactId, candidates, true);
+
+        mSb.setLength(0);
+        mSb.append(RawContacts.CONTACT_ID).append(" IN (");
+        for (int i = 0; i < secondaryContactIds.size(); i++) {
+            if (i != 0) {
+                mSb.append(',');
+            }
+            mSb.append(secondaryContactIds.get(i));
+        }
+
+        // We only want to compare structured names to structured names
+        // at this stage, we need to ignore all other sources of name lookup data.
+        mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL);
+
+        matchAllCandidates(db, mSb.toString(), candidates, matcher,
+                ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
+
+        return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false);
+    }
+
+    private interface NameLookupQuery {
+        String TABLE = Tables.NAME_LOOKUP;
+
+        String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
+        String SELECTION_STRUCTURED_NAME_BASED =
+                SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
+
+        String[] COLUMNS = new String[] {
+                NameLookupColumns.NORMALIZED_NAME,
+                NameLookupColumns.NAME_TYPE
+        };
+
+        int NORMALIZED_NAME = 0;
+        int NAME_TYPE = 1;
+    }
+
+    private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
+            MatchCandidateList candidates, boolean structuredNameBased) {
+        candidates.clear();
+        mSelectionArgs1[0] = String.valueOf(rawContactId);
+        Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
+                structuredNameBased
+                        ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
+                        : NameLookupQuery.SELECTION,
+                mSelectionArgs1, null, null, null);
+        try {
+            while (c.moveToNext()) {
+                String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
+                int type = c.getInt(NameLookupQuery.NAME_TYPE);
+                candidates.add(normalizedName, type);
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Computes scores for contacts that have matching data rows.
+     */
+    private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
+            ContactMatcher matcher) {
+
+        updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
+        updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+        long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY, false);
+        if (bestMatch != -1) {
+            return bestMatch;
+        }
+
+        updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+        updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+
+        return -1;
+    }
+
+    private interface IdentityLookupMatchQuery {
+        final String TABLE = Tables.DATA + " dataA"
+                + " JOIN " + Tables.DATA + " dataB" +
+                " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE +
+                " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")"
+                + " JOIN " + Tables.RAW_CONTACTS +
+                " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+                + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+        final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
+                + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
+                + " AND dataA." + Identity.NAMESPACE + " NOT NULL"
+                + " AND dataA." + Identity.IDENTITY + " NOT NULL"
+                + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
+                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
+                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+
+        final String[] COLUMNS = new String[] {
+            RawContacts.CONTACT_ID
+        };
+
+        int CONTACT_ID = 0;
+    }
+
+    /**
+     * Finds contacts with exact identity matches to the the specified raw contact.
+     */
+    private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId,
+            ContactMatcher matcher) {
+        mSelectionArgs2[0] = String.valueOf(rawContactId);
+        mSelectionArgs2[1] = String.valueOf(mMimeTypeIdIdentity);
+        Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS,
+                IdentityLookupMatchQuery.SELECTION,
+                mSelectionArgs2, RawContacts.CONTACT_ID, null, null);
+        try {
+            while (c.moveToNext()) {
+                final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID);
+                matcher.matchIdentity(contactId);
+            }
+        } finally {
+            c.close();
+        }
+
+    }
+
+    private interface NameLookupMatchQuery {
+        String TABLE = Tables.NAME_LOOKUP + " nameA"
+                + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
+                " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
+                        + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
+                + " JOIN " + Tables.RAW_CONTACTS +
+                " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
+                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+        String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
+                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
+                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+
+        String[] COLUMNS = new String[] {
+            RawContacts.CONTACT_ID,
+            "nameA." + NameLookupColumns.NORMALIZED_NAME,
+            "nameA." + NameLookupColumns.NAME_TYPE,
+            "nameB." + NameLookupColumns.NAME_TYPE,
+        };
+
+        int CONTACT_ID = 0;
+        int NAME = 1;
+        int NAME_TYPE_A = 2;
+        int NAME_TYPE_B = 3;
+    }
+
+    /**
+     * Finds contacts with names matching the name of the specified raw contact.
+     */
+    private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
+            ContactMatcher matcher) {
+        mSelectionArgs1[0] = String.valueOf(rawContactId);
+        Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
+                NameLookupMatchQuery.SELECTION,
+                mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
+        try {
+            while (c.moveToNext()) {
+                long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
+                String name = c.getString(NameLookupMatchQuery.NAME);
+                int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
+                int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
+                matcher.matchName(contactId, nameTypeA, name,
+                        nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT);
+                if (nameTypeA == NameLookupType.NICKNAME &&
+                        nameTypeB == NameLookupType.NICKNAME) {
+                    matcher.updateScoreWithNicknameMatch(contactId);
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    private interface NameLookupMatchQueryWithParameter {
+        String TABLE = Tables.NAME_LOOKUP
+                + " JOIN " + Tables.RAW_CONTACTS +
+                " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = "
+                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+        String[] COLUMNS = new String[] {
+            RawContacts.CONTACT_ID,
+            NameLookupColumns.NORMALIZED_NAME,
+            NameLookupColumns.NAME_TYPE,
+        };
+
+        int CONTACT_ID = 0;
+        int NAME = 1;
+        int NAME_TYPE = 2;
+    }
+
+    private final class NameLookupSelectionBuilder extends NameLookupBuilder {
+
+        private final MatchCandidateList mNameLookupCandidates;
+
+        private StringBuilder mSelection = new StringBuilder(
+                NameLookupColumns.NORMALIZED_NAME + " IN(");
+
+
+        public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) {
+            super(splitter);
+            this.mNameLookupCandidates = candidates;
+        }
+
+        @Override
+        protected String[] getCommonNicknameClusters(String normalizedName) {
+            return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
+        }
+
+        @Override
+        protected void insertNameLookup(
+                long rawContactId, long dataId, int lookupType, String string) {
+            mNameLookupCandidates.add(string, lookupType);
+            DatabaseUtils.appendEscapedSQLString(mSelection, string);
+            mSelection.append(',');
+        }
+
+        public boolean isEmpty() {
+            return mNameLookupCandidates.isEmpty();
+        }
+
+        public String getSelection() {
+            mSelection.setLength(mSelection.length() - 1);      // Strip last comma
+            mSelection.append(')');
+            return mSelection.toString();
+        }
+
+        public int getLookupType(String name) {
+            for (int i = 0; i < mNameLookupCandidates.mCount; i++) {
+                if (mNameLookupCandidates.mList.get(i).mName.equals(name)) {
+                    return mNameLookupCandidates.mList.get(i).mLookupType;
+                }
+            }
+            throw new IllegalStateException();
+        }
+    }
+
+    /**
+     * Finds contacts with names matching the specified name.
+     */
+    private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
+            MatchCandidateList candidates, ContactMatcher matcher) {
+        candidates.clear();
+        NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
+                mNameSplitter, candidates);
+        builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
+        if (builder.isEmpty()) {
+            return;
+        }
+
+        Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
+                NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
+                null, PRIMARY_HIT_LIMIT_STRING);
+        try {
+            while (c.moveToNext()) {
+                long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
+                String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
+                int nameTypeA = builder.getLookupType(name);
+                int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
+                matcher.matchName(contactId, nameTypeA, name, nameTypeB, name,
+                        ContactMatcher.MATCHING_ALGORITHM_EXACT);
+                if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
+                    matcher.updateScoreWithNicknameMatch(contactId);
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    private interface EmailLookupQuery {
+        String TABLE = Tables.DATA + " dataA"
+                + " JOIN " + Tables.DATA + " dataB" +
+                " ON lower(" + "dataA." + Email.DATA + ")=lower(dataB." + Email.DATA + ")"
+                + " JOIN " + Tables.RAW_CONTACTS +
+                " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+        String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
+                + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
+                + " AND dataA." + Email.DATA + " NOT NULL"
+                + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
+                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
+                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+
+        String[] COLUMNS = new String[] {
+            RawContacts.CONTACT_ID
+        };
+
+        int CONTACT_ID = 0;
+    }
+
+    private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
+            ContactMatcher matcher) {
+        mSelectionArgs2[0] = String.valueOf(rawContactId);
+        mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail);
+        Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
+                EmailLookupQuery.SELECTION,
+                mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
+        try {
+            while (c.moveToNext()) {
+                long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
+                matcher.updateScoreWithEmailMatch(contactId);
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    private interface PhoneLookupQuery {
+        String TABLE = Tables.PHONE_LOOKUP + " phoneA"
+                + " JOIN " + Tables.DATA + " dataA"
+                + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
+                + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
+                + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
+                        + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
+                + " JOIN " + Tables.DATA + " dataB"
+                + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
+                + " JOIN " + Tables.RAW_CONTACTS
+                + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+        String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
+                + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
+                        + "dataB." + Phone.NUMBER + ",?)"
+                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
+                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+
+        String[] COLUMNS = new String[] {
+            RawContacts.CONTACT_ID
+        };
+
+        int CONTACT_ID = 0;
+    }
+
+    private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
+            ContactMatcher matcher) {
+        mSelectionArgs2[0] = String.valueOf(rawContactId);
+        mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
+        Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
+                PhoneLookupQuery.SELECTION,
+                mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
+        try {
+            while (c.moveToNext()) {
+                long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
+                matcher.updateScoreWithPhoneNumberMatch(contactId);
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Loads name lookup rows for approximate name matching and updates match scores based on that
+     * data.
+     */
+    private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
+            ContactMatcher matcher) {
+        HashSet<String> firstLetters = new HashSet<String>();
+        for (int i = 0; i < candidates.mCount; i++) {
+            final NameMatchCandidate candidate = candidates.mList.get(i);
+            if (candidate.mName.length() >= 2) {
+                String firstLetter = candidate.mName.substring(0, 2);
+                if (!firstLetters.contains(firstLetter)) {
+                    firstLetters.add(firstLetter);
+                    final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
+                            + firstLetter + "*') AND "
+                            + "(" + NameLookupColumns.NAME_TYPE + " IN("
+                                    + NameLookupType.NAME_COLLATION_KEY + ","
+                                    + NameLookupType.EMAIL_BASED_NICKNAME + ","
+                                    + NameLookupType.NICKNAME + ")) AND "
+                            + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+                    matchAllCandidates(db, selection, candidates, matcher,
+                            ContactMatcher.MATCHING_ALGORITHM_APPROXIMATE,
+                            String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT));
+                }
+            }
+        }
+    }
+
+    private interface ContactNameLookupQuery {
+        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
+
+        String[] COLUMNS = new String[] {
+                RawContacts.CONTACT_ID,
+                NameLookupColumns.NORMALIZED_NAME,
+                NameLookupColumns.NAME_TYPE
+        };
+
+        int CONTACT_ID = 0;
+        int NORMALIZED_NAME = 1;
+        int NAME_TYPE = 2;
+    }
+
+    /**
+     * Loads all candidate rows from the name lookup table and updates match scores based
+     * on that data.
+     */
+    private void matchAllCandidates(SQLiteDatabase db, String selection,
+            MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) {
+        final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
+                selection, null, null, null, null, limit);
+
+        try {
+            while (c.moveToNext()) {
+                Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
+                String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
+                int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
+
+                // Note the N^2 complexity of the following fragment. This is not a huge concern
+                // since the number of candidates is very small and in general secondary hits
+                // in the absence of primary hits are rare.
+                for (int i = 0; i < candidates.mCount; i++) {
+                    NameMatchCandidate candidate = candidates.mList.get(i);
+                    matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
+                            nameType, name, algorithm);
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    private interface RawContactsQuery {
+        String SQL_FORMAT_HAS_SUPER_PRIMARY_NAME =
+                " EXISTS(SELECT 1 " +
+                        " FROM " + Tables.DATA + " d " +
+                        " WHERE d." + DataColumns.MIMETYPE_ID + "=%d " +
+                        " AND d." + Data.RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID +
+                        " AND d." + Data.IS_SUPER_PRIMARY + "=1)";
+
+        String SQL_FORMAT =
+                "SELECT "
+                        + RawContactsColumns.CONCRETE_ID + ","
+                        + RawContactsColumns.DISPLAY_NAME + ","
+                        + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
+                        + AccountsColumns.CONCRETE_ACCOUNT_TYPE + ","
+                        + AccountsColumns.CONCRETE_ACCOUNT_NAME + ","
+                        + AccountsColumns.CONCRETE_DATA_SET + ","
+                        + RawContacts.SOURCE_ID + ","
+                        + RawContacts.CUSTOM_RINGTONE + ","
+                        + RawContacts.SEND_TO_VOICEMAIL + ","
+                        + RawContacts.LAST_TIME_CONTACTED + ","
+                        + RawContacts.TIMES_CONTACTED + ","
+                        + RawContacts.STARRED + ","
+                        + RawContacts.PINNED + ","
+                        + DataColumns.CONCRETE_ID + ","
+                        + DataColumns.CONCRETE_MIMETYPE_ID + ","
+                        + Data.IS_SUPER_PRIMARY + ","
+                        + Photo.PHOTO_FILE_ID + ","
+                        + SQL_FORMAT_HAS_SUPER_PRIMARY_NAME +
+                " FROM " + Tables.RAW_CONTACTS +
+                " JOIN " + Tables.ACCOUNTS + " ON ("
+                    + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+                    + ")" +
+                " LEFT OUTER JOIN " + Tables.DATA +
+                " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
+                        + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
+                                + " AND " + Photo.PHOTO + " NOT NULL)"
+                        + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
+                                + " AND " + Phone.NUMBER + " NOT NULL)))";
+
+        String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
+                " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
+
+        String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
+                " WHERE " + RawContacts.CONTACT_ID + "=?"
+                + " AND " + RawContacts.DELETED + "=0";
+
+        int RAW_CONTACT_ID = 0;
+        int DISPLAY_NAME = 1;
+        int DISPLAY_NAME_SOURCE = 2;
+        int ACCOUNT_TYPE = 3;
+        int ACCOUNT_NAME = 4;
+        int DATA_SET = 5;
+        int SOURCE_ID = 6;
+        int CUSTOM_RINGTONE = 7;
+        int SEND_TO_VOICEMAIL = 8;
+        int LAST_TIME_CONTACTED = 9;
+        int TIMES_CONTACTED = 10;
+        int STARRED = 11;
+        int PINNED = 12;
+        int DATA_ID = 13;
+        int MIMETYPE_ID = 14;
+        int IS_SUPER_PRIMARY = 15;
+        int PHOTO_FILE_ID = 16;
+        int HAS_SUPER_PRIMARY_NAME = 17;
+    }
+
+    private interface ContactReplaceSqlStatement {
+        String UPDATE_SQL =
+                "UPDATE " + Tables.CONTACTS +
+                " SET "
+                        + Contacts.NAME_RAW_CONTACT_ID + "=?, "
+                        + Contacts.PHOTO_ID + "=?, "
+                        + Contacts.PHOTO_FILE_ID + "=?, "
+                        + Contacts.SEND_TO_VOICEMAIL + "=?, "
+                        + Contacts.CUSTOM_RINGTONE + "=?, "
+                        + Contacts.LAST_TIME_CONTACTED + "=?, "
+                        + Contacts.TIMES_CONTACTED + "=?, "
+                        + Contacts.STARRED + "=?, "
+                        + Contacts.PINNED + "=?, "
+                        + Contacts.HAS_PHONE_NUMBER + "=?, "
+                        + Contacts.LOOKUP_KEY + "=?, "
+                        + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=? " +
+                " WHERE " + Contacts._ID + "=?";
+
+        String INSERT_SQL =
+                "INSERT INTO " + Tables.CONTACTS + " ("
+                        + Contacts.NAME_RAW_CONTACT_ID + ", "
+                        + Contacts.PHOTO_ID + ", "
+                        + Contacts.PHOTO_FILE_ID + ", "
+                        + Contacts.SEND_TO_VOICEMAIL + ", "
+                        + Contacts.CUSTOM_RINGTONE + ", "
+                        + Contacts.LAST_TIME_CONTACTED + ", "
+                        + Contacts.TIMES_CONTACTED + ", "
+                        + Contacts.STARRED + ", "
+                        + Contacts.PINNED + ", "
+                        + Contacts.HAS_PHONE_NUMBER + ", "
+                        + Contacts.LOOKUP_KEY + ", "
+                        + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
+                        + ") " +
+                " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
+
+        int NAME_RAW_CONTACT_ID = 1;
+        int PHOTO_ID = 2;
+        int PHOTO_FILE_ID = 3;
+        int SEND_TO_VOICEMAIL = 4;
+        int CUSTOM_RINGTONE = 5;
+        int LAST_TIME_CONTACTED = 6;
+        int TIMES_CONTACTED = 7;
+        int STARRED = 8;
+        int PINNED = 9;
+        int HAS_PHONE_NUMBER = 10;
+        int LOOKUP_KEY = 11;
+        int CONTACT_LAST_UPDATED_TIMESTAMP = 12;
+        int CONTACT_ID = 13;
+    }
+
+    /**
+     * Computes aggregate-level data for the specified aggregate contact ID.
+     */
+    private void computeAggregateData(SQLiteDatabase db, long contactId,
+            SQLiteStatement statement) {
+        mSelectionArgs1[0] = String.valueOf(contactId);
+        computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
+    }
+
+    /**
+     * Indicates whether the given photo entry and priority gives this photo a higher overall
+     * priority than the current best photo entry and priority.
+     */
+    private boolean hasHigherPhotoPriority(PhotoEntry photoEntry, int priority,
+            PhotoEntry bestPhotoEntry, int bestPriority) {
+        int photoComparison = photoEntry.compareTo(bestPhotoEntry);
+        return photoComparison < 0 || photoComparison == 0 && priority > bestPriority;
+    }
+
+    /**
+     * Computes aggregate-level data from constituent raw contacts.
+     */
+    private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
+            SQLiteStatement statement) {
+        long currentRawContactId = -1;
+        long bestPhotoId = -1;
+        long bestPhotoFileId = 0;
+        PhotoEntry bestPhotoEntry = null;
+        boolean foundSuperPrimaryPhoto = false;
+        int photoPriority = -1;
+        int totalRowCount = 0;
+        int contactSendToVoicemail = 0;
+        String contactCustomRingtone = null;
+        long contactLastTimeContacted = 0;
+        int contactTimesContacted = 0;
+        int contactStarred = 0;
+        int contactPinned = Integer.MAX_VALUE;
+        int hasPhoneNumber = 0;
+        StringBuilder lookupKey = new StringBuilder();
+
+        mDisplayNameCandidate.clear();
+
+        Cursor c = db.rawQuery(sql, sqlArgs);
+        try {
+            while (c.moveToNext()) {
+                long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
+                if (rawContactId != currentRawContactId) {
+                    currentRawContactId = rawContactId;
+                    totalRowCount++;
+
+                    // Assemble sub-account.
+                    String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
+                    String dataSet = c.getString(RawContactsQuery.DATA_SET);
+                    String accountWithDataSet = (!TextUtils.isEmpty(dataSet))
+                            ? accountType + "/" + dataSet
+                            : accountType;
+
+                    // Display name
+                    String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
+                    int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
+                    int isNameSuperPrimary = c.getInt(RawContactsQuery.HAS_SUPER_PRIMARY_NAME);
+                    processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
+                            mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet),
+                            isNameSuperPrimary != 0);
+
+                    // Contact options
+                    if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
+                        boolean sendToVoicemail =
+                                (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0);
+                        if (sendToVoicemail) {
+                            contactSendToVoicemail++;
+                        }
+                    }
+
+                    if (contactCustomRingtone == null
+                            && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) {
+                        contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
+                    }
+
+                    long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
+                    if (lastTimeContacted > contactLastTimeContacted) {
+                        contactLastTimeContacted = lastTimeContacted;
+                    }
+
+                    int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
+                    if (timesContacted > contactTimesContacted) {
+                        contactTimesContacted = timesContacted;
+                    }
+
+                    if (c.getInt(RawContactsQuery.STARRED) != 0) {
+                        contactStarred = 1;
+                    }
+
+                    // contactPinned should be the lowest value of its constituent raw contacts,
+                    // excluding negative integers
+                    final int rawContactPinned = c.getInt(RawContactsQuery.PINNED);
+                    if (rawContactPinned > PinnedPositions.UNPINNED) {
+                        contactPinned = Math.min(contactPinned, rawContactPinned);
+                    }
+
+                    appendLookupKey(
+                            lookupKey,
+                            accountWithDataSet,
+                            c.getString(RawContactsQuery.ACCOUNT_NAME),
+                            rawContactId,
+                            c.getString(RawContactsQuery.SOURCE_ID),
+                            displayName);
+                }
+
+                if (!c.isNull(RawContactsQuery.DATA_ID)) {
+                    long dataId = c.getLong(RawContactsQuery.DATA_ID);
+                    long photoFileId = c.getLong(RawContactsQuery.PHOTO_FILE_ID);
+                    int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
+                    boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
+                    if (mimetypeId == mMimeTypeIdPhoto) {
+                        if (!foundSuperPrimaryPhoto) {
+                            // Lookup the metadata for the photo, if available.  Note that data set
+                            // does not come into play here, since accounts are looked up in the
+                            // account manager in the priority resolver.
+                            PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
+                            String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
+                            int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
+                            if (superPrimary || hasHigherPhotoPriority(
+                                    photoEntry, priority, bestPhotoEntry, photoPriority)) {
+                                bestPhotoEntry = photoEntry;
+                                photoPriority = priority;
+                                bestPhotoId = dataId;
+                                bestPhotoFileId = photoFileId;
+                                foundSuperPrimaryPhoto |= superPrimary;
+                            }
+                        }
+                    } else if (mimetypeId == mMimeTypeIdPhone) {
+                        hasPhoneNumber = 1;
+                    }
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        if (contactPinned == Integer.MAX_VALUE) {
+            contactPinned = PinnedPositions.UNPINNED;
+        }
+
+        statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID,
+                mDisplayNameCandidate.rawContactId);
+
+        if (bestPhotoId != -1) {
+            statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
+        } else {
+            statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
+        }
+
+        if (bestPhotoFileId != 0) {
+            statement.bindLong(ContactReplaceSqlStatement.PHOTO_FILE_ID, bestPhotoFileId);
+        } else {
+            statement.bindNull(ContactReplaceSqlStatement.PHOTO_FILE_ID);
+        }
+
+        statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
+                totalRowCount == contactSendToVoicemail ? 1 : 0);
+        DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
+                contactCustomRingtone);
+        statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
+                contactLastTimeContacted);
+        statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
+                contactTimesContacted);
+        statement.bindLong(ContactReplaceSqlStatement.STARRED,
+                contactStarred);
+        statement.bindLong(ContactReplaceSqlStatement.PINNED,
+                contactPinned);
+        statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
+                hasPhoneNumber);
+        statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
+                Uri.encode(lookupKey.toString()));
+        statement.bindLong(ContactReplaceSqlStatement.CONTACT_LAST_UPDATED_TIMESTAMP,
+                Clock.getInstance().currentTimeMillis());
+    }
+
+    /**
+     * Builds a lookup key using the given data.
+     */
+    protected void appendLookupKey(StringBuilder sb, String accountTypeWithDataSet,
+            String accountName, long rawContactId, String sourceId, String displayName) {
+        ContactLookupKey.appendToLookupKey(sb, accountTypeWithDataSet, accountName, rawContactId,
+                sourceId, displayName);
+    }
+
+    /**
+     * Uses the supplied values to determine if they represent a "better" display name
+     * for the aggregate contact currently evaluated.  If so, it updates
+     * {@link #mDisplayNameCandidate} with the new values.
+     */
+    private void processDisplayNameCandidate(long rawContactId, String displayName,
+            int displayNameSource, boolean writableAccount, boolean isNameSuperPrimary) {
+
+        boolean replace = false;
+        if (mDisplayNameCandidate.rawContactId == -1) {
+            // No previous values available
+            replace = true;
+        } else if (!TextUtils.isEmpty(displayName)) {
+            if (isNameSuperPrimary) {
+                // A super primary name is better than any other name
+                replace = true;
+            } else if (mDisplayNameCandidate.isNameSuperPrimary == isNameSuperPrimary) {
+                if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
+                    // New values come from an superior source, e.g. structured name vs phone number
+                    replace = true;
+                } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) {
+                    if (!mDisplayNameCandidate.writableAccount && writableAccount) {
+                        replace = true;
+                    } else if (mDisplayNameCandidate.writableAccount == writableAccount) {
+                        if (NameNormalizer.compareComplexity(displayName,
+                                mDisplayNameCandidate.displayName) > 0) {
+                            // New name is more complex than the previously found one
+                            replace = true;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (replace) {
+            mDisplayNameCandidate.rawContactId = rawContactId;
+            mDisplayNameCandidate.displayName = displayName;
+            mDisplayNameCandidate.displayNameSource = displayNameSource;
+            mDisplayNameCandidate.isNameSuperPrimary = isNameSuperPrimary;
+            mDisplayNameCandidate.writableAccount = writableAccount;
+        }
+    }
+
+    private interface PhotoIdQuery {
+        final String[] COLUMNS = new String[] {
+            AccountsColumns.CONCRETE_ACCOUNT_TYPE,
+            DataColumns.CONCRETE_ID,
+            Data.IS_SUPER_PRIMARY,
+            Photo.PHOTO_FILE_ID,
+        };
+
+        int ACCOUNT_TYPE = 0;
+        int DATA_ID = 1;
+        int IS_SUPER_PRIMARY = 2;
+        int PHOTO_FILE_ID = 3;
+    }
+
+    public void updatePhotoId(SQLiteDatabase db, long rawContactId) {
+
+        long contactId = mDbHelper.getContactId(rawContactId);
+        if (contactId == 0) {
+            return;
+        }
+
+        long bestPhotoId = -1;
+        long bestPhotoFileId = 0;
+        int photoPriority = -1;
+
+        long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
+
+        String tables = Tables.RAW_CONTACTS
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                    + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+                    + ")"
+                + " JOIN " + Tables.DATA + " ON("
+                + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
+                + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
+                        + Photo.PHOTO + " NOT NULL))";
+
+        mSelectionArgs1[0] = String.valueOf(contactId);
+        final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
+                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
+        try {
+            PhotoEntry bestPhotoEntry = null;
+            while (c.moveToNext()) {
+                long dataId = c.getLong(PhotoIdQuery.DATA_ID);
+                long photoFileId = c.getLong(PhotoIdQuery.PHOTO_FILE_ID);
+                boolean superPrimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0;
+                PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
+
+                // Note that data set does not come into play here, since accounts are looked up in
+                // the account manager in the priority resolver.
+                String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE);
+                int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
+                if (superPrimary || hasHigherPhotoPriority(
+                        photoEntry, priority, bestPhotoEntry, photoPriority)) {
+                    bestPhotoEntry = photoEntry;
+                    photoPriority = priority;
+                    bestPhotoId = dataId;
+                    bestPhotoFileId = photoFileId;
+                    if (superPrimary) {
+                        break;
+                    }
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        if (bestPhotoId == -1) {
+            mPhotoIdUpdate.bindNull(1);
+        } else {
+            mPhotoIdUpdate.bindLong(1, bestPhotoId);
+        }
+
+        if (bestPhotoFileId == 0) {
+            mPhotoIdUpdate.bindNull(2);
+        } else {
+            mPhotoIdUpdate.bindLong(2, bestPhotoFileId);
+        }
+
+        mPhotoIdUpdate.bindLong(3, contactId);
+        mPhotoIdUpdate.execute();
+    }
+
+    private interface PhotoFileQuery {
+        final String[] COLUMNS = new String[] {
+                PhotoFiles.HEIGHT,
+                PhotoFiles.WIDTH,
+                PhotoFiles.FILESIZE
+        };
+
+        int HEIGHT = 0;
+        int WIDTH = 1;
+        int FILESIZE = 2;
+    }
+
+    private class PhotoEntry implements Comparable<PhotoEntry> {
+        // Pixel count (width * height) for the image.
+        final int pixelCount;
+
+        // File size (in bytes) of the image.  Not populated if the image is a thumbnail.
+        final int fileSize;
+
+        private PhotoEntry(int pixelCount, int fileSize) {
+            this.pixelCount = pixelCount;
+            this.fileSize = fileSize;
+        }
+
+        @Override
+        public int compareTo(PhotoEntry pe) {
+            if (pe == null) {
+                return -1;
+            }
+            if (pixelCount == pe.pixelCount) {
+                return pe.fileSize - fileSize;
+            } else {
+                return pe.pixelCount - pixelCount;
+            }
+        }
+    }
+
+    private PhotoEntry getPhotoMetadata(SQLiteDatabase db, long photoFileId) {
+        if (photoFileId == 0) {
+            // Assume standard thumbnail size.  Don't bother getting a file size for priority;
+            // we should fall back to photo priority resolver if all we have are thumbnails.
+            int thumbDim = mContactsProvider.getMaxThumbnailDim();
+            return new PhotoEntry(thumbDim * thumbDim, 0);
+        } else {
+            Cursor c = db.query(Tables.PHOTO_FILES, PhotoFileQuery.COLUMNS, PhotoFiles._ID + "=?",
+                    new String[]{String.valueOf(photoFileId)}, null, null, null);
+            try {
+                if (c.getCount() == 1) {
+                    c.moveToFirst();
+                    int pixelCount =
+                            c.getInt(PhotoFileQuery.HEIGHT) * c.getInt(PhotoFileQuery.WIDTH);
+                    return new PhotoEntry(pixelCount, c.getInt(PhotoFileQuery.FILESIZE));
+                }
+            } finally {
+                c.close();
+            }
+        }
+        return new PhotoEntry(0, 0);
+    }
+
+    private interface DisplayNameQuery {
+        String SQL_HAS_SUPER_PRIMARY_NAME =
+                " EXISTS(SELECT 1 " +
+                        " FROM " + Tables.DATA + " d " +
+                        " WHERE d." + DataColumns.MIMETYPE_ID + "=? " +
+                        " AND d." + Data.RAW_CONTACT_ID + "=" + Views.RAW_CONTACTS
+                        + "." + RawContacts._ID +
+                        " AND d." + Data.IS_SUPER_PRIMARY + "=1)";
+
+        String SQL =
+                "SELECT "
+                        + RawContacts._ID + ","
+                        + RawContactsColumns.DISPLAY_NAME + ","
+                        + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
+                        + SQL_HAS_SUPER_PRIMARY_NAME + ","
+                        + RawContacts.SOURCE_ID + ","
+                        + RawContacts.ACCOUNT_TYPE_AND_DATA_SET +
+                " FROM " + Views.RAW_CONTACTS +
+                " WHERE " + RawContacts.CONTACT_ID + "=? ";
+
+        int _ID = 0;
+        int DISPLAY_NAME = 1;
+        int DISPLAY_NAME_SOURCE = 2;
+        int HAS_SUPER_PRIMARY_NAME = 3;
+        int SOURCE_ID = 4;
+        int ACCOUNT_TYPE_AND_DATA_SET = 5;
+    }
+
+    public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
+        long contactId = mDbHelper.getContactId(rawContactId);
+        if (contactId == 0) {
+            return;
+        }
+
+        updateDisplayNameForContact(db, contactId);
+    }
+
+    public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
+        boolean lookupKeyUpdateNeeded = false;
+
+        mDisplayNameCandidate.clear();
+
+        mSelectionArgs2[0] = String.valueOf(mDbHelper.getMimeTypeIdForStructuredName());
+        mSelectionArgs2[1] = String.valueOf(contactId);
+        final Cursor c = db.rawQuery(DisplayNameQuery.SQL, mSelectionArgs2);
+        try {
+            while (c.moveToNext()) {
+                long rawContactId = c.getLong(DisplayNameQuery._ID);
+                String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
+                int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
+                int isNameSuperPrimary = c.getInt(DisplayNameQuery.HAS_SUPER_PRIMARY_NAME);
+                String accountTypeAndDataSet = c.getString(
+                        DisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET);
+                processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
+                        mContactsProvider.isWritableAccountWithDataSet(accountTypeAndDataSet),
+                        isNameSuperPrimary != 0);
+
+                // If the raw contact has no source id, the lookup key is based on the display
+                // name, so the lookup key needs to be updated.
+                lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID);
+            }
+        } finally {
+            c.close();
+        }
+
+        if (mDisplayNameCandidate.rawContactId != -1) {
+            mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
+            mDisplayNameUpdate.bindLong(2, contactId);
+            mDisplayNameUpdate.execute();
+        }
+
+        if (lookupKeyUpdateNeeded) {
+            updateLookupKeyForContact(db, contactId);
+        }
+    }
+
+
+    /**
+     * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the
+     * specified raw contact.
+     */
+    public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) {
+
+        long contactId = mDbHelper.getContactId(rawContactId);
+        if (contactId == 0) {
+            return;
+        }
+
+        final SQLiteStatement hasPhoneNumberUpdate = db.compileStatement(
+                "UPDATE " + Tables.CONTACTS +
+                " SET " + Contacts.HAS_PHONE_NUMBER + "="
+                        + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)"
+                        + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS
+                        + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
+                                + " AND " + Phone.NUMBER + " NOT NULL"
+                                + " AND " + RawContacts.CONTACT_ID + "=?)" +
+                " WHERE " + Contacts._ID + "=?");
+        try {
+            hasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE));
+            hasPhoneNumberUpdate.bindLong(2, contactId);
+            hasPhoneNumberUpdate.bindLong(3, contactId);
+            hasPhoneNumberUpdate.execute();
+        } finally {
+            hasPhoneNumberUpdate.close();
+        }
+    }
+
+    private interface LookupKeyQuery {
+        String TABLE = Views.RAW_CONTACTS;
+        String[] COLUMNS = new String[] {
+            RawContacts._ID,
+            RawContactsColumns.DISPLAY_NAME,
+            RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
+            RawContacts.ACCOUNT_NAME,
+            RawContacts.SOURCE_ID,
+        };
+
+        int ID = 0;
+        int DISPLAY_NAME = 1;
+        int ACCOUNT_TYPE_AND_DATA_SET = 2;
+        int ACCOUNT_NAME = 3;
+        int SOURCE_ID = 4;
+    }
+
+    public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
+        long contactId = mDbHelper.getContactId(rawContactId);
+        if (contactId == 0) {
+            return;
+        }
+
+        updateLookupKeyForContact(db, contactId);
+    }
+
+    private void updateLookupKeyForContact(SQLiteDatabase db, long contactId) {
+        String lookupKey = computeLookupKeyForContact(db, contactId);
+
+        if (lookupKey == null) {
+            mLookupKeyUpdate.bindNull(1);
+        } else {
+            mLookupKeyUpdate.bindString(1, Uri.encode(lookupKey));
+        }
+        mLookupKeyUpdate.bindLong(2, contactId);
+
+        mLookupKeyUpdate.execute();
+    }
+
+    protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) {
+        StringBuilder sb = new StringBuilder();
+        mSelectionArgs1[0] = String.valueOf(contactId);
+        final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS,
+                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID);
+        try {
+            while (c.moveToNext()) {
+                ContactLookupKey.appendToLookupKey(sb,
+                        c.getString(LookupKeyQuery.ACCOUNT_TYPE_AND_DATA_SET),
+                        c.getString(LookupKeyQuery.ACCOUNT_NAME),
+                        c.getLong(LookupKeyQuery.ID),
+                        c.getString(LookupKeyQuery.SOURCE_ID),
+                        c.getString(LookupKeyQuery.DISPLAY_NAME));
+            }
+        } finally {
+            c.close();
+        }
+        return sb.length() == 0 ? null : sb.toString();
+    }
+
+    /**
+     * Execute {@link SQLiteStatement} that will update the
+     * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}.
+     */
+    public void updateStarred(long rawContactId) {
+        long contactId = mDbHelper.getContactId(rawContactId);
+        if (contactId == 0) {
+            return;
+        }
+
+        mStarredUpdate.bindLong(1, contactId);
+        mStarredUpdate.execute();
+    }
+
+    /**
+     * Execute {@link SQLiteStatement} that will update the
+     * {@link Contacts#PINNED} flag for the given {@link RawContacts#_ID}.
+     */
+    public void updatePinned(long rawContactId) {
+        long contactId = mDbHelper.getContactId(rawContactId);
+        if (contactId == 0) {
+            return;
+        }
+        mPinnedUpdate.bindLong(1, contactId);
+        mPinnedUpdate.execute();
+    }
+
+    /**
+     * Finds matching contacts and returns a cursor on those.
+     */
+    public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb,
+            String[] projection, long contactId, int maxSuggestions, String filter,
+            ArrayList<AggregationSuggestionParameter> parameters) {
+        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
+        db.beginTransaction();
+        try {
+            List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters);
+            return queryMatchingContacts(qb, db, projection, bestMatches, maxSuggestions, filter);
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    private interface ContactIdQuery {
+        String[] COLUMNS = new String[] {
+            Contacts._ID
+        };
+
+        int _ID = 0;
+    }
+
+    /**
+     * Loads contacts with specified IDs and returns them in the order of IDs in the
+     * supplied list.
+     */
+    private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db,
+            String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(Contacts._ID);
+        sb.append(" IN (");
+        for (int i = 0; i < bestMatches.size(); i++) {
+            MatchScore matchScore = bestMatches.get(i);
+            if (i != 0) {
+                sb.append(",");
+            }
+            sb.append(matchScore.getContactId());
+        }
+        sb.append(")");
+
+        if (!TextUtils.isEmpty(filter)) {
+            sb.append(" AND " + Contacts._ID + " IN ");
+            mContactsProvider.appendContactFilterAsNestedQuery(sb, filter);
+        }
+
+        // Run a query and find ids of best matching contacts satisfying the filter (if any)
+        HashSet<Long> foundIds = new HashSet<Long>();
+        Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(),
+                null, null, null, null);
+        try {
+            while(cursor.moveToNext()) {
+                foundIds.add(cursor.getLong(ContactIdQuery._ID));
+            }
+        } finally {
+            cursor.close();
+        }
+
+        // Exclude all contacts that did not match the filter
+        Iterator<MatchScore> iter = bestMatches.iterator();
+        while (iter.hasNext()) {
+            long id = iter.next().getContactId();
+            if (!foundIds.contains(id)) {
+                iter.remove();
+            }
+        }
+
+        // Limit the number of returned suggestions
+        final List<MatchScore> limitedMatches;
+        if (bestMatches.size() > maxSuggestions) {
+            limitedMatches = bestMatches.subList(0, maxSuggestions);
+        } else {
+            limitedMatches = bestMatches;
+        }
+
+        // Build an in-clause with the remaining contact IDs
+        sb.setLength(0);
+        sb.append(Contacts._ID);
+        sb.append(" IN (");
+        for (int i = 0; i < limitedMatches.size(); i++) {
+            MatchScore matchScore = limitedMatches.get(i);
+            if (i != 0) {
+                sb.append(",");
+            }
+            sb.append(matchScore.getContactId());
+        }
+        sb.append(")");
+
+        // Run the final query with the required projection and contact IDs found by the first query
+        cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID);
+
+        // Build a sorted list of discovered IDs
+        ArrayList<Long> sortedContactIds = new ArrayList<Long>(limitedMatches.size());
+        for (MatchScore matchScore : limitedMatches) {
+            sortedContactIds.add(matchScore.getContactId());
+        }
+
+        Collections.sort(sortedContactIds);
+
+        // Map cursor indexes according to the descending order of match scores
+        int[] positionMap = new int[limitedMatches.size()];
+        for (int i = 0; i < positionMap.length; i++) {
+            long id = limitedMatches.get(i).getContactId();
+            positionMap[i] = sortedContactIds.indexOf(id);
+        }
+
+        return new ReorderingCursorWrapper(cursor, positionMap);
+    }
+
+    /**
+     * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
+     * descending order of match score.
+     * @param parameters
+     */
+    private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
+            ArrayList<AggregationSuggestionParameter> parameters) {
+
+        MatchCandidateList candidates = new MatchCandidateList();
+        ContactMatcher matcher = new ContactMatcher();
+
+        // Don't aggregate a contact with itself
+        matcher.keepOut(contactId);
+
+        if (parameters == null || parameters.size() == 0) {
+            final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
+                    RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+            try {
+                while (c.moveToNext()) {
+                    long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID);
+                    updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
+                            matcher);
+                }
+            } finally {
+                c.close();
+            }
+        } else {
+            updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates,
+                    matcher, parameters);
+        }
+
+        return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST);
+    }
+
+    /**
+     * Computes scores for contacts that have matching data rows.
+     */
+    private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
+            long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
+
+        updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
+        updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+        updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+        updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+        loadNameMatchCandidates(db, rawContactId, candidates, false);
+        lookupApproximateNameMatches(db, candidates, matcher);
+    }
+
+    private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
+            MatchCandidateList candidates, ContactMatcher matcher,
+            ArrayList<AggregationSuggestionParameter> parameters) {
+        for (AggregationSuggestionParameter parameter : parameters) {
+            if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) {
+                updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher);
+            }
+
+            // TODO: add support for other parameter kinds
+        }
+    }
+}