am 1b6c85b9: (-s ours) DO NOT MERGE cherry-pick of CL I0ff20aa2 from eclair-mr2

Merge commit '1b6c85b96f5cccae94d8bad6ea727accacfb7a0f' into eclair-mr2

* commit '1b6c85b96f5cccae94d8bad6ea727accacfb7a0f':
  DO NOT MERGE cherry-pick of CL I0ff20aa2 from eclair-mr2
diff --git a/src/com/android/providers/contacts/ContactAggregationScheduler.java b/src/com/android/providers/contacts/ContactAggregationScheduler.java
deleted file mode 100644
index 497b6a3..0000000
--- a/src/com/android/providers/contacts/ContactAggregationScheduler.java
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Copyright (C) 2009 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;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.util.Log;
-
-/**
- * A scheduler for asynchronous aggregation of contacts. Aggregation will start after
- * a short delay after it is scheduled, unless it is scheduled again, in which case the
- * aggregation pass is delayed.  There is an upper boundary on how long aggregation can
- * be delayed.
- */
-public class ContactAggregationScheduler {
-
-    private static final String TAG = "ContactAggregator";
-
-    public interface Aggregator {
-
-        /**
-         * Performs an aggregation run.
-         */
-        void run();
-
-        /**
-         * Interrupts aggregation.
-         */
-        void interrupt();
-    }
-
-    // Message ID used for communication with the aggregator
-    private static final int START_AGGREGATION_MESSAGE_ID = 1;
-
-    // Aggregation is delayed by this many milliseconds to allow changes to accumulate
-    public static final int AGGREGATION_DELAY = 1000;
-
-    // Maximum delay of aggregation from the initial aggregation request
-    public static final int MAX_AGGREGATION_DELAY = 10000;
-
-    // Minimum gap between requests that should cause a delay of aggregation
-    public static final int DELAYED_EXECUTION_TIMEOUT = 500;
-
-    public static final int STATUS_STAND_BY = 0;
-    public static final int STATUS_SCHEDULED = 1;
-    public static final int STATUS_RUNNING = 2;
-    public static final int STATUS_INTERRUPTED = 3;
-
-
-    private Aggregator mAggregator;
-
-    // Aggregation status
-    private int mStatus = STATUS_STAND_BY;
-
-    // If true, we need to automatically reschedule aggregation after the current pass is done
-    private boolean mRescheduleWhenComplete;
-
-    // The time when aggregation was requested for the first time.
-    // Reset when aggregation is completed
-    private long mInitialRequestTimestamp;
-
-    // Last time aggregation was requested
-    private long mLastAggregationEndedTimestamp;
-
-    private HandlerThread mHandlerThread;
-    private Handler mMessageHandler;
-
-    public void setAggregator(Aggregator aggregator) {
-        mAggregator = aggregator;
-    }
-
-    public void start() {
-        mHandlerThread = new HandlerThread("ContactAggregator", Process.THREAD_PRIORITY_BACKGROUND);
-        mHandlerThread.start();
-        mMessageHandler = new Handler(mHandlerThread.getLooper()) {
-
-            @Override
-            public void handleMessage(Message msg) {
-                switch (msg.what) {
-                    case START_AGGREGATION_MESSAGE_ID:
-                        run();
-                        break;
-
-                    default:
-                        throw new IllegalStateException("Unhandled message: " + msg.what);
-                }
-            }
-        };
-    }
-
-    public void stop() {
-        mAggregator.interrupt();
-        Looper looper = mHandlerThread.getLooper();
-        if (looper != null) {
-            looper.quit();
-        }
-    }
-
-    /**
-     * Schedules an aggregation pass after a short delay.
-     */
-    public synchronized void schedule() {
-
-        switch (mStatus) {
-            case STATUS_STAND_BY: {
-
-                mInitialRequestTimestamp = currentTime();
-                mStatus = STATUS_SCHEDULED;
-                if (mInitialRequestTimestamp - mLastAggregationEndedTimestamp <
-                        DELAYED_EXECUTION_TIMEOUT) {
-                    runDelayed();
-                } else {
-                    runNow();
-                }
-                break;
-            }
-
-            case STATUS_INTERRUPTED: {
-
-                // If the previous aggregation run was interrupted, do not reset
-                // the initial request timestamp - we don't want a continuous string
-                // of interrupted runs
-                mStatus = STATUS_SCHEDULED;
-                runDelayed();
-                break;
-            }
-
-            case STATUS_SCHEDULED: {
-
-                // If it has been less than MAX_AGGREGATION_DELAY millis since the initial request,
-                // reschedule the request.
-                if (currentTime() - mInitialRequestTimestamp < MAX_AGGREGATION_DELAY) {
-                    runDelayed();
-                }
-                break;
-            }
-
-            case STATUS_RUNNING: {
-
-                // If it has been less than MAX_AGGREGATION_DELAY millis since the initial request,
-                // interrupt the current pass and reschedule the request.
-                if (currentTime() - mInitialRequestTimestamp < MAX_AGGREGATION_DELAY) {
-                    mAggregator.interrupt();
-                    mStatus = STATUS_INTERRUPTED;
-                }
-
-                mRescheduleWhenComplete = true;
-                break;
-            }
-        }
-    }
-
-    /**
-     * Called just before an aggregation pass begins.
-     */
-    public void run() {
-        synchronized (this) {
-            mStatus = STATUS_RUNNING;
-            mRescheduleWhenComplete = false;
-        }
-        try {
-            mAggregator.run();
-        } finally {
-            mLastAggregationEndedTimestamp = currentTime();
-            synchronized (this) {
-                if (mStatus == STATUS_RUNNING) {
-                    mStatus = STATUS_STAND_BY;
-                }
-                if (mRescheduleWhenComplete) {
-                    mRescheduleWhenComplete = false;
-                    schedule();
-                } else {
-                    Log.w(TAG, "No more aggregation requests");
-                }
-            }
-        }
-    }
-
-    /* package */ void runNow() {
-
-        // If aggregation has already been requested, cancel the previous request
-        mMessageHandler.removeMessages(START_AGGREGATION_MESSAGE_ID);
-
-        // Schedule aggregation for right now
-        mMessageHandler.sendEmptyMessage(START_AGGREGATION_MESSAGE_ID);
-    }
-
-    /* package */ void runDelayed() {
-
-        // If aggregation has already been requested, cancel the previous request
-        mMessageHandler.removeMessages(START_AGGREGATION_MESSAGE_ID);
-
-        // Schedule aggregation for AGGREGATION_DELAY milliseconds from now
-        mMessageHandler.sendEmptyMessageDelayed(
-                START_AGGREGATION_MESSAGE_ID, AGGREGATION_DELAY);
-    }
-
-    /* package */ long currentTime() {
-        return System.currentTimeMillis();
-    }
-}
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
index 4475a83..8d72cc8 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -21,9 +21,9 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DisplayNameSources;
-import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
 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.Tables;
@@ -36,19 +36,14 @@
 import android.database.sqlite.SQLiteStatement;
 import android.net.Uri;
 import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.CommonDataKinds;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.text.TextUtils;
-import android.text.util.Rfc822Token;
-import android.text.util.Rfc822Tokenizer;
 import android.util.EventLog;
 import android.util.Log;
 
@@ -63,91 +58,18 @@
  * 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.
- * <p>
- * ContactAggregator runs on a separate thread.
  */
-public class ContactAggregator implements ContactAggregationScheduler.Aggregator {
+public class ContactAggregator {
 
     private static final String TAG = "ContactAggregator";
 
-    private interface DataMimetypeQuery {
+    private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
 
-        // Data mime types used in the contact matching algorithm
-        String MIMETYPE_SELECTION_IN_CLAUSE = MimetypesColumns.MIMETYPE + " IN ('"
-                + Email.CONTENT_ITEM_TYPE + "','"
-                + Nickname.CONTENT_ITEM_TYPE + "','"
-                + Phone.CONTENT_ITEM_TYPE + "','"
-                + StructuredName.CONTENT_ITEM_TYPE + "')";
-
-        String[] COLUMNS = new String[] {
-                MimetypesColumns.MIMETYPE, Data.DATA1
-        };
-
-        int MIMETYPE = 0;
-        int DATA1 = 1;
-    }
-
-    private interface DataContactIdQuery {
-        String TABLE = Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS;
-
-        String[] COLUMNS = new String[] {
-                Data.DATA1, RawContacts.CONTACT_ID
-        };
-
-        int DATA1 = 0;
-        int CONTACT_ID = 1;
-    }
-
-    private interface NameLookupQuery {
-        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;
-    }
-
-    private interface EmailLookupQuery {
-        String TABLE = Tables.DATA_JOIN_RAW_CONTACTS;
-
-        String[] COLUMNS = new String[] {
-            RawContacts.CONTACT_ID
-        };
-
-        int CONTACT_ID = 0;
-    }
-
-    private interface ContactIdQuery {
-        String TABLE = Tables.CONTACTS;
-
-        String[] COLUMNS = new String[] {
-            Contacts._ID
-        };
-
-        int _ID = 0;
-    }
-
-    private static final String[] CONTACT_ID_COLUMN = new String[] { RawContacts._ID };
-    private static final String[] CONTACT_ID_COLUMNS = new String[]{ RawContacts.CONTACT_ID };
-    private static final int COL_CONTACT_ID = 0;
-
-    private static final int MODE_AGGREGATION = 0;
-    private static final int MODE_SUGGESTIONS = 1;
-
-    /**
-     * When yielding the transaction to another thread, sleep for this many milliseconds
-     * to allow the other thread to build up a transaction before yielding back.
-     */
-    private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
-
-    /**
-     * The maximum number of contacts aggregated in a single transaction.
-     */
-    private static final int MAX_TRANSACTION_SIZE = 50;
+    private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
+            NameLookupColumns.NAME_TYPE + " IN ("
+                    + NameLookupType.NAME_EXACT + ","
+                    + NameLookupType.NAME_VARIANT + ","
+                    + NameLookupType.NAME_COLLATION_KEY + ")";
 
     // From system/core/logcat/event-log-tags
     // aggregator [time, count] will be logged for each aggregator cycle.
@@ -156,10 +78,12 @@
 
     // 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 more than this many contacts with matching name during aggregation
     // suggestion lookup, ignore the remaining results.
@@ -167,12 +91,8 @@
 
     private final ContactsProvider2 mContactsProvider;
     private final ContactsDatabaseHelper mDbHelper;
-    private final ContactAggregationScheduler mScheduler;
     private boolean mEnabled = true;
 
-    // Set if the current aggregation pass should be interrupted
-    private volatile boolean mCancel;
-
     /** Precompiled sql statement for setting an aggregated presence */
     private SQLiteStatement mAggregatedPresenceReplace;
     private SQLiteStatement mPresenceContactIdUpdate;
@@ -188,9 +108,25 @@
     private SQLiteStatement mContactIdAndMarkAggregatedUpdate;
     private SQLiteStatement mContactIdUpdate;
     private SQLiteStatement mMarkAggregatedUpdate;
+    private SQLiteStatement mContactUpdate;
+    private SQLiteStatement mContactInsert;
 
     private HashSet<Long> mRawContactsMarkedForAggregation = new HashSet<Long>();
 
+    private String[] mSelectionArgs1 = new String[1];
+    private String[] mSelectionArgs2 = new String[2];
+    private String[] mSelectionArgs3 = new String[3];
+    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 ContentValues mValues = new ContentValues();
+
+
     /**
      * Captures a potential match for a given name. The matching algorithm
      * constructs a bunch of NameMatchCandidate objects for various potential matches
@@ -233,34 +169,11 @@
         }
     }
 
-    private class AggregationNameLookupBuilder extends NameLookupBuilder {
-
-        private final MatchCandidateList mCandidates;
-
-        public AggregationNameLookupBuilder(NameSplitter splitter, MatchCandidateList candidates) {
-            super(splitter);
-            mCandidates = candidates;
-        }
-
-        @Override
-        protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
-                String name) {
-            mCandidates.add(name, lookupType);
-        }
-
-        @Override
-        protected String[] getCommonNicknameClusters(String normalizedName) {
-            return mContactsProvider.getCommonNicknameClusters(normalizedName);
-        }
-    }
-
     /**
-     * Constructor.  Starts a contact aggregation thread.  Call {@link #quit} to kill the
-     * aggregation thread.  Call {@link #schedule} to kick off the aggregation process after
-     * a delay of {@link ContactAggregationScheduler#AGGREGATION_DELAY} milliseconds.
+     * Constructor.
      */
     public ContactAggregator(ContactsProvider2 contactsProvider,
-            ContactsDatabaseHelper contactsDatabaseHelper, ContactAggregationScheduler scheduler) {
+            ContactsDatabaseHelper contactsDatabaseHelper) {
         mContactsProvider = contactsProvider;
         mDbHelper = contactsDatabaseHelper;
 
@@ -303,7 +216,7 @@
 
         mDisplayNameUpdate = db.compileStatement(
                 "UPDATE " + Tables.CONTACTS +
-                " SET " + Contacts.DISPLAY_NAME + "=? " +
+                " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
                 " WHERE " + Contacts._ID + "=?");
 
         mLookupKeyUpdate = db.compileStatement(
@@ -348,14 +261,21 @@
                 " SET " + PresenceColumns.CONTACT_ID + "=?" +
                 " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
 
-        mScheduler = scheduler;
-        mScheduler.setAggregator(this);
-        mScheduler.start();
+        mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
+        mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
 
-        // Perform an aggregation pass in the beginning, which will most of the time
-        // do nothing.  It will only be useful if the content provider has been killed
-        // before completing aggregation.
-        mScheduler.schedule();
+        mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.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(
+                RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
+                mMimeTypeIdPhoto, mMimeTypeIdPhone);
+
+        mRawContactsQueryByContactId = String.format(
+                RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
+                mMimeTypeIdPhoto, mMimeTypeIdPhone);
     }
 
     public void setEnabled(boolean enabled) {
@@ -366,160 +286,17 @@
         return mEnabled;
     }
 
-    /**
-     * Schedules aggregation pass after a short delay.  This method should be called every time
-     * the {@link RawContacts#CONTACT_ID} field is reset on any record.
-     */
-    public void schedule() {
-        if (mEnabled) {
-            mScheduler.schedule();
-        }
-    }
-
-    /**
-     * Kills the contact aggregation thread.
-     */
-    public void quit() {
-        mScheduler.stop();
-    }
-
-    /**
-     * Invoked by the scheduler to cancel aggregation.
-     */
-    public void interrupt() {
-        mCancel = true;
-    }
-
     private interface AggregationQuery {
-        String TABLE = Tables.RAW_CONTACTS;
-
-        String[] COLUMNS = {
-                RawContacts._ID, RawContacts.CONTACT_ID
-        };
+        String SQL =
+                "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
+                " FROM " + Tables.RAW_CONTACTS +
+                " WHERE " + RawContacts._ID + " IN(";
 
         int _ID = 0;
         int CONTACT_ID = 1;
-
-        String SELECTION = RawContactsColumns.AGGREGATION_NEEDED + "=1"
-                + " AND " + RawContacts.AGGREGATION_MODE
-                        + "=" + RawContacts.AGGREGATION_MODE_DEFAULT
-                + " AND " + RawContacts.CONTACT_ID + " NOT NULL";
     }
 
     /**
-     * Find all contacts that require aggregation and pass them through aggregation one by one.
-     * Do not call directly.  It is invoked by the scheduler.
-     */
-    public void run() {
-        if (!mEnabled) {
-            return;
-        }
-
-        mCancel = false;
-
-        SQLiteDatabase db = mDbHelper.getWritableDatabase();
-        MatchCandidateList candidates = new MatchCandidateList();
-        ContactMatcher matcher = new ContactMatcher();
-        ContentValues values = new ContentValues();
-        long rawContactIds[] = new long[MAX_TRANSACTION_SIZE];
-        long contactIds[] = new long[MAX_TRANSACTION_SIZE];
-        while (!mCancel) {
-            if (!aggregateBatch(db, candidates, matcher, values, rawContactIds, contactIds)) {
-                break;
-            }
-        }
-    }
-
-    /**
-     * Takes a batch of contacts and aggregates them. Returns the number of successfully
-     * processed raw contacts.
-     *
-     * @return true if there are possibly more contacts to aggregate
-     */
-    private boolean aggregateBatch(SQLiteDatabase db, MatchCandidateList candidates,
-            ContactMatcher matcher, ContentValues values, long[] rawContactIds, long[] contactIds) {
-        boolean lastBatch = false;
-        long elapsedTime = 0;
-        int aggregatedCount = 0;
-        while (!mCancel && aggregatedCount < MAX_TRANSACTION_SIZE) {
-            db.beginTransaction();
-            try {
-
-                long start = System.currentTimeMillis();
-                int count = findContactsToAggregate(db, rawContactIds, contactIds,
-                        MAX_TRANSACTION_SIZE - aggregatedCount);
-                if (mCancel || count == 0) {
-                    lastBatch = true;
-                    break;
-                }
-
-                Log.i(TAG, "Contact aggregation: " + count);
-                EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION,
-                        System.currentTimeMillis() - start, -count);
-
-                for (int i = 0; i < count; i++) {
-                    start = System.currentTimeMillis();
-                    aggregateContact(db, rawContactIds[i], contactIds[i], candidates, matcher,
-                            values);
-                    long end = System.currentTimeMillis();
-                    elapsedTime += (end - start);
-                    aggregatedCount++;
-                    if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
-
-                        // We have yielded the database, so the rawContactIds and contactIds
-                        // arrays are no longer current - we need to refetch them
-                        break;
-                    }
-                }
-                db.setTransactionSuccessful();
-            } finally {
-                db.endTransaction();
-            }
-        }
-
-        EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, aggregatedCount);
-        String performance = aggregatedCount == 0 ? "" : ", " + (elapsedTime / aggregatedCount)
-                + " ms per contact";
-        if (aggregatedCount != 0) {
-            Log.i(TAG, "Contact aggregation complete: " + aggregatedCount + performance);
-        }
-
-        if (aggregatedCount > 0) {
-
-            // Aggregation does not affect raw contacts and therefore there is no reason
-            // to send a notification to sync adapters.  We do need to notify every one else though.
-            mContactsProvider.notifyChange(false);
-        }
-
-        return !lastBatch;
-    }
-
-    /**
-     * Finds a batch of contacts marked for aggregation. The maximum batch size
-     * is {@link #MAX_TRANSACTION_SIZE} contacts.
-     * @param limit
-     */
-    private int findContactsToAggregate(SQLiteDatabase db, long[] rawContactIds,
-            long[] contactIds, int limit) {
-        Cursor c = db.query(AggregationQuery.TABLE, AggregationQuery.COLUMNS,
-                AggregationQuery.SELECTION, null, null, null, null,
-                String.valueOf(limit));
-
-        int count = 0;
-        try {
-            while (c.moveToNext()) {
-                rawContactIds[count] = c.getLong(AggregationQuery._ID);
-                contactIds[count] = c.getLong(AggregationQuery.CONTACT_ID);
-                count++;
-            }
-        } finally {
-            c.close();
-        }
-        return count;
-    }
-
-
-    /**
      * Aggregate all raw contacts that were marked for aggregation in the current transaction.
      * Call just before committing the transaction.
      */
@@ -530,27 +307,33 @@
         }
 
         long start = System.currentTimeMillis();
-        Log.i(TAG, "Contact aggregation: " + count);
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "Contact aggregation: " + count);
+        }
+
         EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -count);
 
+        String selectionArgs[] = new String[count];
+
+        int index = 0;
+        mSb.setLength(0);
+        mSb.append(AggregationQuery.SQL);
+        for (long rawContactId : mRawContactsMarkedForAggregation) {
+            if (index > 0) {
+                mSb.append(',');
+            }
+            mSb.append('?');
+            selectionArgs[index++] = String.valueOf(rawContactId);
+        }
+
+        mSb.append(')');
+
         long rawContactIds[] = new long[count];
         long contactIds[] = new long[count];
-
-        StringBuilder sb = new StringBuilder();
-        sb.append(RawContacts._ID + " IN(");
-        for (long rawContactId : mRawContactsMarkedForAggregation) {
-            sb.append(rawContactId);
-            sb.append(',');
-        }
-
-        sb.setLength(sb.length() - 1);
-        sb.append(')');
-
-        Cursor c = db.query(AggregationQuery.TABLE, AggregationQuery.COLUMNS, sb.toString(), null,
-                null, null, null);
-
-        int index = 0;
+        Cursor c = db.rawQuery(mSb.toString(), selectionArgs);
         try {
+            count = c.getCount();
+            index = 0;
             while (c.moveToNext()) {
                 rawContactIds[index] = c.getLong(AggregationQuery._ID);
                 contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
@@ -560,18 +343,17 @@
             c.close();
         }
 
-        MatchCandidateList candidates = new MatchCandidateList();
-        ContactMatcher matcher = new ContactMatcher();
-        ContentValues values = new ContentValues();
-
         for (int i = 0; i < count; i++) {
-            aggregateContact(db, rawContactIds[i], contactIds[i], candidates, matcher, values);
+            aggregateContact(db, rawContactIds[i], contactIds[i], mCandidates, mMatcher, mValues);
         }
 
         long elapsedTime = System.currentTimeMillis() - start;
         EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, count);
-        String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact";
-        Log.i(TAG, "Contact aggregation complete: " + count + performance);
+
+        if (VERBOSE_LOGGING) {
+            String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact";
+            Log.i(TAG, "Contact aggregation complete: " + count + performance);
+        }
     }
 
     public void clearPendingAggregations() {
@@ -593,13 +375,10 @@
     /**
      * Creates a new contact based on the given raw contact.  Does not perform aggregation.
      */
-    public void insertContact(SQLiteDatabase db, long rawContactId) {
-        ContentValues contactValues = new ContentValues();
-        contactValues.put(Contacts.DISPLAY_NAME, "");
-        contactValues.put(Contacts.IN_VISIBLE_GROUP, false);
-        computeAggregateData(db,
-                RawContactsColumns.CONCRETE_ID + "=" + rawContactId, contactValues);
-        long contactId = db.insert(Tables.CONTACTS, Contacts.DISPLAY_NAME, contactValues);
+    public void onRawContactInsert(SQLiteDatabase db, long rawContactId) {
+        mSelectionArgs1[0] = String.valueOf(rawContactId);
+        computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
+        long contactId = mContactInsert.executeInsert();
         setContactId(rawContactId, contactId);
         mDbHelper.updateContactVisible(contactId);
     }
@@ -625,9 +404,9 @@
         }
 
         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
-        final ContentValues values = new ContentValues();
-        computeAggregateData(db, contactId, values);
-        db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+        computeAggregateData(db, contactId, mContactUpdate);
+        mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+        mContactUpdate.execute();
 
         mDbHelper.updateContactVisible(contactId);
         updateAggregatedPresence(contactId);
@@ -675,11 +454,10 @@
             markAggregated(rawContactId);
         } else if (contactId == -1) {
             // Splitting an aggregate
-            ContentValues contactValues = new ContentValues();
-            contactValues.put(RawContactsColumns.DISPLAY_NAME, "");
-            computeAggregateData(db,
-                    RawContactsColumns.CONCRETE_ID + "=" + rawContactId, contactValues);
-            contactId = db.insert(Tables.CONTACTS, Contacts.DISPLAY_NAME, contactValues);
+            mSelectionArgs1[0] = String.valueOf(rawContactId);
+            computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
+                    mContactInsert);
+            contactId = mContactInsert.executeInsert();
             setContactIdAndMarkAggregated(rawContactId, contactId);
             mDbHelper.updateContactVisible(contactId);
 
@@ -702,8 +480,9 @@
             }
 
             setContactIdAndMarkAggregated(rawContactId, contactId);
-            computeAggregateData(db, contactId, values);
-            db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+            computeAggregateData(db, contactId, mContactUpdate);
+            mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+            mContactUpdate.execute();
             mDbHelper.updateContactVisible(contactId);
             updateAggregatedPresence(contactId);
         }
@@ -741,6 +520,52 @@
         mPresenceContactIdUpdate.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 "
@@ -771,6 +596,16 @@
      */
     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
@@ -825,189 +660,233 @@
     private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
             MatchCandidateList candidates, ContactMatcher matcher) {
 
-        updateMatchScoresBasedOnDataMatches(db, rawContactId, MODE_AGGREGATION, candidates, matcher);
-
-        // See if we have already found a good match based on name matches alone
-        long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+        // Find good matches based on name alone
+        long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, candidates, matcher);
         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, candidates, matcher);
+            bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher);
         }
 
         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,
-            MatchCandidateList candidates, ContactMatcher matcher) {
+            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;
         }
 
-        StringBuilder selection = new StringBuilder();
-        selection.append(RawContacts.CONTACT_ID).append(" IN (");
+        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) {
-                selection.append(',');
+                mSb.append(',');
             }
-            selection.append(secondaryContactIds.get(i));
+            mSb.append(secondaryContactIds.get(i));
         }
-        selection.append(") AND " + MimetypesColumns.MIMETYPE + "='"
-                + StructuredName.CONTENT_ITEM_TYPE + "'");
 
-        final Cursor c = db.query(DataContactIdQuery.TABLE, DataContactIdQuery.COLUMNS,
-                selection.toString(), null, null, null, null);
+        // 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);
 
-        MatchCandidateList nameCandidates = new MatchCandidateList();
-        AggregationNameLookupBuilder builder =
-            new AggregationNameLookupBuilder(mContactsProvider.getNameSplitter(), nameCandidates);
+        matchAllCandidates(db, mSb.toString(), candidates, matcher,
+                ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
+
+        return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
+    }
+
+    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 name = c.getString(DataContactIdQuery.DATA1);
-                long contactId = c.getLong(DataContactIdQuery.CONTACT_ID);
-
-                nameCandidates.clear();
-                builder.insertNameLookup(0, 0, name);
-
-                // 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);
-
-                    // We only want to compare structured names to structured names
-                    // at this stage, we need to ignore all other sources of name lookup data.
-                    if (NameLookupType.isBasedOnStructuredName(candidate.mLookupType)) {
-                        for (int j = 0; j < nameCandidates.mCount; j++) {
-                            NameMatchCandidate nameCandidate = nameCandidates.mList.get(j);
-                            matcher.matchName(contactId,
-                                    nameCandidate.mLookupType, nameCandidate.mName,
-                                    candidate.mLookupType, candidate.mName,
-                                    ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE);
-                        }
-                    }
-                }
+                String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
+                int type = c.getInt(NameLookupQuery.NAME_TYPE);
+                candidates.add(normalizedName, type);
             }
         } finally {
             c.close();
         }
-
-        return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
     }
 
     /**
      * Computes scores for contacts that have matching data rows.
      */
-    private void updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
-            int mode, MatchCandidateList candidates, ContactMatcher matcher) {
+    private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
+            MatchCandidateList candidates, ContactMatcher matcher) {
 
-        final Cursor c = db.query(Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS,
-                DataMimetypeQuery.COLUMNS,
-                Data.RAW_CONTACT_ID + "=" + rawContactId + " AND ("
-                        + DataMimetypeQuery.MIMETYPE_SELECTION_IN_CLAUSE + ")",
-                null, null, null, null);
+        updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+        long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+        if (bestMatch != -1) {
+            return bestMatch;
+        }
 
+        updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+        updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+
+        return -1;
+    }
+
+    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";
+
+        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;
+    }
+
+    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()) {
-                String mimeType = c.getString(DataMimetypeQuery.MIMETYPE);
-                String data = c.getString(DataMimetypeQuery.DATA1);
-                if (mimeType.equals(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) {
-                    addMatchCandidatesStructuredName(data, candidates);
-                } else if (mimeType.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
-                    if (!TextUtils.isEmpty(data)) {
-                        addMatchCandidatesEmail(data, mode, candidates);
-                        lookupEmailMatches(db, data, matcher);
-                    }
-                } else if (mimeType.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
-                    if (!TextUtils.isEmpty(data)) {
-                        lookupPhoneMatches(db, data, matcher);
-                    }
-                } else if (mimeType.equals(CommonDataKinds.Nickname.CONTENT_ITEM_TYPE)) {
-                    if (!TextUtils.isEmpty(data)) {
-                        addMatchCandidatesNickname(data, mode, candidates);
-                        lookupNicknameMatches(db, data, matcher);
-                    }
+                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();
         }
-
-        lookupNameMatches(db, candidates, matcher);
-
-        if (mode == MODE_SUGGESTIONS) {
-            lookupApproximateNameMatches(db, candidates, matcher);
-        }
     }
 
-    /**
-     * Looks for matches based on the full name.
-     */
-    private void addMatchCandidatesStructuredName(String name, MatchCandidateList candidates) {
-        AggregationNameLookupBuilder builder =
-                new AggregationNameLookupBuilder(mContactsProvider.getNameSplitter(), candidates);
-        builder.insertNameLookup(0, 0, name);
+    private interface EmailLookupQuery {
+        String TABLE = Tables.DATA + " dataA"
+                + " JOIN " + Tables.DATA + " dataB" +
+                " ON (" + "dataA." + Email.DATA + "=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 + "=?"
+                + " AND dataA." + DataColumns.MIMETYPE_ID + "=?"
+                + " AND dataA." + Email.DATA + " NOT NULL"
+                + " AND dataB." + DataColumns.MIMETYPE_ID + "=?"
+                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
+
+        String[] COLUMNS = new String[] {
+            RawContacts.CONTACT_ID
+        };
+
+        int CONTACT_ID = 0;
     }
 
-    /**
-     * Extracts the user name portion from an email address and normalizes it so that it
-     * can be matched against names and nicknames.
-     */
-    private void addMatchCandidatesEmail(String email, int mode, MatchCandidateList candidates) {
-        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
-        if (tokens.length == 0) {
-            return;
-        }
-
-        String address = tokens[0].getAddress();
-        int at = address.indexOf('@');
-        if (at != -1) {
-            address = address.substring(0, at);
-        }
-
-        candidates.add(NameNormalizer.normalize(address), NameLookupType.EMAIL_BASED_NICKNAME);
-    }
-
-
-    /**
-     * Normalizes the nickname and adds it to the list of candidates.
-     */
-    private void addMatchCandidatesNickname(String nickname, int mode,
-            MatchCandidateList candidates) {
-        candidates.add(NameNormalizer.normalize(nickname), NameLookupType.NICKNAME);
-    }
-
-    /**
-     * Given a list of {@link NameMatchCandidate}'s, finds all matches and computes their scores.
-     */
-    private void lookupNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
+    private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
             ContactMatcher matcher) {
+        mSelectionArgs3[0] = String.valueOf(rawContactId);
+        mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdEmail);
+        Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
+                EmailLookupQuery.SELECTION,
+                mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING);
+        try {
+            while (c.moveToNext()) {
+                long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
+                matcher.updateScoreWithEmailMatch(contactId);
+            }
+        } finally {
+            c.close();
+        }
+    }
 
-        if (candidates.mCount == 0) {
-            return;
+    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";
+
+        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();
         }
 
-        StringBuilder selection = new StringBuilder();
-        selection.append(RawContactsColumns.AGGREGATION_NEEDED + "=0 AND ");
-        selection.append(NameLookupColumns.NORMALIZED_NAME);
-        selection.append(" IN (");
-        for (int i = 0; i < candidates.mCount; i++) {
-            DatabaseUtils.appendEscapedSQLString(selection, candidates.mList.get(i).mName);
-            selection.append(",");
-        }
-
-        // Yank the last comma
-        selection.setLength(selection.length() - 1);
-        selection.append(")");
-
-        matchAllCandidates(db, selection.toString(), candidates, matcher,
-                ContactMatcher.MATCHING_ALGORITHM_EXACT, String.valueOf(PRIMARY_HIT_LIMIT));
     }
 
     /**
@@ -1037,22 +916,38 @@
         }
     }
 
+    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(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
+        final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
                 selection, null, null, null, null, limit);
 
         try {
             while (c.moveToNext()) {
-                Long contactId = c.getLong(NameLookupQuery.CONTACT_ID);
-                String name = c.getString(NameLookupQuery.NORMALIZED_NAME);
-                int nameType = c.getInt(NameLookupQuery.NAME_TYPE);
+                Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
+                String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
+                int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
 
-                // Determine which candidate produced this match
+                // 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,
@@ -1064,79 +959,38 @@
         }
     }
 
-    private void lookupPhoneMatches(SQLiteDatabase db, String phoneNumber, ContactMatcher matcher) {
-        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        mDbHelper.buildPhoneLookupAndRawContactQuery(qb, phoneNumber);
-        Cursor c = qb.query(db, CONTACT_ID_COLUMNS, RawContactsColumns.AGGREGATION_NEEDED + "=0",
-                null, null, null, null, String.valueOf(SECONDARY_HIT_LIMIT));
-        try {
-            while (c.moveToNext()) {
-                long contactId = c.getLong(COL_CONTACT_ID);
-                matcher.updateScoreWithPhoneNumberMatch(contactId);
-            }
-        } finally {
-            c.close();
-        }
-    }
-
-    /**
-     * Finds exact email matches and updates their match scores.
-     */
-    private void lookupEmailMatches(SQLiteDatabase db, String address, ContactMatcher matcher) {
-        long mimetypeId = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
-        Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
-                DataColumns.MIMETYPE_ID + "=" + mimetypeId
-                        + " AND " + Email.DATA + "=?"
-                        + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0",
-                new String[] {address}, null, null, null, String.valueOf(SECONDARY_HIT_LIMIT));
-        try {
-            while (c.moveToNext()) {
-                long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
-                matcher.updateScoreWithEmailMatch(contactId);
-            }
-        } finally {
-            c.close();
-        }
-    }
-
-    /**
-     * Finds exact nickname matches in the name lookup table and updates their match scores.
-     */
-    private void lookupNicknameMatches(SQLiteDatabase db, String nickname, ContactMatcher matcher) {
-        String normalized = NameNormalizer.normalize(nickname);
-        Cursor c = db.query(true, Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS, CONTACT_ID_COLUMNS,
-                NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NICKNAME + " AND "
-                        + NameLookupColumns.NORMALIZED_NAME + "='" + normalized + "' AND "
-                        + RawContactsColumns.AGGREGATION_NEEDED + "=0",
-                null, null, null, null, null);
-        try {
-            while (c.moveToNext()) {
-                long contactId = c.getLong(COL_CONTACT_ID);
-                matcher.updateScoreWithNicknameMatch(contactId);
-            }
-        } finally {
-            c.close();
-        }
-    }
-
     private interface RawContactsQuery {
-        String[] COLUMNS = new String[] {
-            RawContactsColumns.CONCRETE_ID,
-            RawContactsColumns.DISPLAY_NAME,
-            RawContactsColumns.DISPLAY_NAME_SOURCE,
-            RawContacts.ACCOUNT_TYPE,
-            RawContacts.ACCOUNT_NAME,
-            RawContacts.SOURCE_ID,
-            RawContacts.CUSTOM_RINGTONE,
-            RawContacts.SEND_TO_VOICEMAIL,
-            RawContacts.LAST_TIME_CONTACTED,
-            RawContacts.TIMES_CONTACTED,
-            RawContacts.STARRED,
-            RawContacts.IS_RESTRICTED,
-            DataColumns.CONCRETE_ID,
-            DataColumns.CONCRETE_MIMETYPE_ID,
-            Data.IS_SUPER_PRIMARY,
-        };
+        String SQL_FORMAT =
+                "SELECT "
+                        + RawContactsColumns.CONCRETE_ID + ","
+                        + RawContactsColumns.DISPLAY_NAME + ","
+                        + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
+                        + RawContacts.ACCOUNT_TYPE + ","
+                        + RawContacts.ACCOUNT_NAME + ","
+                        + RawContacts.SOURCE_ID + ","
+                        + RawContacts.CUSTOM_RINGTONE + ","
+                        + RawContacts.SEND_TO_VOICEMAIL + ","
+                        + RawContacts.LAST_TIME_CONTACTED + ","
+                        + RawContacts.TIMES_CONTACTED + ","
+                        + RawContacts.STARRED + ","
+                        + RawContacts.IS_RESTRICTED + ","
+                        + DataColumns.CONCRETE_ID + ","
+                        + DataColumns.CONCRETE_MIMETYPE_ID + ","
+                        + Data.IS_SUPER_PRIMARY +
+                " FROM " + Tables.RAW_CONTACTS +
+                " 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;
@@ -1155,23 +1009,69 @@
         int IS_SUPER_PRIMARY = 14;
     }
 
+    private interface ContactReplaceSqlStatement {
+        String UPDATE_SQL =
+                "UPDATE " + Tables.CONTACTS +
+                " SET "
+                        + Contacts.NAME_RAW_CONTACT_ID + "=?, "
+                        + Contacts.PHOTO_ID + "=?, "
+                        + Contacts.SEND_TO_VOICEMAIL + "=?, "
+                        + Contacts.CUSTOM_RINGTONE + "=?, "
+                        + Contacts.LAST_TIME_CONTACTED + "=?, "
+                        + Contacts.TIMES_CONTACTED + "=?, "
+                        + Contacts.STARRED + "=?, "
+                        + Contacts.HAS_PHONE_NUMBER + "=?, "
+                        + ContactsColumns.SINGLE_IS_RESTRICTED + "=?, "
+                        + Contacts.LOOKUP_KEY + "=? " +
+                " WHERE " + Contacts._ID + "=?";
+
+        String INSERT_SQL =
+                "INSERT INTO " + Tables.CONTACTS + " ("
+                        + Contacts.NAME_RAW_CONTACT_ID + ", "
+                        + Contacts.PHOTO_ID + ", "
+                        + Contacts.SEND_TO_VOICEMAIL + ", "
+                        + Contacts.CUSTOM_RINGTONE + ", "
+                        + Contacts.LAST_TIME_CONTACTED + ", "
+                        + Contacts.TIMES_CONTACTED + ", "
+                        + Contacts.STARRED + ", "
+                        + Contacts.HAS_PHONE_NUMBER + ", "
+                        + ContactsColumns.SINGLE_IS_RESTRICTED + ", "
+                        + Contacts.LOOKUP_KEY + ", "
+                        + Contacts.IN_VISIBLE_GROUP + ") " +
+                " VALUES (?,?,?,?,?,?,?,?,?,?,0)";
+
+        int NAME_RAW_CONTACT_ID = 1;
+        int PHOTO_ID = 2;
+        int SEND_TO_VOICEMAIL = 3;
+        int CUSTOM_RINGTONE = 4;
+        int LAST_TIME_CONTACTED = 5;
+        int TIMES_CONTACTED = 6;
+        int STARRED = 7;
+        int HAS_PHONE_NUMBER = 8;
+        int SINGLE_IS_RESTRICTED = 9;
+        int LOOKUP_KEY = 10;
+        int CONTACT_ID = 11;
+    }
+
     /**
      * Computes aggregate-level data for the specified aggregate contact ID.
      */
-    private void computeAggregateData(SQLiteDatabase db, long contactId, ContentValues values) {
-        computeAggregateData(db, RawContacts.CONTACT_ID + "=" + contactId
-                + " AND " + RawContacts.DELETED + "=0", values);
+    private void computeAggregateData(SQLiteDatabase db, long contactId,
+            SQLiteStatement statement) {
+        mSelectionArgs1[0] = String.valueOf(contactId);
+        computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
     }
 
     /**
      * Computes aggregate-level data from constituent raw contacts.
      */
-    private void computeAggregateData(final SQLiteDatabase db, String selection,
-            final ContentValues values) {
+    private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
+            SQLiteStatement statement) {
         long currentRawContactId = -1;
         int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
         String bestDisplayName = null;
         long bestPhotoId = -1;
+        long bestNameRawContactId = -1;
         boolean foundSuperPrimaryPhoto = false;
         String photoAccount = null;
         int totalRowCount = 0;
@@ -1179,25 +1079,12 @@
         String contactCustomRingtone = null;
         long contactLastTimeContacted = 0;
         int contactTimesContacted = 0;
-        boolean contactStarred = false;
+        int contactStarred = 0;
         int singleIsRestricted = 1;
         int hasPhoneNumber = 0;
-        StringBuilder lookupKey = new StringBuilder();
 
-        long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
-        long phoneMimeType = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
-
-        String isPhotoSql = "(" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
-                + Photo.PHOTO + " NOT NULL)";
-        String isPhoneSql = "(" + DataColumns.MIMETYPE_ID + "=" + phoneMimeType + " AND "
-                + Phone.NUMBER + " NOT NULL)";
-
-        String tables = Tables.RAW_CONTACTS + " LEFT OUTER JOIN " + Tables.DATA + " ON("
-                + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
-                + " AND (" + isPhotoSql + " OR " + isPhoneSql + "))";
-
-        final Cursor c = db.query(tables, RawContactsQuery.COLUMNS, selection, null, null, null,
-                null);
+        mSb.setLength(0);       // Lookup key
+        Cursor c = db.rawQuery(sql, sqlArgs);
         try {
             while (c.moveToNext()) {
                 long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
@@ -1212,17 +1099,24 @@
                         if (bestDisplayName == null) {
                             bestDisplayName = displayName;
                             bestDisplayNameSource = displayNameSource;
+                            bestNameRawContactId = rawContactId;
                         } else if (bestDisplayNameSource != displayNameSource) {
                             if (bestDisplayNameSource < displayNameSource) {
                                 bestDisplayName = displayName;
                                 bestDisplayNameSource = displayNameSource;
+                                bestNameRawContactId = rawContactId;
                             }
                         } else if (NameNormalizer.compareComplexity(displayName,
                                 bestDisplayName) > 0) {
                             bestDisplayName = displayName;
+                            bestNameRawContactId = rawContactId;
                         }
                     }
 
+                    if (bestNameRawContactId == -1) {
+                        bestNameRawContactId = rawContactId;
+                    }
+
                     // Contact options
                     if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
                         boolean sendToVoicemail =
@@ -1247,7 +1141,9 @@
                         contactTimesContacted = timesContacted;
                     }
 
-                    contactStarred |= (c.getInt(RawContactsQuery.STARRED) != 0);
+                    if (c.getInt(RawContactsQuery.STARRED) != 0) {
+                        contactStarred = 1;
+                    }
 
                     // Single restricted
                     if (totalRowCount > 1) {
@@ -1262,7 +1158,7 @@
                         }
                     }
 
-                    ContactLookupKey.appendToLookupKey(lookupKey,
+                    ContactLookupKey.appendToLookupKey(mSb,
                             c.getString(RawContactsQuery.ACCOUNT_TYPE),
                             c.getString(RawContactsQuery.ACCOUNT_NAME),
                             c.getString(RawContactsQuery.SOURCE_ID),
@@ -1273,7 +1169,7 @@
                     long dataId = c.getLong(RawContactsQuery.DATA_ID);
                     int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
                     boolean superprimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
-                    if (mimetypeId == photoMimeType) {
+                    if (mimetypeId == mMimeTypeIdPhoto) {
 
                         // For now, just choose the first photo in a list sorted by account name.
                         String account = c.getString(RawContactsQuery.ACCOUNT_NAME);
@@ -1285,7 +1181,7 @@
                             bestPhotoId = dataId;
                             foundSuperPrimaryPhoto |= superprimary;
                         }
-                    } else if (mimetypeId == phoneMimeType) {
+                    } else if (mimetypeId == mMimeTypeIdPhone) {
                         hasPhoneNumber = 1;
                     }
                 }
@@ -1295,28 +1191,30 @@
             c.close();
         }
 
-        values.clear();
-
-        // If don't have anything to base the display name on, let's just leave what was in
-        // that field hoping that there was something there before and it is still valid.
-        if (bestDisplayName != null) {
-            values.put(Contacts.DISPLAY_NAME, bestDisplayName);
-        }
+        statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID, bestNameRawContactId);
 
         if (bestPhotoId != -1) {
-            values.put(Contacts.PHOTO_ID, bestPhotoId);
+            statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
         } else {
-            values.putNull(Contacts.PHOTO_ID);
+            statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
         }
 
-        values.put(Contacts.SEND_TO_VOICEMAIL, totalRowCount == contactSendToVoicemail);
-        values.put(Contacts.CUSTOM_RINGTONE, contactCustomRingtone);
-        values.put(Contacts.LAST_TIME_CONTACTED, contactLastTimeContacted);
-        values.put(Contacts.TIMES_CONTACTED, contactTimesContacted);
-        values.put(Contacts.STARRED, contactStarred);
-        values.put(Contacts.HAS_PHONE_NUMBER, hasPhoneNumber);
-        values.put(ContactsColumns.SINGLE_IS_RESTRICTED, singleIsRestricted);
-        values.put(Contacts.LOOKUP_KEY, Uri.encode(lookupKey.toString()));
+        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.HAS_PHONE_NUMBER,
+                hasPhoneNumber);
+        statement.bindLong(ContactReplaceSqlStatement.SINGLE_IS_RESTRICTED,
+                singleIsRestricted);
+        statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
+                Uri.encode(mSb.toString()));
     }
 
     private interface PhotoIdQuery {
@@ -1348,8 +1246,9 @@
                 + " 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 + "=" + contactId, null, null, null, null);
+                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
         try {
             while (c.moveToNext()) {
                 long dataId = c.getLong(PhotoIdQuery.DATA_ID);
@@ -1386,57 +1285,68 @@
 
     private interface DisplayNameQuery {
         String[] COLUMNS = new String[] {
+            RawContacts._ID,
             RawContactsColumns.DISPLAY_NAME,
             RawContactsColumns.DISPLAY_NAME_SOURCE,
         };
 
-        int DISPLAY_NAME = 0;
-        int DISPLAY_NAME_SOURCE = 1;
+        int _ID = 0;
+        int DISPLAY_NAME = 1;
+        int DISPLAY_NAME_SOURCE = 2;
     }
 
-    public void updateDisplayName(SQLiteDatabase db, long rawContactId) {
-
+    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) {
         int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
         String bestDisplayName = null;
+        long bestNameRawContactId = -1;
 
-
+        mSelectionArgs1[0] = String.valueOf(contactId);
         final Cursor c = db.query(Tables.RAW_CONTACTS, DisplayNameQuery.COLUMNS,
-                RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
         try {
             while (c.moveToNext()) {
+                long id = c.getLong(DisplayNameQuery._ID);
                 String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
                 int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
                 if (!TextUtils.isEmpty(displayName)) {
                     if (bestDisplayName == null) {
                         bestDisplayName = displayName;
                         bestDisplayNameSource = displayNameSource;
+                        bestNameRawContactId = id;
                     } else if (bestDisplayNameSource != displayNameSource) {
                         if (bestDisplayNameSource < displayNameSource) {
                             bestDisplayName = displayName;
                             bestDisplayNameSource = displayNameSource;
+                            bestNameRawContactId = id;
                         }
                     } else if (NameNormalizer.compareComplexity(displayName,
                             bestDisplayName) > 0) {
                         bestDisplayName = displayName;
+                        bestNameRawContactId = id;
                     }
                 }
+                if (bestNameRawContactId == -1) {
+                    bestNameRawContactId = id;
+                }
             }
         } finally {
             c.close();
         }
 
-        if (bestDisplayName == null) {
-            mDisplayNameUpdate.bindNull(1);
-        } else {
-            mDisplayNameUpdate.bindString(1, bestDisplayName);
+        if (bestNameRawContactId != -1) {
+            mDisplayNameUpdate.bindLong(1, bestNameRawContactId);
+            mDisplayNameUpdate.bindLong(2, contactId);
+            mDisplayNameUpdate.execute();
         }
-        mDisplayNameUpdate.bindLong(2, contactId);
-        mDisplayNameUpdate.execute();
     }
 
     /**
@@ -1476,12 +1386,13 @@
             return;
         }
 
-        StringBuilder lookupKey = new StringBuilder();
+        mSb.setLength(0);
+        mSelectionArgs1[0] = String.valueOf(contactId);
         final Cursor c = db.query(Tables.RAW_CONTACTS, LookupKeyQuery.COLUMNS,
-                RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
         try {
             while (c.moveToNext()) {
-                ContactLookupKey.appendToLookupKey(lookupKey,
+                ContactLookupKey.appendToLookupKey(mSb,
                         c.getString(LookupKeyQuery.ACCOUNT_TYPE),
                         c.getString(LookupKeyQuery.ACCOUNT_NAME),
                         c.getString(LookupKeyQuery.SOURCE_ID),
@@ -1491,10 +1402,10 @@
             c.close();
         }
 
-        if (lookupKey.length() == 0) {
+        if (mSb.length() == 0) {
             mLookupKeyUpdate.bindNull(1);
         } else {
-            mLookupKeyUpdate.bindString(1, lookupKey.toString());
+            mLookupKeyUpdate.bindString(1, mSb.toString());
         }
         mLookupKeyUpdate.bindLong(2, contactId);
         mLookupKeyUpdate.execute();
@@ -1526,6 +1437,14 @@
                 filter);
     }
 
+    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.
@@ -1610,6 +1529,16 @@
         return new ReorderingCursorWrapper(cursor, positionMap);
     }
 
+    private interface RawContactIdQuery {
+        String TABLE = Tables.RAW_CONTACTS;
+
+        String[] COLUMNS = new String[] {
+            RawContacts._ID
+        };
+
+        int _ID = 0;
+    }
+
     /**
      * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
      * descending order of match score.
@@ -1622,12 +1551,12 @@
         // Don't aggregate a contact with itself
         matcher.keepOut(contactId);
 
-        final Cursor c = db.query(Tables.RAW_CONTACTS, CONTACT_ID_COLUMN,
+        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(0);
-                updateMatchScoresBasedOnDataMatches(db, rawContactId, MODE_SUGGESTIONS, candidates,
+                long rawContactId = c.getLong(RawContactIdQuery._ID);
+                updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
                         matcher);
             }
         } finally {
@@ -1636,4 +1565,17 @@
 
         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) {
+
+        updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+        updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+        updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+        loadNameMatchCandidates(db, rawContactId, candidates, false);
+        lookupApproximateNameMatches(db, candidates, matcher);
+    }
 }
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index f248116..740ade6 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -25,6 +25,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
+import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
@@ -49,6 +50,7 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.SocialContract.Activities;
 import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.util.HashMap;
@@ -61,11 +63,14 @@
 /* package */ class ContactsDatabaseHelper extends SQLiteOpenHelper {
     private static final String TAG = "ContactsDatabaseHelper";
 
-    private static final int DATABASE_VERSION = 105;
+    private static final int DATABASE_VERSION = 203;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
 
+    /** size of the compiled-sql statement cache mainatained by {@link SQLiteDatabase} */
+    private static final int MAX_CACHE_SIZE_FOR_CONTACTS_DB = 250;
+
     public interface Tables {
         public static final String CONTACTS = "contacts";
         public static final String RAW_CONTACTS = "raw_contacts";
@@ -181,7 +186,7 @@
                 + "LEFT OUTER JOIN packages ON (activities.package_id = packages._id) "
                 + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id) "
                 + "LEFT OUTER JOIN raw_contacts ON (activities.author_contact_id = " +
-                		"raw_contacts._id) "
+                        "raw_contacts._id) "
                 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
 
         public static final String NAME_LOOKUP_JOIN_RAW_CONTACTS = "name_lookup "
@@ -251,8 +256,6 @@
         public static final String LAST_STATUS_UPDATE_ID = "status_update_id";
 
         public static final String CONCRETE_ID = Tables.CONTACTS + "." + BaseColumns._ID;
-        public static final String CONCRETE_DISPLAY_NAME = Tables.CONTACTS + "."
-                + Contacts.DISPLAY_NAME;
 
         public static final String CONCRETE_TIMES_CONTACTED = Tables.CONTACTS + "."
                 + Contacts.TIMES_CONTACTED;
@@ -296,6 +299,12 @@
         public static final String DISPLAY_NAME = "display_name";
         public static final String DISPLAY_NAME_SOURCE = "display_name_source";
         public static final String AGGREGATION_NEEDED = "aggregation_needed";
+        public static final String CONTACT_IN_VISIBLE_GROUP = "contact_in_visible_group";
+
+        public static final String CONCRETE_DISPLAY_NAME =
+                Tables.RAW_CONTACTS + "." + DISPLAY_NAME;
+        public static final String CONCRETE_CONTACT_ID =
+                Tables.RAW_CONTACTS + "." + RawContacts.CONTACT_ID;
     }
 
     /**
@@ -376,6 +385,7 @@
         public static final String DATA_ID = "data_id";
         public static final String RAW_CONTACT_ID = "raw_contact_id";
         public static final String NORMALIZED_NUMBER = "normalized_number";
+        public static final String MIN_MATCH = "min_match";
     }
 
     public interface NameLookupColumns {
@@ -496,12 +506,14 @@
     /** Compiled statements for updating {@link Contacts#IN_VISIBLE_GROUP}. */
     private SQLiteStatement mVisibleUpdate;
     private SQLiteStatement mVisibleSpecificUpdate;
+    private SQLiteStatement mVisibleUpdateRawContacts;
+    private SQLiteStatement mVisibleSpecificUpdateRawContacts;
 
     private boolean mReopenDatabase = false;
 
     private static ContactsDatabaseHelper sSingleton = null;
 
-    private boolean mUseStrictPhoneNumberComparation;
+    private boolean mUseStrictPhoneNumberComparison;
 
     /**
      * List of package names with access to {@link RawContacts#IS_RESTRICTED} data.
@@ -526,7 +538,7 @@
 
         mContext = context;
         mSyncState = new SyncStateContentProviderHelper();
-        mUseStrictPhoneNumberComparation =
+        mUseStrictPhoneNumberComparison =
                 resources.getBoolean(
                         com.android.internal.R.bool.config_use_strict_phone_number_comparation);
         int resourceId = resources.getIdentifier("unrestricted_packages", "array",
@@ -570,6 +582,18 @@
         mVisibleSpecificUpdate = db.compileStatement(visibleUpdate + " WHERE "
                 + ContactsColumns.CONCRETE_ID + "=?");
 
+        String visibleUpdateRawContacts =
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" +
+                        "SELECT " + Contacts.IN_VISIBLE_GROUP +
+                        " FROM " + Tables.CONTACTS +
+                        " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")" +
+                " WHERE " + RawContacts.DELETED + "=0";
+
+        mVisibleUpdateRawContacts = db.compileStatement(visibleUpdateRawContacts);
+        mVisibleSpecificUpdateRawContacts = db.compileStatement(visibleUpdateRawContacts +
+                    " AND " + RawContacts.CONTACT_ID + "=?");
+
         db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
         db.execSQL("CREATE TABLE IF NOT EXISTS " + DATABASE_PRESENCE + "." + Tables.PRESENCE + " ("+
                 StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
@@ -645,7 +669,7 @@
         // One row per group of contacts corresponding to the same person
         db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
                 BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                Contacts.DISPLAY_NAME + " TEXT," +
+                Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
                 Contacts.PHOTO_ID + " INTEGER REFERENCES data(_id)," +
                 Contacts.CUSTOM_RINGTONE + " TEXT," +
                 Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
@@ -660,8 +684,7 @@
         ");");
 
         db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
-                Contacts.IN_VISIBLE_GROUP + "," +
-                Contacts.DISPLAY_NAME + " COLLATE LOCALIZED" +
+                Contacts.IN_VISIBLE_GROUP +
         ");");
 
         db.execSQL("CREATE INDEX contacts_has_phone_index ON " + Tables.CONTACTS + " (" +
@@ -672,6 +695,10 @@
                 ContactsColumns.SINGLE_IS_RESTRICTED +
         ");");
 
+        db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
+                Contacts.NAME_RAW_CONTACT_ID +
+        ");");
+
         // Contacts table
         db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" +
                 RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -694,6 +721,7 @@
                 RawContactsColumns.DISPLAY_NAME + " TEXT," +
                 RawContactsColumns.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
                         DisplayNameSources.UNDEFINED + "," +
+                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.SYNC1 + " TEXT, " +
                 RawContacts.SYNC2 + " TEXT, " +
                 RawContacts.SYNC3 + " TEXT, " +
@@ -724,6 +752,11 @@
 //                RawContactsColumns.AGGREGATION_NEEDED +
 //        ");");
 
+        db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
+                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+                RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
+        ");");
+
         // Package name mapping table
         db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" +
                 PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -784,7 +817,8 @@
                         + " INTEGER PRIMARY KEY REFERENCES data(_id) NOT NULL," +
                 PhoneLookupColumns.RAW_CONTACT_ID
                         + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
-                PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL" +
+                PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," +
+                PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" +
         ");");
 
         db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" +
@@ -793,6 +827,12 @@
                 PhoneLookupColumns.DATA_ID +
         ");");
 
+        db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
+                PhoneLookupColumns.MIN_MATCH + "," +
+                PhoneLookupColumns.RAW_CONTACT_ID + "," +
+                PhoneLookupColumns.DATA_ID +
+        ");");
+
         // Private name/nickname table used for lookup
         db.execSQL("CREATE TABLE " + Tables.NAME_LOOKUP + " (" +
                 NameLookupColumns.DATA_ID
@@ -991,7 +1031,7 @@
          */
         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_marked_deleted;");
         db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted "
-                + "   BEFORE UPDATE ON " + Tables.RAW_CONTACTS
+                + "   AFTER UPDATE ON " + Tables.RAW_CONTACTS
                 + " BEGIN "
                 + "   UPDATE " + Tables.RAW_CONTACTS
                 + "     SET "
@@ -1001,7 +1041,7 @@
                 + " END");
 
         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_updated;");
-        db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated BEFORE UPDATE ON " + Tables.DATA
+        db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated AFTER UPDATE ON " + Tables.DATA
                 + " BEGIN "
                 + "   UPDATE " + Tables.DATA
                 + "     SET " + Data.DATA_VERSION + "=OLD." + Data.DATA_VERSION + "+1 "
@@ -1028,7 +1068,7 @@
 
         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_updated1;");
         db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_updated1 "
-                + "   BEFORE UPDATE ON " + Tables.GROUPS
+                + "   AFTER UPDATE ON " + Tables.GROUPS
                 + " BEGIN "
                 + "   UPDATE " + Tables.GROUPS
                 + "     SET "
@@ -1097,14 +1137,16 @@
         String dataSelect = "SELECT "
                 + DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
                 + Data.RAW_CONTACT_ID + ", "
-                + RawContacts.CONTACT_ID + ", "
+                + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
                 + syncColumns + ", "
                 + dataColumns + ", "
                 + contactOptionColumns + ", "
-                + ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME + ", "
+                + "name_raw_contact." + RawContactsColumns.DISPLAY_NAME
+                        + " AS " + Contacts.DISPLAY_NAME + ", "
+                + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
+                        + " AS " + Contacts.IN_VISIBLE_GROUP + ", "
                 + Contacts.LOOKUP_KEY + ", "
                 + Contacts.PHOTO_ID + ", "
-                + Contacts.IN_VISIBLE_GROUP + ", "
                 + ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
                 + " FROM " + Tables.DATA
@@ -1113,7 +1155,9 @@
                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
                 +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
                 + " JOIN " + Tables.CONTACTS + " ON ("
-                +   RawContacts.CONTACT_ID + "=" + Tables.CONTACTS + "." + Contacts._ID + ")"
+                +   RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
+                + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
+                +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"
                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
                 +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
                 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
@@ -1123,7 +1167,7 @@
 
         db.execSQL("CREATE VIEW " + Views.DATA_ALL + " AS " + dataSelect);
         db.execSQL("CREATE VIEW " + Views.DATA_RESTRICTED + " AS " + dataSelect + " WHERE "
-                + RawContacts.IS_RESTRICTED + "=0");
+                + RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0");
 
         String rawContactOptionColumns =
                 RawContacts.CUSTOM_RINGTONE + ","
@@ -1148,13 +1192,13 @@
         String contactsColumns =
                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
                         + " AS " + Contacts.CUSTOM_RINGTONE + ", "
-                + ContactsColumns.CONCRETE_DISPLAY_NAME
+                + "name_raw_contact." + RawContactsColumns.DISPLAY_NAME
                         + " AS " + Contacts.DISPLAY_NAME + ", "
-                + Contacts.IN_VISIBLE_GROUP + ", "
+                + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
+                        + " AS " + Contacts.IN_VISIBLE_GROUP + ", "
                 + Contacts.HAS_PHONE_NUMBER + ", "
                 + Contacts.LOOKUP_KEY + ", "
                 + Contacts.PHOTO_ID + ", "
-                + Contacts.IN_VISIBLE_GROUP + ", "
                 + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
                         + " AS " + Contacts.LAST_TIME_CONTACTED + ", "
                 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
@@ -1168,16 +1212,13 @@
         String contactsSelect = "SELECT "
                 + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
                 + contactsColumns
-                + " FROM " + Tables.CONTACTS;
-
-        String restrictedContactsSelect = "SELECT "
-                + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
-                + contactsColumns
                 + " FROM " + Tables.CONTACTS
-                + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0";
+                + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
+                +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
 
         db.execSQL("CREATE VIEW " + Views.CONTACTS_ALL + " AS " + contactsSelect);
-        db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + restrictedContactsSelect);
+        db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + contactsSelect
+                + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0");
     }
 
     private static void createGroupsView(SQLiteDatabase db) {
@@ -1336,18 +1377,133 @@
             oldVersion++;
         }
 
-        if (oldVersion == 104) {
+        if (oldVersion == 104 || oldVersion == 201) {
             LegacyApiSupport.createViews(db);
             LegacyApiSupport.createSettingsTable(db);
             oldVersion++;
         }
 
+        if (oldVersion == 105) {
+            addColumnPhoneNumberMinMatch(db);
+            oldVersion = 202;
+        }
+
+        if (oldVersion == 202) {
+            addNameRawContactIdColumn(db);
+            createContactsViews(db);
+            oldVersion++;
+        }
+
         if (oldVersion != newVersion) {
             throw new IllegalStateException(
                     "error upgrading the database to version " + newVersion);
         }
     }
 
+    private void addColumnPhoneNumberMinMatch(SQLiteDatabase db) {
+        db.execSQL(
+                "ALTER TABLE " + Tables.PHONE_LOOKUP +
+                " ADD " + PhoneLookupColumns.MIN_MATCH + " TEXT;");
+
+        db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
+                PhoneLookupColumns.MIN_MATCH + "," +
+                PhoneLookupColumns.RAW_CONTACT_ID + "," +
+                PhoneLookupColumns.DATA_ID +
+        ");");
+
+        updateIndexStats(db, Tables.PHONE_LOOKUP,
+                "phone_lookup_min_match_index", "10000 2 2 1");
+
+        SQLiteStatement update = db.compileStatement(
+                "UPDATE " + Tables.PHONE_LOOKUP +
+                " SET " + PhoneLookupColumns.MIN_MATCH + "=?" +
+                " WHERE " + PhoneLookupColumns.DATA_ID + "=?");
+
+        // Populate the new column
+        Cursor c = db.query(Tables.PHONE_LOOKUP + " JOIN " + Tables.DATA +
+                " ON (" + PhoneLookupColumns.DATA_ID + "=" + DataColumns.CONCRETE_ID + ")",
+                new String[]{Data._ID, Phone.NUMBER}, null, null, null, null, null);
+        try {
+            while (c.moveToNext()) {
+                long dataId = c.getLong(0);
+                String number = c.getString(1);
+                if (!TextUtils.isEmpty(number)) {
+                    update.bindString(1, PhoneNumberUtils.toCallerIDMinMatch(number));
+                    update.bindLong(2, dataId);
+                    update.execute();
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    private static void addNameRawContactIdColumn(SQLiteDatabase db) {
+        db.execSQL(
+                "ALTER TABLE " + Tables.CONTACTS +
+                " ADD " + Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)");
+        db.execSQL(
+                "ALTER TABLE " + Tables.RAW_CONTACTS +
+                " ADD " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
+                        + " INTEGER NOT NULL DEFAULT 0");
+
+        // For each Contact, find the RawContact that contributed the display name
+        db.execSQL(
+                "UPDATE " + Tables.CONTACTS +
+                " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
+                        " SELECT " + RawContacts._ID +
+                        " FROM " + Tables.RAW_CONTACTS +
+                        " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
+                        " AND " + RawContactsColumns.CONCRETE_DISPLAY_NAME + "=" +
+                                Tables.CONTACTS + "." + Contacts.DISPLAY_NAME +
+                        " ORDER BY " + RawContacts._ID +
+                        " LIMIT 1)"
+        );
+
+        db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
+                Contacts.NAME_RAW_CONTACT_ID +
+        ");");
+
+        // If for some unknown reason we missed some names, let's make sure there are
+        // no contacts without a name, picking a raw contact "at random".
+        db.execSQL(
+                "UPDATE " + Tables.CONTACTS +
+                " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
+                        " SELECT " + RawContacts._ID +
+                        " FROM " + Tables.RAW_CONTACTS +
+                        " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
+                        " ORDER BY " + RawContacts._ID +
+                        " LIMIT 1)" +
+                " WHERE " + Contacts.NAME_RAW_CONTACT_ID + " IS NULL"
+        );
+
+        // Wipe out DISPLAY_NAME on the Contacts table as it is no longer in use.
+        db.execSQL(
+                "UPDATE " + Tables.CONTACTS +
+                " SET " + Contacts.DISPLAY_NAME + "=NULL"
+        );
+
+        // Copy the IN_VISIBLE_GROUP flag down to all raw contacts to allow
+        // indexing on (display_name, in_visible_group)
+        db.execSQL(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" +
+                        "SELECT " + Contacts.IN_VISIBLE_GROUP +
+                        " FROM " + Tables.CONTACTS +
+                        " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")"
+        );
+
+        db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
+                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+                RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
+        ");");
+
+        db.execSQL("DROP INDEX contacts_visible_index");
+        db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
+                Contacts.IN_VISIBLE_GROUP +
+        ");");
+    }
+
     /**
      * Adds index stats into the SQLite database to force it to always use the lookup indexes.
      */
@@ -1376,6 +1532,8 @@
 
             updateIndexStats(db, Tables.PHONE_LOOKUP,
                     "phone_lookup_index", "10000 2 2 1");
+            updateIndexStats(db, Tables.PHONE_LOOKUP,
+                    "phone_lookup_min_match_index", "10000 2 2 1");
 
             updateIndexStats(db, Tables.DATA,
                     "data_mimetype_data1_index", "60000 5000 2");
@@ -1414,6 +1572,8 @@
             close();
             db = super.getWritableDatabase();
         }
+        // let {@link SQLiteDatabase} cache my compiled-sql statements.
+        db.setMaxSqlCacheSize(MAX_CACHE_SIZE_FOR_CONTACTS_DB);
         return db;
     }
 
@@ -1435,8 +1595,6 @@
         db.execSQL("DELETE FROM " + Tables.CALLS + ";");
 
         // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
-
-        db.execSQL("VACUUM;");
     }
 
     /**
@@ -1554,16 +1712,20 @@
         final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
         mVisibleUpdate.bindLong(1, groupMembershipMimetypeId);
         mVisibleUpdate.execute();
+        mVisibleUpdateRawContacts.execute();
     }
 
     /**
      * Update {@link Contacts#IN_VISIBLE_GROUP} for a specific contact.
      */
-    public void updateContactVisible(long aggId) {
+    public void updateContactVisible(long contactId) {
         final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
         mVisibleSpecificUpdate.bindLong(1, groupMembershipMimetypeId);
-        mVisibleSpecificUpdate.bindLong(2, aggId);
+        mVisibleSpecificUpdate.bindLong(2, contactId);
         mVisibleSpecificUpdate.execute();
+
+        mVisibleSpecificUpdateRawContacts.bindLong(1, contactId);
+        mVisibleSpecificUpdateRawContacts.execute();
     }
 
     /**
@@ -1592,25 +1754,25 @@
     }
 
     public void buildPhoneLookupAndRawContactQuery(SQLiteQueryBuilder qb, String number) {
-        String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+        String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
         qb.setTables(Tables.DATA_JOIN_RAW_CONTACTS +
                 " JOIN " + Tables.PHONE_LOOKUP
                 + " ON(" + DataColumns.CONCRETE_ID + "=" + PhoneLookupColumns.DATA_ID + ")");
 
         StringBuilder sb = new StringBuilder();
-        sb.append(PhoneLookupColumns.NORMALIZED_NUMBER + " GLOB '");
-        sb.append(normalizedNumber);
-        sb.append("*' AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
+        sb.append(PhoneLookupColumns.MIN_MATCH + "='");
+        sb.append(minMatch);
+        sb.append("' AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
         DatabaseUtils.appendEscapedSQLString(sb, number);
-        sb.append(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
+        sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
 
         qb.appendWhere(sb.toString());
     }
 
     public void buildPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number) {
-        String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+        String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
         StringBuilder sb = new StringBuilder();
-        appendPhoneLookupTables(sb, normalizedNumber, true);
+        appendPhoneLookupTables(sb, minMatch, true);
         qb.setTables(sb.toString());
 
         sb = new StringBuilder();
@@ -1620,33 +1782,37 @@
 
     public String buildPhoneLookupAsNestedQuery(String number) {
         StringBuilder sb = new StringBuilder();
-        final String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+        final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
         sb.append("(SELECT DISTINCT raw_contact_id" + " FROM ");
-        appendPhoneLookupTables(sb, normalizedNumber, false);
+        appendPhoneLookupTables(sb, minMatch, false);
         sb.append(" WHERE ");
         appendPhoneLookupSelection(sb, number);
         sb.append(")");
         return sb.toString();
     }
 
-    private void appendPhoneLookupTables(StringBuilder sb, final String normalizedNumber,
+    private void appendPhoneLookupTables(StringBuilder sb, final String minMatch,
             boolean joinContacts) {
         sb.append(Tables.RAW_CONTACTS);
         if (joinContacts) {
-            sb.append(" JOIN " + getContactView() + " contacts"
-                    + " ON (contacts._id = raw_contacts.contact_id)");
+            sb.append(" JOIN " + getContactView() + " contacts_view"
+                    + " ON (contacts_view._id = raw_contacts.contact_id)");
         }
         sb.append(", (SELECT data_id FROM phone_lookup "
-                + "WHERE (phone_lookup.normalized_number GLOB '");
-        sb.append(normalizedNumber);
-        sb.append("*')) AS lookup, " + Tables.DATA);
+                + "WHERE (" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.MIN_MATCH + " = '");
+        sb.append(minMatch);
+        sb.append("')) AS lookup, " + Tables.DATA);
     }
 
     private void appendPhoneLookupSelection(StringBuilder sb, String number) {
         sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id"
                 + " AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
         DatabaseUtils.appendEscapedSQLString(sb, number);
-        sb.append(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
+        sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
+    }
+
+    public String getUseStrictPhoneNumberComparisonParameter() {
+        return mUseStrictPhoneNumberComparison ? "1" : "0";
     }
 
     /**
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 57e1e5d..807a46b 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -60,6 +60,7 @@
 import android.content.UriMatcher;
 import android.content.SharedPreferences.Editor;
 import android.content.res.AssetFileDescriptor;
+import android.database.CharArrayBuffer;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteConstraintException;
@@ -110,6 +111,7 @@
 import java.io.OutputStream;
 import java.lang.ref.SoftReference;
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -214,20 +216,6 @@
 
     private static final int RAW_CONTACT_ENTITIES = 15001;
 
-    private interface ContactsQuery {
-        public static final String TABLE = Tables.RAW_CONTACTS;
-
-        public static final String[] PROJECTION = new String[] {
-            RawContactsColumns.CONCRETE_ID,
-            RawContacts.ACCOUNT_NAME,
-            RawContacts.ACCOUNT_TYPE,
-        };
-
-        public static final int RAW_CONTACT_ID = 0;
-        public static final int ACCOUNT_NAME = 1;
-        public static final int ACCOUNT_TYPE = 2;
-    }
-
     private interface DataContactsQuery {
         public static final String TABLE = "data "
                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
@@ -244,22 +232,6 @@
         public static final int CONTACT_ID = 2;
     }
 
-    private interface DisplayNameQuery {
-        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
-
-        public static final String[] COLUMNS = new String[] {
-            MimetypesColumns.MIMETYPE,
-            Data.IS_PRIMARY,
-            Data.DATA1,
-            Organization.TITLE,
-        };
-
-        public static final int MIMETYPE = 0;
-        public static final int IS_PRIMARY = 1;
-        public static final int DATA = 2;
-        public static final int TITLE = 3;
-    }
-
     private interface DataDeleteQuery {
         public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
 
@@ -295,39 +267,18 @@
     }
 
 
-    private interface NicknameLookupQuery {
-        String TABLE = Tables.NICKNAME_LOOKUP;
-
-        String[] COLUMNS = new String[] {
-            NicknameLookupColumns.CLUSTER
-        };
-
-        int CLUSTER = 0;
-    }
-
     private interface RawContactsQuery {
         String TABLE = Tables.RAW_CONTACTS;
 
         String[] COLUMNS = new String[] {
-                ContactsContract.RawContacts.DELETED
+                RawContacts.DELETED,
+                RawContacts.ACCOUNT_TYPE,
+                RawContacts.ACCOUNT_NAME,
         };
 
         int DELETED = 0;
-    }
-
-    private static final HashMap<String, Integer> sDisplayNameSources;
-    static {
-        sDisplayNameSources = new HashMap<String, Integer>();
-        sDisplayNameSources.put(StructuredName.CONTENT_ITEM_TYPE,
-                DisplayNameSources.STRUCTURED_NAME);
-        sDisplayNameSources.put(Nickname.CONTENT_ITEM_TYPE,
-                DisplayNameSources.NICKNAME);
-        sDisplayNameSources.put(Organization.CONTENT_ITEM_TYPE,
-                DisplayNameSources.ORGANIZATION);
-        sDisplayNameSources.put(Phone.CONTENT_ITEM_TYPE,
-                DisplayNameSources.PHONE);
-        sDisplayNameSources.put(Email.CONTENT_ITEM_TYPE,
-                DisplayNameSources.EMAIL);
+        int ACCOUNT_TYPE = 1;
+        int ACCOUNT_NAME = 2;
     }
 
     public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
@@ -347,6 +298,18 @@
                                     + " FROM " + Tables.GROUPS
                                     + " WHERE " + Groups.TITLE + "=?)))";
 
+    /** Sql for updating DIRTY flag on multiple raw contacts */
+    private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
+            "UPDATE " + Tables.RAW_CONTACTS +
+            " SET " + RawContacts.DIRTY + "=1" +
+            " WHERE " + RawContacts._ID + " IN (";
+
+    /** Sql for updating VERSION on multiple raw contacts */
+    private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
+            "UPDATE " + Tables.RAW_CONTACTS +
+            " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
+            " WHERE " + RawContacts._ID + " IN (";
+
     /** Contains just BaseColumns._COUNT */
     private static final HashMap<String, String> sCountProjectionMap;
     /** Contains just the contacts columns */
@@ -379,6 +342,12 @@
     /** Contains Live Folders columns */
     private static final HashMap<String, String> sLiveFoldersProjectionMap;
 
+    // where clause to update the status_updates table
+    private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
+            StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
+            " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
+            " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
+
     /** Precompiled sql statement for setting a data record to the primary. */
     private SQLiteStatement mSetPrimaryStatement;
     /** Precompiled sql statement for setting a data record to the super primary. */
@@ -387,8 +356,6 @@
     private SQLiteStatement mContactsLastTimeContactedUpdate;
     /** Precompiled sql statement for updating a contact display name */
     private SQLiteStatement mRawContactDisplayNameUpdate;
-    /** Precompiled sql statement for marking a raw contact as dirty */
-    private SQLiteStatement mRawContactDirtyUpdate;
     /** Precompiled sql statement for updating an aggregated status update */
     private SQLiteStatement mLastStatusUpdate;
     private SQLiteStatement mNameLookupInsert;
@@ -401,7 +368,15 @@
 
     private long mMimeTypeIdEmail;
     private long mMimeTypeIdIm;
+    private long mMimeTypeIdStructuredName;
+    private long mMimeTypeIdOrganization;
+    private long mMimeTypeIdNickname;
+    private long mMimeTypeIdPhone;
     private StringBuilder mSb = new StringBuilder();
+    private String[] mSelectionArgs1 = new String[1];
+    private String[] mSelectionArgs2 = new String[2];
+    private String[] mSelectionArgs3 = new String[3];
+    private Account mAccount;
 
     static {
         // Contacts URI matching table
@@ -732,28 +707,37 @@
 
         sPhoneLookupProjectionMap = new HashMap<String, String>();
         sPhoneLookupProjectionMap.put(PhoneLookup._ID,
-                ContactsColumns.CONCRETE_ID + " AS " + PhoneLookup._ID);
+                "contacts_view." + Contacts._ID
+                        + " AS " + PhoneLookup._ID);
         sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY,
-                Contacts.LOOKUP_KEY + " AS " + PhoneLookup.LOOKUP_KEY);
+                "contacts_view." + Contacts.LOOKUP_KEY
+                        + " AS " + PhoneLookup.LOOKUP_KEY);
         sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
-                ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME);
+                "contacts_view." + Contacts.DISPLAY_NAME
+                        + " AS " + PhoneLookup.DISPLAY_NAME);
         sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
-                ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
+                "contacts_view." + Contacts.LAST_TIME_CONTACTED
                         + " AS " + PhoneLookup.LAST_TIME_CONTACTED);
         sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
-                ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED);
+                "contacts_view." + Contacts.TIMES_CONTACTED
+                        + " AS " + PhoneLookup.TIMES_CONTACTED);
         sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
-                ContactsColumns.CONCRETE_STARRED + " AS " + PhoneLookup.STARRED);
+                "contacts_view." + Contacts.STARRED
+                        + " AS " + PhoneLookup.STARRED);
         sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
-                Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
+                "contacts_view." + Contacts.IN_VISIBLE_GROUP
+                        + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
         sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
-                Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID);
+                "contacts_view." + Contacts.PHOTO_ID
+                        + " AS " + PhoneLookup.PHOTO_ID);
         sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
-                ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE);
+                "contacts_view." + Contacts.CUSTOM_RINGTONE
+                        + " AS " + PhoneLookup.CUSTOM_RINGTONE);
         sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
-                Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
+                "contacts_view." + Contacts.HAS_PHONE_NUMBER
+                        + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
         sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
-                ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
+                "contacts_view." + Contacts.SEND_TO_VOICEMAIL
                         + " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
         sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
                 Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
@@ -875,6 +859,7 @@
         protected final String mMimetype;
         protected long mMimetypeId;
 
+        @SuppressWarnings("all")
         public DataRowHandler(String mimetype) {
             mMimetype = mimetype;
 
@@ -933,7 +918,8 @@
             }
 
             if (values.size() > 0) {
-                mDb.update(Tables.DATA, values, Data._ID + " = " + dataId, null);
+                mSelectionArgs1[0] = String.valueOf(dataId);
+                mDb.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1);
             }
 
             if (!callerIsSyncAdapter) {
@@ -945,8 +931,10 @@
             long dataId = c.getLong(DataDeleteQuery._ID);
             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
             boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
-            int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
-            db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
+            mSelectionArgs1[0] = String.valueOf(dataId);
+            int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
+            mSelectionArgs1[0] = String.valueOf(rawContactId);
+            db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
             if (count != 0 && primary) {
                 fixPrimary(db, rawContactId);
             }
@@ -954,16 +942,15 @@
         }
 
         private void fixPrimary(SQLiteDatabase db, long rawContactId) {
-            long newPrimaryId = findNewPrimaryDataId(db, rawContactId);
-            if (newPrimaryId != -1) {
-                setIsPrimary(rawContactId, newPrimaryId, getMimeTypeId());
-            }
-        }
-
-        protected long findNewPrimaryDataId(SQLiteDatabase db, long rawContactId) {
+            long mimeTypeId = getMimeTypeId();
             long primaryId = -1;
             int primaryType = -1;
-            Cursor c = queryData(db, rawContactId);
+            mSelectionArgs1[0] = String.valueOf(rawContactId);
+            Cursor c = db.query(DataDeleteQuery.TABLE,
+                    DataDeleteQuery.CONCRETE_COLUMNS,
+                    Data.RAW_CONTACT_ID + "=?" +
+                        " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
+                    mSelectionArgs1, null, null, null);
             try {
                 while (c.moveToNext()) {
                     long dataId = c.getLong(DataDeleteQuery._ID);
@@ -976,7 +963,9 @@
             } finally {
                 c.close();
             }
-            return primaryId;
+            if (primaryId != -1) {
+                setIsPrimary(rawContactId, primaryId, mimeTypeId);
+            }
         }
 
         /**
@@ -987,50 +976,10 @@
             return 0;
         }
 
-        protected Cursor queryData(SQLiteDatabase db, long rawContactId) {
-            return db.query(DataDeleteQuery.TABLE, DataDeleteQuery.CONCRETE_COLUMNS,
-                    Data.RAW_CONTACT_ID + "=" + rawContactId +
-                    " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'",
-                    null, null, null, null);
-        }
-
         protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
-            String bestDisplayName = null;
-            int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
-
-            Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS,
-                    Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null);
-            try {
-                while (c.moveToNext()) {
-                    String mimeType = c.getString(DisplayNameQuery.MIMETYPE);
-
-                    // Display name is at DATA1 in all type.  This is ensured in the constructor.
-                    String name = c.getString(DisplayNameQuery.DATA);
-                    if (TextUtils.isEmpty(name)
-                            && Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                        name = c.getString(DisplayNameQuery.TITLE);
-                    }
-                    boolean primary = StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)
-                        || (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0);
-
-                    if (name != null) {
-                        Integer source = sDisplayNameSources.get(mimeType);
-                        if (source != null
-                                && (source > bestDisplayNameSource
-                                        || (source == bestDisplayNameSource && primary))) {
-                            bestDisplayNameSource = source;
-                            bestDisplayName = name;
-                        }
-                    }
-                }
-
-            } finally {
-                c.close();
-            }
-
-            setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource);
             if (!isNewRawContact(rawContactId)) {
-                mContactAggregator.updateDisplayName(db, rawContactId);
+                updateRawContactDisplayName(db, rawContactId);
+                mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
             }
         }
 
@@ -1045,8 +994,9 @@
         public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
                 ContentValues update) {
             final ContentValues values = new ContentValues();
-            final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=" + dataId,
-                    null, null, null, null);
+            mSelectionArgs1[0] = String.valueOf(dataId);
+            final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
+                    mSelectionArgs1, null, null, null);
             try {
                 if (cursor.moveToFirst()) {
                     for (int i = 0; i < cursor.getColumnCount(); i++) {
@@ -1491,9 +1441,13 @@
                 phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
                 phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
                 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
+                phoneValues.put(PhoneLookupColumns.MIN_MATCH,
+                        PhoneNumberUtils.toCallerIDMinMatch(number));
+
                 db.replace(Tables.PHONE_LOOKUP, null, phoneValues);
             } else {
-                db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=" + dataId, null);
+                mSelectionArgs1[0] = String.valueOf(dataId);
+                db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1);
             }
         }
 
@@ -1573,7 +1527,8 @@
 
             if (containsGroupSourceId) {
                 final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
-                final long groupId = getOrMakeGroup(db, rawContactId, sourceId);
+                final long groupId = getOrMakeGroup(db, rawContactId, sourceId,
+                        mInsertedRawContacts.get(rawContactId));
                 values.remove(GroupMembership.GROUP_SOURCE_ID);
                 values.put(GroupMembership.GROUP_ROW_ID, groupId);
             }
@@ -1622,45 +1577,54 @@
         }
     }
 
+    /**
+     * An entry in group id cache. It maps the combination of (account type, account name
+     * and source id) to group row id.
+     */
+    public class GroupIdCacheEntry {
+        String accountType;
+        String accountName;
+        String sourceId;
+        long groupId;
+    }
 
     private HashMap<String, DataRowHandler> mDataRowHandlers;
-    private final ContactAggregationScheduler mAggregationScheduler;
     private ContactsDatabaseHelper mDbHelper;
 
     private NameSplitter mNameSplitter;
     private NameLookupBuilder mNameLookupBuilder;
-    private HashMap<String, SoftReference<String[]>> mNicknameClusterCache =
-            new HashMap<String, SoftReference<String[]>>();
+
+    // We will use this much memory (in bits) to optimize the nickname cluster lookup
+    private static final int NICKNAME_BLOOM_FILTER_SIZE = 0x1FFF;   // =long[128]
+    private BitSet mNicknameBloomFilter;
+
+    private HashMap<String, SoftReference<String[]>> mNicknameClusterCache = Maps.newHashMap();
+
     private PostalSplitter mPostalSplitter;
 
+    // We don't need a soft cache for groups - the assumption is that there will only
+    // be a small number of contact groups. The cache is keyed off source id.  The value
+    // is a list of groups with this group id.
+    private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
+
     private ContactAggregator mContactAggregator;
     private LegacyApiSupport mLegacyApiSupport;
     private GlobalSearchSupport mGlobalSearchSupport;
 
     private ContentValues mValues = new ContentValues();
+    private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128);
 
     private volatile CountDownLatch mAccessLatch;
-    private boolean mImportMode;
 
-    private HashSet<Long> mInsertedRawContacts = Sets.newHashSet();
+    private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap();
     private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
+    private HashSet<Long> mDirtyRawContacts = Sets.newHashSet();
     private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap();
 
     private boolean mVisibleTouched = false;
 
     private boolean mSyncToNetwork;
 
-    public ContactsProvider2() {
-        this(new ContactAggregationScheduler());
-    }
-
-    /**
-     * Constructor for testing.
-     */
-    /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
-        mAggregationScheduler = scheduler;
-    }
-
     @Override
     public boolean onCreate() {
         super.onCreate();
@@ -1677,7 +1641,7 @@
         mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
         mGlobalSearchSupport = new GlobalSearchSupport(this);
         mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
-        mContactAggregator = new ContactAggregator(this, mDbHelper, mAggregationScheduler);
+        mContactAggregator = new ContactAggregator(this, mDbHelper);
         mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
 
         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
@@ -1711,12 +1675,9 @@
                         + RawContactsColumns.DISPLAY_NAME_SOURCE + "=?" +
                 " WHERE " + RawContacts._ID + "=?");
 
-        mRawContactDirtyUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
-                + RawContacts.DIRTY + "=1 WHERE " + RawContacts._ID + "=?");
-
         mLastStatusUpdate = db.compileStatement(
-                "UPDATE " + Tables.CONTACTS
-                + " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
+                "UPDATE " + Tables.CONTACTS +
+                " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
                         "(SELECT " + DataColumns.CONCRETE_ID +
                         " FROM " + Tables.STATUS_UPDATES +
                         " JOIN " + Tables.DATA +
@@ -1728,8 +1689,8 @@
                         " WHERE " + RawContacts.CONTACT_ID + "=?" +
                         " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
                                 + StatusUpdates.STATUS +
-                        " LIMIT 1)"
-                + " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
+                        " LIMIT 1)" +
+                " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
 
         final Locale locale = Locale.getDefault();
         mNameSplitter = new NameSplitter(
@@ -1810,6 +1771,11 @@
 
         mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
         mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
+        mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
+        mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
+        mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE);
+        mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+        preloadNicknameBloomFilter();
         return (db != null);
     }
 
@@ -1824,10 +1790,6 @@
         return ContactsDatabaseHelper.getInstance(context);
     }
 
-    /* package */ ContactAggregationScheduler getContactAggregationScheduler() {
-        return mAggregationScheduler;
-    }
-
     /* package */ NameSplitter getNameSplitter() {
         return mNameSplitter;
     }
@@ -1852,6 +1814,7 @@
             @Override
             public void run() {
                 if (importLegacyContacts()) {
+                    // TODO aggregate all newly added raw contacts
 
                     /*
                      * When the import process is done, we can unlock the provider and
@@ -1859,7 +1822,6 @@
                      */
                     mAccessLatch.countDown();
                     mAccessLatch = null;
-                    scheduleContactAggregation();
                 }
             }
         };
@@ -1884,7 +1846,6 @@
     /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
         boolean aggregatorEnabled = mContactAggregator.isEnabled();
         mContactAggregator.setEnabled(false);
-        mImportMode = true;
         try {
             importer.importContacts();
             mContactAggregator.setEnabled(aggregatorEnabled);
@@ -1892,20 +1853,9 @@
         } catch (Throwable e) {
            Log.e(TAG, "Legacy contact import failed", e);
            return false;
-        } finally {
-            mImportMode = false;
         }
     }
 
-    @Override
-    protected void finalize() throws Throwable {
-        if (mContactAggregator != null) {
-            mContactAggregator.quit();
-        }
-
-        super.finalize();
-    }
-
     /**
      * Wipes all data from the contacts database.
      */
@@ -1974,10 +1924,12 @@
         mInsertedRawContacts.clear();
         mUpdatedRawContacts.clear();
         mUpdatedSyncStates.clear();
+        mDirtyRawContacts.clear();
     }
 
     @Override
     protected void beforeTransactionCommit() {
+
         if (VERBOSE_LOGGING) {
             Log.v(TAG, "beforeTransactionCommit");
         }
@@ -1994,15 +1946,26 @@
         if (VERBOSE_LOGGING) {
             Log.v(TAG, "flushTransactionChanges");
         }
-        for (long rawContactId : mInsertedRawContacts) {
-            mContactAggregator.insertContact(mDb, rawContactId);
+
+        for (long rawContactId : mInsertedRawContacts.keySet()) {
+            updateRawContactDisplayName(mDb, rawContactId);
+            mContactAggregator.onRawContactInsert(mDb, rawContactId);
         }
 
-        String ids;
+        if (!mDirtyRawContacts.isEmpty()) {
+            mSb.setLength(0);
+            mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
+            appendIds(mSb, mDirtyRawContacts);
+            mSb.append(")");
+            mDb.execSQL(mSb.toString());
+        }
+
         if (!mUpdatedRawContacts.isEmpty()) {
-            ids = buildIdsString(mUpdatedRawContacts);
-            mDb.execSQL("UPDATE raw_contacts SET version = version + 1 WHERE _id in " + ids,
-                    new Object[]{});
+            mSb.setLength(0);
+            mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
+            appendIds(mSb, mUpdatedRawContacts);
+            mSb.append(")");
+            mDb.execSQL(mSb.toString());
         }
 
         for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
@@ -2013,19 +1976,16 @@
         clearTransactionalChanges();
     }
 
-    private String buildIdsString(HashSet<Long> ids) {
-        StringBuilder idsBuilder = null;
+    /**
+     * Appends comma separated ids.
+     * @param ids Should not be empty
+     */
+    private void appendIds(StringBuilder sb, HashSet<Long> ids) {
         for (long id : ids) {
-            if (idsBuilder == null) {
-                idsBuilder = new StringBuilder();
-                idsBuilder.append("(");
-            } else {
-                idsBuilder.append(",");
-            }
-            idsBuilder.append(id);
+            sb.append(id).append(',');
         }
-        idsBuilder.append(")");
-        return idsBuilder.toString();
+
+        sb.setLength(sb.length() - 1); // Yank the last comma
     }
 
     @Override
@@ -2039,12 +1999,8 @@
                 syncToNetwork);
     }
 
-    protected void scheduleContactAggregation() {
-        mContactAggregator.schedule();
-    }
-
     private boolean isNewRawContact(long rawContactId) {
-        return mInsertedRawContacts.contains(rawContactId);
+        return mInsertedRawContacts.containsKey(rawContactId);
     }
 
     private DataRowHandler getDataRowHandler(final String mimeType) {
@@ -2059,7 +2015,7 @@
     @Override
     protected Uri insertInTransaction(Uri uri, ContentValues values) {
         if (VERBOSE_LOGGING) {
-            Log.v(TAG, "insertInTransaction: " + uri);
+            Log.v(TAG, "insertInTransaction: " + uri + " " + values);
         }
 
         final boolean callerIsSyncAdapter =
@@ -2079,8 +2035,7 @@
             }
 
             case RAW_CONTACTS: {
-                final Account account = readAccountFromQueryParams(uri);
-                id = insertRawContact(values, account);
+                id = insertRawContact(uri, values);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 break;
             }
@@ -2099,8 +2054,7 @@
             }
 
             case GROUPS: {
-                final Account account = readAccountFromQueryParams(uri);
-                id = insertGroup(uri, values, account, callerIsSyncAdapter);
+                id = insertGroup(uri, values, callerIsSyncAdapter);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 break;
             }
@@ -2131,25 +2085,49 @@
     /**
      * If account is non-null then store it in the values. If the account is already
      * specified in the values then it must be consistent with the account, if it is non-null.
-     * @param values the ContentValues to read from and update
-     * @param account the explicitly provided Account
-     * @return false if the accounts are inconsistent
+     * @param uri the ContentValues to read from and update
+     * @param values the explicitly provided Account
+     * @return false if the parameters are inconsistent
      */
-    private boolean resolveAccount(ContentValues values, Account account) {
-        // If either is specified then both must be specified.
-        final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
-        final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
-        if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
-            final Account valuesAccount = new Account(accountName, accountType);
-            if (account != null && !valuesAccount.equals(account)) {
+    private boolean resolveAccount(Uri uri, ContentValues values) {
+        String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+        String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
+
+        if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
+            accountName = null;
+            accountType = null;
+        }
+
+        String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+        String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+
+        if (TextUtils.isEmpty(valueAccountName) && TextUtils.isEmpty(valueAccountType)) {
+            values.put(RawContacts.ACCOUNT_NAME, accountName);
+            values.put(RawContacts.ACCOUNT_TYPE, accountType);
+        } else {
+            if (accountName != null && !accountName.equals(valueAccountName)) {
                 return false;
             }
-            account = valuesAccount;
+
+            if (accountType != null && !accountType.equals(valueAccountType)) {
+                return false;
+            }
+
+            accountName = valueAccountName;
+            accountType = valueAccountType;
         }
-        if (account != null) {
-            values.put(RawContacts.ACCOUNT_NAME, account.name);
-            values.put(RawContacts.ACCOUNT_TYPE, account.type);
+
+        if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
+            mAccount = null;
+            return true;
         }
+
+        if (mAccount == null
+                || !mAccount.name.equals(accountName)
+                || !mAccount.type.equals(accountType)) {
+            mAccount = new Account(accountName, accountType);
+        }
+
         return true;
     }
 
@@ -2166,29 +2144,30 @@
     /**
      * Inserts an item in the contacts table
      *
-     * @param values the values for the new row
-     * @param account the account this contact should be associated with. may be null.
+     * @param uri the values for the new row
+     * @param values the account this contact should be associated with. may be null.
      * @return the row ID of the newly created row
      */
-    private long insertRawContact(ContentValues values, Account account) {
-        ContentValues overriddenValues = new ContentValues(values);
-        overriddenValues.putNull(RawContacts.CONTACT_ID);
-        if (!resolveAccount(overriddenValues, account)) {
+    private long insertRawContact(Uri uri, ContentValues values) {
+        mValues.clear();
+        mValues.putAll(values);
+        mValues.putNull(RawContacts.CONTACT_ID);
+
+        if (!resolveAccount(uri, mValues)) {
             return -1;
         }
 
         if (values.containsKey(RawContacts.DELETED)
                 && values.getAsInteger(RawContacts.DELETED) != 0) {
-            overriddenValues.put(RawContacts.AGGREGATION_MODE,
-                    RawContacts.AGGREGATION_MODE_DISABLED);
+            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
         }
 
-        long rawContactId =
-                mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
+        long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues);
         mContactAggregator.markNewForAggregation(rawContactId);
 
         // Trigger creation of a Contact based on this RawContact at the end of transaction
-        mInsertedRawContacts.add(rawContactId);
+        mInsertedRawContacts.put(rawContactId, mAccount);
+
         return rawContactId;
     }
 
@@ -2276,35 +2255,60 @@
      * @throws IllegalArgumentException if the contact is not associated with an account
      * @throws IllegalStateException if a group needs to be created but the creation failed
      */
-    private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) {
-        Account account = null;
-        Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "="
-                + rawContactId, null, null, null, null);
-        try {
-            if (c.moveToNext()) {
-                final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
-                final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
-                if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
-                    account = new Account(accountName, accountType);
+    private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId,
+            Account account) {
+
+        if (account == null) {
+            mSelectionArgs1[0] = String.valueOf(rawContactId);
+            Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
+                    RawContacts._ID + "=?", mSelectionArgs1, null, null, null);
+            try {
+                if (c.moveToFirst()) {
+                    String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME);
+                    String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
+                    if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+                        account = new Account(accountName, accountType);
+                    }
                 }
+            } finally {
+                c.close();
             }
-        } finally {
-            c.close();
         }
+
         if (account == null) {
             throw new IllegalArgumentException("if the groupmembership only "
-                    + "has a sourceid the the contact must be associate with "
+                    + "has a sourceid the the contact must be associated with "
                     + "an account");
         }
 
+        ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId);
+        if (entries == null) {
+            entries = new ArrayList<GroupIdCacheEntry>(1);
+            mGroupIdCache.put(sourceId, entries);
+        }
+
+        int count = entries.size();
+        for (int i = 0; i < count; i++) {
+            GroupIdCacheEntry entry = entries.get(i);
+            if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) {
+                return entry.groupId;
+            }
+        }
+
+        GroupIdCacheEntry entry = new GroupIdCacheEntry();
+        entry.accountName = account.name;
+        entry.accountType = account.type;
+        entry.sourceId = sourceId;
+        entries.add(0, entry);
+
         // look up the group that contains this sourceId and has the same account name and type
         // as the contact refered to by rawContactId
-        c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
+        Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
                 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
                 new String[]{sourceId, account.name, account.type}, null, null, null);
         try {
-            if (c.moveToNext()) {
-                return c.getLong(0);
+            if (c.moveToFirst()) {
+                entry.groupId = c.getLong(0);
             } else {
                 ContentValues groupValues = new ContentValues();
                 groupValues.put(Groups.ACCOUNT_NAME, account.name);
@@ -2315,11 +2319,94 @@
                     throw new IllegalStateException("unable to create a new group with "
                             + "this sourceid: " + groupValues);
                 }
-                return groupId;
+                entry.groupId = groupId;
             }
         } finally {
             c.close();
         }
+
+        return entry.groupId;
+    }
+
+    private interface DisplayNameQuery {
+        public static final String RAW_SQL =
+                "SELECT "
+                        + DataColumns.MIMETYPE_ID + ","
+                        + Data.IS_PRIMARY + ","
+                        + Data.DATA1 + ","
+                        + Organization.TITLE +
+                " FROM " + Tables.DATA +
+                " WHERE " + Data.RAW_CONTACT_ID + "=?" +
+                        " AND (" + Data.DATA1 + " NOT NULL OR " +
+                                Organization.TITLE + " NOT NULL)";
+
+        public static final int MIMETYPE = 0;
+        public static final int IS_PRIMARY = 1;
+        public static final int DATA = 2;
+        public static final int TITLE = 3;
+    }
+
+    /**
+     * Updates a raw contact display name based on data rows, e.g. structured name,
+     * organization, email etc.
+     */
+    private void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
+        String bestDisplayName = null;
+        int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
+
+        mSelectionArgs1[0] = String.valueOf(rawContactId);
+        Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1);
+        try {
+            while (c.moveToNext()) {
+                int mimeType = c.getInt(DisplayNameQuery.MIMETYPE);
+
+                // Display name is at DATA1 in all type. This is ensured in the
+                // constructor.
+                mCharArrayBuffer.sizeCopied = 0;
+                c.copyStringToBuffer(DisplayNameQuery.DATA, mCharArrayBuffer);
+                if (mimeType == mMimeTypeIdOrganization && mCharArrayBuffer.sizeCopied == 0) {
+                    c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer);
+                }
+
+                if (mCharArrayBuffer.sizeCopied != 0) {
+                    int source = getDisplayNameSource(mimeType);
+                    if (source > bestDisplayNameSource) {
+                        bestDisplayNameSource = source;
+                        bestDisplayName = new String(mCharArrayBuffer.data, 0,
+                                mCharArrayBuffer.sizeCopied);
+                    } else if (source == bestDisplayNameSource
+                            && source != DisplayNameSources.UNDEFINED) {
+                        if (mimeType == mMimeTypeIdStructuredName
+                                || c.getInt(DisplayNameQuery.IS_PRIMARY) != 0) {
+                            bestDisplayNameSource = source;
+                            bestDisplayName = new String(mCharArrayBuffer.data, 0,
+                                    mCharArrayBuffer.sizeCopied);
+                        }
+                    }
+                }
+            }
+
+        } finally {
+            c.close();
+        }
+
+        setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource);
+    }
+
+    private int getDisplayNameSource(int mimeTypeId) {
+        if (mimeTypeId == mMimeTypeIdStructuredName) {
+            return DisplayNameSources.STRUCTURED_NAME;
+        } else if (mimeTypeId == mMimeTypeIdEmail) {
+            return DisplayNameSources.EMAIL;
+        } else if (mimeTypeId == mMimeTypeIdPhone) {
+            return DisplayNameSources.PHONE;
+        } else if (mimeTypeId == mMimeTypeIdOrganization) {
+            return DisplayNameSources.ORGANIZATION;
+        } else if (mimeTypeId == mMimeTypeIdNickname) {
+            return DisplayNameSources.NICKNAME;
+        } else {
+            return DisplayNameSources.UNDEFINED;
+        }
     }
 
     /**
@@ -2358,8 +2445,9 @@
 
         // Note that the query will return data according to the access restrictions,
         // so we don't need to worry about deleting data we don't have permission to read.
-        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=" + dataId, null,
-                null);
+        mSelectionArgs1[0] = String.valueOf(dataId);
+        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?",
+                mSelectionArgs1, null);
 
         try {
             if (!c.moveToFirst()) {
@@ -2395,27 +2483,28 @@
     /**
      * Inserts an item in the groups table
      */
-    private long insertGroup(Uri uri, ContentValues values, Account account,
-            boolean callerIsSyncAdapter) {
-        ContentValues overriddenValues = new ContentValues(values);
-        if (!resolveAccount(overriddenValues, account)) {
+    private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
+        mValues.clear();
+        mValues.putAll(values);
+
+        if (!resolveAccount(uri, mValues)) {
             return -1;
         }
 
         // Replace package with internal mapping
-        final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
+        final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
         if (packageName != null) {
-            overriddenValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
+            mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
         }
-        overriddenValues.remove(Groups.RES_PACKAGE);
+        mValues.remove(Groups.RES_PACKAGE);
 
         if (!callerIsSyncAdapter) {
-            overriddenValues.put(Groups.DIRTY, 1);
+            mValues.put(Groups.DIRTY, 1);
         }
 
-        long result = mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
+        long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
 
-        if (overriddenValues.containsKey(Groups.GROUP_VISIBLE)) {
+        if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
             mVisibleTouched = true;
         }
 
@@ -2661,12 +2750,15 @@
 
             case RAW_CONTACTS: {
                 int numDeletes = 0;
-                Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
+                Cursor c = mDb.query(Tables.RAW_CONTACTS,
+                        new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
                         appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
                 try {
                     while (c.moveToNext()) {
                         final long rawContactId = c.getLong(0);
-                        numDeletes += deleteRawContact(rawContactId, callerIsSyncAdapter);
+                        long contactId = c.getLong(1);
+                        numDeletes += deleteRawContact(rawContactId, contactId,
+                                callerIsSyncAdapter);
                     }
                 } finally {
                     c.close();
@@ -2676,7 +2768,8 @@
 
             case RAW_CONTACTS_ID: {
                 final long rawContactId = ContentUris.parseId(uri);
-                return deleteRawContact(rawContactId, callerIsSyncAdapter);
+                return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId),
+                        callerIsSyncAdapter);
             }
 
             case DATA: {
@@ -2691,7 +2784,8 @@
             case POSTALS_ID: {
                 long dataId = ContentUris.parseId(uri);
                 mSyncToNetwork |= !callerIsSyncAdapter;
-                return deleteData(Data._ID + "=" + dataId, null, callerIsSyncAdapter);
+                mSelectionArgs1[0] = String.valueOf(dataId);
+                return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
             }
 
             case GROUPS_ID: {
@@ -2732,14 +2826,8 @@
         }
     }
 
-    private static boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) {
-        final String flag = uri.getQueryParameter(name);
-        return flag == null
-                ? defaultValue
-                : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
-    }
-
     public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
+        mGroupIdCache.clear();
         final long groupMembershipMimetypeId = mDbHelper
                 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
         mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
@@ -2781,10 +2869,13 @@
         return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
     }
 
-    public int deleteRawContact(long rawContactId, boolean callerIsSyncAdapter) {
+    public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
+        mContactAggregator.invalidateAggregationExceptionCache();
         if (callerIsSyncAdapter) {
             mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
-            return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
+            int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
+            mContactAggregator.updateDisplayNameForContact(mDb, contactId);
+            return count;
         } else {
             mDbHelper.removeContactIfSingleton(rawContactId);
             return markRawContactAsDeleted(rawContactId);
@@ -2792,8 +2883,14 @@
     }
 
     private int deleteStatusUpdates(String selection, String[] selectionArgs) {
-        // TODO delete from both tables: presence and status_updates
-        return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
+      // delete from both tables: presence and status_updates
+      // TODO should account type/name be appended to the where clause?
+      if (VERBOSE_LOGGING) {
+          Log.v(TAG, "deleting data from status_updates for " + selection);
+      }
+      mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
+          selectionArgs);
+      return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
     }
 
     private int markRawContactAsDeleted(long rawContactId) {
@@ -2820,7 +2917,7 @@
         final int match = sUriMatcher.match(uri);
         if (match == SYNCSTATE_ID && selection == null) {
             long rowId = ContentUris.parseId(uri);
-            Object data = values.get(ContactsContract.SyncStateColumns.DATA);
+            Object data = values.get(ContactsContract.SyncState.DATA);
             mUpdatedSyncStates.put(rowId, data);
             return 1;
         }
@@ -2903,10 +3000,12 @@
             case RAW_CONTACTS_ID: {
                 long rawContactId = ContentUris.parseId(uri);
                 if (selection != null) {
-                    count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId
+                    selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+                    count = updateRawContacts(values, RawContacts._ID + "=?"
                                     + " AND(" + selection + ")", selectionArgs);
                 } else {
-                    count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId, null);
+                    mSelectionArgs1[0] = String.valueOf(rawContactId);
+                    count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1);
                 }
                 break;
             }
@@ -2922,7 +3021,8 @@
 
             case GROUPS_ID: {
                 long groupId = ContentUris.parseId(uri);
-                String selectionWithId = (Groups._ID + "=" + groupId + " ")
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
+                String selectionWithId = Groups._ID + "=? "
                         + (selection == null ? "" : " AND " + selection);
                 count = updateGroups(uri, values, selectionWithId, selectionArgs,
                         callerIsSyncAdapter);
@@ -2943,6 +3043,11 @@
                 break;
             }
 
+            case STATUS_UPDATES: {
+                count = updateStatusUpdate(uri, values, selection, selectionArgs);
+                break;
+            }
+
             default: {
                 mSyncToNetwork = true;
                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
@@ -2952,9 +3057,68 @@
         return count;
     }
 
+    private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
+        String[] selectionArgs) {
+        // update status_updates table, if status is provided
+        // TODO should account type/name be appended to the where clause?
+        int updateCount = 0;
+        ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
+        if (settableValues.size() > 0) {
+          updateCount = mDb.update(Tables.STATUS_UPDATES,
+                    settableValues,
+                    getWhereClauseForStatusUpdatesTable(selection),
+                    selectionArgs);
+        }
+
+        // now update the Presence table
+        settableValues = getSettableColumnsForPresenceTable(values);
+        if (settableValues.size() > 0) {
+          updateCount = mDb.update(Tables.PRESENCE, settableValues,
+                    selection, selectionArgs);
+        }
+        // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
+        // potentially get updated in this method.
+        return updateCount;
+    }
+
+    /**
+     * Build a where clause to select the rows to be updated in status_updates table.
+     */
+    private String getWhereClauseForStatusUpdatesTable(String selection) {
+        mSb.setLength(0);
+        mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
+        mSb.append(selection);
+        mSb.append(")");
+        return mSb.toString();
+    }
+
+    private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
+        mValues.clear();
+        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
+            StatusUpdates.STATUS);
+        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
+            StatusUpdates.STATUS_TIMESTAMP);
+        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
+            StatusUpdates.STATUS_RES_PACKAGE);
+        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
+            StatusUpdates.STATUS_LABEL);
+        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
+            StatusUpdates.STATUS_ICON);
+        return mValues;
+    }
+
+    private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
+        mValues.clear();
+        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
+            StatusUpdates.PRESENCE);
+        return mValues;
+    }
+
     private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
             String[] selectionArgs, boolean callerIsSyncAdapter) {
 
+        mGroupIdCache.clear();
+
         ContentValues updatedValues;
         if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
             updatedValues = mValues;
@@ -2972,8 +3136,9 @@
         if (updatedValues.containsKey(Groups.SHOULD_SYNC)
                 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
             final long groupId = ContentUris.parseId(uri);
+            mSelectionArgs1[0] = String.valueOf(groupId);
             Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
-                    Groups.ACCOUNT_TYPE}, Groups._ID + "=" + groupId, null, null,
+                    Groups.ACCOUNT_TYPE}, Groups._ID + "=?", mSelectionArgs1, null,
                     null, null);
             String accountName;
             String accountType;
@@ -2983,7 +3148,7 @@
                     accountType = c.getString(1);
                     if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
                         Account account = new Account(accountName, accountType);
-                    ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
+                        ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
                                 new Bundle());
                         break;
                     }
@@ -3032,12 +3197,16 @@
         final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
                 && values.getAsInteger(RawContacts.DELETED) == 0);
         int previousDeleted = 0;
+        String accountType = null;
+        String accountName = null;
         if (requestUndoDelete) {
             Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
                     null, null, null, null);
             try {
                 if (cursor.moveToFirst()) {
                     previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
+                    accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
+                    accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
                 }
             } finally {
                 cursor.close();
@@ -3055,7 +3224,7 @@
             }
             if (requestUndoDelete && previousDeleted == 1) {
                 // undo delete, needs aggregation again.
-                mInsertedRawContacts.add(rawContactId);
+                mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType));
             }
         }
         return count;
@@ -3164,7 +3333,8 @@
             mValues.put(RawContacts.DIRTY, 1);
         }
 
-        mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=" + contactId, null);
+        mSelectionArgs1[0] = String.valueOf(contactId);
+        mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
 
         // Copy changeable values to prevent automatically managed fields from
         // being explicitly updated by clients.
@@ -3180,7 +3350,7 @@
         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
                 values, Contacts.STARRED);
 
-        return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=" + contactId, null);
+        return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1);
     }
 
     public void updateContactLastContactedTime(long contactId, long lastTimeContacted) {
@@ -3204,9 +3374,11 @@
         }
 
         if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
+            mSelectionArgs2[0] = String.valueOf(rawContactId1);
+            mSelectionArgs2[1] = String.valueOf(rawContactId2);
             db.delete(Tables.AGGREGATION_EXCEPTIONS,
-                    AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND "
-                    + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null);
+                    AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
+                    + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
         } else {
             ContentValues exceptionValues = new ContentValues(3);
             exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
@@ -3216,6 +3388,7 @@
                     exceptionValues);
         }
 
+        mContactAggregator.invalidateAggregationExceptionCache();
         mContactAggregator.markForAggregation(rawContactId1);
         mContactAggregator.markForAggregation(rawContactId2);
 
@@ -3387,7 +3560,8 @@
             case CONTACTS_ID: {
                 long contactId = ContentUris.parseId(uri);
                 setTablesAndProjectionMapForContacts(qb, uri, projection);
-                qb.appendWhere(Contacts._ID + "=" + contactId);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+                qb.appendWhere(Contacts._ID + "=?");
                 break;
             }
 
@@ -3403,10 +3577,17 @@
                     long contactId = Long.parseLong(pathSegments.get(3));
                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
                     setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
-                    lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " +
-                            Contacts.LOOKUP_KEY + "=");
-                    lookupQb.appendWhereEscapeString(lookupKey);
-                    Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder,
+                    String[] args;
+                    if (selectionArgs == null) {
+                        args = new String[2];
+                    } else {
+                        args = new String[selectionArgs.length + 2];
+                        System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
+                    }
+                    args[0] = String.valueOf(contactId);
+                    args[1] = lookupKey;
+                    lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
+                    Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
                             groupBy, limit);
                     if (c.getCount() != 0) {
                         return c;
@@ -3416,7 +3597,9 @@
                 }
 
                 setTablesAndProjectionMapForContacts(qb, uri, projection);
-                qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
+                selectionArgs = insertSelectionArg(selectionArgs,
+                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
+                qb.appendWhere(Contacts._ID + "=?");
                 break;
             }
 
@@ -3425,7 +3608,9 @@
                 final String lookupKey = uri.getPathSegments().get(2);
                 qb.setTables(mDbHelper.getContactView(true /* require restricted */));
                 qb.setProjectionMap(sContactsVCardProjectionMap);
-                qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
+                selectionArgs = insertSelectionArg(selectionArgs,
+                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
+                qb.appendWhere(Contacts._ID + "=?");
                 break;
             }
 
@@ -3505,14 +3690,16 @@
             case CONTACTS_DATA: {
                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
-                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
                 break;
             }
 
             case CONTACTS_PHOTO: {
                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
-                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
                 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
                 break;
             }
@@ -3525,8 +3712,9 @@
 
             case PHONES_ID: {
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
+                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
-                qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
+                qb.appendWhere(" AND " + Data._ID + "=?");
                 break;
             }
 
@@ -3536,7 +3724,7 @@
                 if (uri.getPathSegments().size() > 2) {
                     String filterParam = uri.getLastPathSegment();
                     StringBuilder sb = new StringBuilder();
-                    sb.append("(");
+                    sb.append(" AND (");
 
                     boolean orNeeded = false;
                     String normalizedName = NameNormalizer.normalize(filterParam);
@@ -3560,7 +3748,7 @@
                         sb.append("')");
                     }
                     sb.append(")");
-                    qb.appendWhere(" AND " + sb);
+                    qb.appendWhere(sb);
                 }
                 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
                 if (sortOrder == null) {
@@ -3577,8 +3765,9 @@
 
             case EMAILS_ID: {
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
-                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
-                qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
+                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"
+                        + " AND " + Data._ID + "=?");
                 break;
             }
 
@@ -3586,8 +3775,8 @@
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
                 if (uri.getPathSegments().size() > 2) {
-                    qb.appendWhere(" AND " + Email.DATA + "=");
-                    qb.appendWhereEscapeString(uri.getLastPathSegment());
+                    selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+                    qb.appendWhere(" AND " + Email.DATA + "=?");
                 }
                 break;
             }
@@ -3653,9 +3842,10 @@
 
             case POSTALS_ID: {
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
+                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
                         + StructuredPostal.CONTENT_ITEM_TYPE + "'");
-                qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
+                qb.appendWhere(" AND " + Data._ID + "=?");
                 break;
             }
 
@@ -3667,14 +3857,16 @@
             case RAW_CONTACTS_ID: {
                 long rawContactId = ContentUris.parseId(uri);
                 setTablesAndProjectionMapForRawContacts(qb, uri);
-                qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+                qb.appendWhere(" AND " + RawContacts._ID + "=?");
                 break;
             }
 
             case RAW_CONTACTS_DATA: {
                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
-                qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=" + rawContactId);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+                qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
                 break;
             }
 
@@ -3685,7 +3877,8 @@
 
             case DATA_ID: {
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
-                qb.appendWhere(" AND " + Data._ID + "=" + ContentUris.parseId(uri));
+                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+                qb.appendWhere(" AND " + Data._ID + "=?");
                 break;
             }
 
@@ -3715,10 +3908,10 @@
             }
 
             case GROUPS_ID: {
-                long groupId = ContentUris.parseId(uri);
                 qb.setTables(mDbHelper.getGroupView());
                 qb.setProjectionMap(sGroupsProjectionMap);
-                qb.appendWhere(Groups._ID + "=" + groupId);
+                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+                qb.appendWhere(Groups._ID + "=?");
                 break;
             }
 
@@ -3783,7 +3976,8 @@
 
             case STATUS_UPDATES_ID: {
                 setTableAndProjectionMapForStatusUpdates(qb, projection);
-                qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri));
+                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+                qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
                 break;
             }
 
@@ -3828,7 +4022,8 @@
             case RAW_CONTACT_ENTITY_ID: {
                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
                 setTablesAndProjectionMapForRawContactsEntities(qb, uri);
-                qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+                qb.appendWhere(" AND " + RawContacts._ID + "=?");
                 break;
             }
 
@@ -4043,7 +4238,7 @@
             String[] projection) {
         StringBuilder sb = new StringBuilder();
         boolean excludeRestrictedData = false;
-        String requestingPackage = uri.getQueryParameter(
+        String requestingPackage = getQueryParameter(uri,
                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
         if (requestingPackage != null) {
             excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
@@ -4072,7 +4267,7 @@
     private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
         StringBuilder sb = new StringBuilder();
         boolean excludeRestrictedData = false;
-        String requestingPackage = uri.getQueryParameter(
+        String requestingPackage = getQueryParameter(uri,
                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
         if (requestingPackage != null) {
             excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
@@ -4088,7 +4283,7 @@
         boolean excludeRestrictedData = readBooleanQueryParameter(uri,
                 Data.FOR_EXPORT_ONLY, false);
 
-        String requestingPackage = uri.getQueryParameter(
+        String requestingPackage = getQueryParameter(uri,
                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
         if (requestingPackage != null) {
             excludeRestrictedData = excludeRestrictedData
@@ -4106,7 +4301,7 @@
         boolean excludeRestrictedData = readBooleanQueryParameter(uri,
                 Data.FOR_EXPORT_ONLY, false);
 
-        String requestingPackage = uri.getQueryParameter(
+        String requestingPackage = getQueryParameter(uri,
                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
         if (requestingPackage != null) {
             excludeRestrictedData = excludeRestrictedData
@@ -4187,8 +4382,8 @@
     }
 
     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
-        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
-        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
+        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
         if (!TextUtils.isEmpty(accountName)) {
             qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
@@ -4200,8 +4395,8 @@
     }
 
     private String appendAccountToSelection(Uri uri, String selection) {
-        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
-        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
+        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
         if (!TextUtils.isEmpty(accountName)) {
             StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
@@ -4224,8 +4419,8 @@
      * @return A string containing a non-negative integer, or <code>null</code> if
      *         the parameter is not set, or is set to an invalid value.
      */
-    private String getLimit(Uri url) {
-        String limitParam = url.getQueryParameter("limit");
+    private String getLimit(Uri uri) {
+        String limitParam = getQueryParameter(uri, "limit");
         if (limitParam == null) {
             return null;
         }
@@ -4277,7 +4472,7 @@
         if (mDbHelper.hasAccessToRestrictedData()) {
             return "1";
         } else {
-            return RawContacts.IS_RESTRICTED + "=0";
+            return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0";
         }
     }
 
@@ -4299,14 +4494,13 @@
                     throw new FileNotFoundException("Mode " + mode + " not supported.");
                 }
 
-                long contactId = Long.parseLong(uri.getPathSegments().get(1));
-
                 String sql =
                         "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
                         " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID
-                                + " AND " + RawContacts.CONTACT_ID + "=" + contactId;
+                                + " AND " + RawContacts.CONTACT_ID + "=?";
                 SQLiteDatabase db = mDbHelper.getReadableDatabase();
-                return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null);
+                return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql,
+                        new String[]{uri.getPathSegments().get(1)});
             }
 
             case CONTACTS_AS_VCARD: {
@@ -4376,17 +4570,6 @@
         composer.terminate();
     }
 
-
-    private static Account readAccountFromQueryParams(Uri uri) {
-        final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
-        final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
-        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
-            return null;
-        }
-        return new Account(name, type);
-    }
-
-
     /**
      * An implementation of EntityIterator that joins the contacts and data tables
      * and consumes all the data rows for a contact in order to build the Entity for a contact.
@@ -4658,8 +4841,8 @@
             if (groupIdString != null) {
                 qb.appendWhere(Groups._ID + "=" + groupIdString);
             }
-            final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
-            final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
+            final String accountName = getQueryParameter(uri, Groups.ACCOUNT_NAME);
+            final String accountType = getQueryParameter(uri, Groups.ACCOUNT_TYPE);
             if (!TextUtils.isEmpty(accountName)) {
                 qb.appendWhere(Groups.ACCOUNT_NAME + "="
                         + DatabaseUtils.sqlEscapeString(accountName) + " AND "
@@ -4826,8 +5009,7 @@
      * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
      */
     private void setRawContactDirty(long rawContactId) {
-        mRawContactDirtyUpdate.bindLong(1, rawContactId);
-        mRawContactDirtyUpdate.execute();
+        mDirtyRawContacts.add(rawContactId);
     }
 
     /*
@@ -4904,10 +5086,57 @@
         mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name);
     }
 
+    private interface NicknameLookupPreloadQuery {
+        String TABLE = Tables.NICKNAME_LOOKUP;
+
+        String[] COLUMNS = new String[] {
+            NicknameLookupColumns.NAME
+        };
+
+        int NAME = 0;
+    }
+
+    /**
+     * Read all known common nicknames from the database and populate a Bloom
+     * filter using the corresponding hash codes. The idea is to eliminate most
+     * of unnecessary database lookups for nicknames. Given a name, we will take
+     * its hash code and see if it is set in the Bloom filter. If not, we will know
+     * that the name is not in the database. If it is, we still need to run a
+     * query.
+     * <p>
+     * Given the size of the filter and the expected size of the nickname table,
+     * we should expect the combination of the Bloom filter and cache will
+     * prevent around 98-99% of unnecessary queries from running.
+     */
+    private void preloadNicknameBloomFilter() {
+        mNicknameBloomFilter = new BitSet(NICKNAME_BLOOM_FILTER_SIZE + 1);
+        SQLiteDatabase db = mDbHelper.getReadableDatabase();
+        Cursor cursor = db.query(NicknameLookupPreloadQuery.TABLE,
+                NicknameLookupPreloadQuery.COLUMNS,
+                null, null, null, null, null);
+        try {
+            int count = cursor.getCount();
+            for (int i = 0; i < count; i++) {
+                cursor.moveToNext();
+                String normalizedName = cursor.getString(NicknameLookupPreloadQuery.NAME);
+                int hashCode = normalizedName.hashCode();
+                mNicknameBloomFilter.set(hashCode & NICKNAME_BLOOM_FILTER_SIZE);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+
     /**
      * Returns nickname cluster IDs or null. Maintains cache.
      */
     protected String[] getCommonNicknameClusters(String normalizedName) {
+        int hashCode = normalizedName.hashCode();
+        if (!mNicknameBloomFilter.get(hashCode & NICKNAME_BLOOM_FILTER_SIZE)) {
+            return null;
+        }
+
         SoftReference<String[]> ref;
         String[] clusters = null;
         synchronized (mNicknameClusterCache) {
@@ -4930,6 +5159,16 @@
         return clusters;
     }
 
+    private interface NicknameLookupQuery {
+        String TABLE = Tables.NICKNAME_LOOKUP;
+
+        String[] COLUMNS = new String[] {
+            NicknameLookupColumns.CLUSTER
+        };
+
+        int CLUSTER = 0;
+    }
+
     protected String[] loadNicknameClusters(String normalizedName) {
         SQLiteDatabase db = mDbHelper.getReadableDatabase();
         String[] clusters = null;
@@ -5019,7 +5258,7 @@
     private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
             String limit, boolean allowEmailMatch) {
         sb.append("(" +
-                "SELECT DISTINCT " + NameLookupColumns.RAW_CONTACT_ID +
+                "SELECT " + NameLookupColumns.RAW_CONTACT_ID +
                 " FROM " + Tables.NAME_LOOKUP +
                 " WHERE " + NameLookupColumns.NORMALIZED_NAME +
                 " GLOB '");
@@ -5091,7 +5330,75 @@
         } catch (RemoteException e) {
             Log.e(TAG, "Could not acquire sync adapter types");
         }
-
         return false;
     }
+
+    /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
+            boolean defaultValue) {
+
+        // Manually parse the query, which is much faster than calling uri.getQueryParameter
+        String query = uri.getEncodedQuery();
+        if (query == null) {
+            return defaultValue;
+        }
+
+        int index = query.indexOf(parameter);
+        if (index == -1) {
+            return defaultValue;
+        }
+
+        index += parameter.length();
+
+        return !matchQueryParameter(query, index, "=0", false)
+                && !matchQueryParameter(query, index, "=false", true);
+    }
+
+    private static boolean matchQueryParameter(String query, int index, String value,
+            boolean ignoreCase) {
+        int length = value.length();
+        return query.regionMatches(ignoreCase, index, value, 0, length)
+                && (query.length() == index + length || query.charAt(index + length) == '&');
+    }
+
+    /**
+     * A fast re-implementation of {@link Uri#getQueryParameter}
+     */
+    /* package */ static String getQueryParameter(Uri uri, String parameter) {
+        String query = uri.getEncodedQuery();
+        if (query == null) {
+            return null;
+        }
+
+        int queryLength = query.length();
+        int parameterLength = parameter.length();
+
+        String value;
+        int index = 0;
+        while (true) {
+            index = query.indexOf(parameter, index);
+            if (index == -1) {
+                return null;
+            }
+
+            index += parameterLength;
+
+            if (queryLength == index) {
+                return null;
+            }
+
+            if (query.charAt(index) == '=') {
+                index++;
+                break;
+            }
+        }
+
+        int ampIndex = query.indexOf('&', index);
+        if (ampIndex == -1) {
+            value = query.substring(index);
+        } else {
+            value = query.substring(index, ampIndex);
+        }
+
+        return Uri.decode(value);
+    }
 }
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index e939ef7..af2bd9e 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -20,7 +20,7 @@
 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.MimetypesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 
 import android.app.SearchManager;
@@ -32,11 +32,9 @@
 import android.provider.Contacts.Intents;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Presence;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
@@ -74,20 +72,11 @@
     };
 
     private interface SearchSuggestionQuery {
-        public static final String JOIN_RAW_CONTACTS =
-                " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) ";
-
-        public static final String JOIN_CONTACTS =
-                " JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
-        public static final String JOIN_MIMETYPES =
-                " JOIN mimetypes ON (data.mimetype_id = mimetypes._id AND mimetypes.mimetype IN ('"
-                + StructuredName.CONTENT_ITEM_TYPE + "','" + Email.CONTENT_ITEM_TYPE + "','"
-                + Phone.CONTENT_ITEM_TYPE + "','" + Organization.CONTENT_ITEM_TYPE + "','"
-                + GroupMembership.CONTENT_ITEM_TYPE + "')) ";
-
-        public static final String TABLE = "data " + JOIN_RAW_CONTACTS + JOIN_MIMETYPES
-                + JOIN_CONTACTS;
+        public static final String TABLE = "data "
+                + " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+                + " JOIN contacts ON (raw_contacts.contact_id = contacts._id)"
+                + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON ("
+                +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
 
         public static final String PRESENCE_SQL =
                 "(SELECT " + StatusUpdates.PRESENCE_STATUS +
@@ -97,14 +86,13 @@
 
         public static final String[] COLUMNS = {
             ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID,
-            ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME,
+            "name_raw_contact." + RawContactsColumns.DISPLAY_NAME
+                    + " AS " + Contacts.DISPLAY_NAME,
             PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE,
             DataColumns.CONCRETE_ID + " AS data_id",
-            MimetypesColumns.MIMETYPE,
+            DataColumns.MIMETYPE_ID,
             Data.IS_SUPER_PRIMARY,
-            Organization.COMPANY,
-            Email.DATA,
-            Phone.NUMBER,
+            Data.DATA1,
             Contacts.PHOTO_ID,
         };
 
@@ -112,12 +100,12 @@
         public static final int DISPLAY_NAME = 1;
         public static final int PRESENCE_STATUS = 2;
         public static final int DATA_ID = 3;
-        public static final int MIMETYPE = 4;
+        public static final int MIMETYPE_ID = 4;
         public static final int IS_SUPER_PRIMARY = 5;
         public static final int ORGANIZATION = 6;
-        public static final int EMAIL = 7;
-        public static final int PHONE = 8;
-        public static final int PHOTO_ID = 9;
+        public static final int EMAIL = 6;
+        public static final int PHONE = 6;
+        public static final int PHOTO_ID = 7;
     }
 
     private static class SearchSuggestion {
@@ -225,9 +213,34 @@
     }
 
     private final ContactsProvider2 mContactsProvider;
+    private boolean mMimeTypeIdsLoaded;
+    private long mMimeTypeIdEmail;
+    private long mMimeTypeIdStructuredName;
+    private long mMimeTypeIdOrganization;
+    private long mMimeTypeIdPhone;
 
+    @SuppressWarnings("all")
     public GlobalSearchSupport(ContactsProvider2 contactsProvider) {
         mContactsProvider = contactsProvider;
+
+        // To ensure the data column position. This is dead code if properly configured.
+        if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
+                || Email.DATA != Data.DATA1) {
+            throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
+                    + " data is not in DATA1 column");
+        }
+    }
+
+    private void ensureMimetypeIdsLoaded() {
+        if (!mMimeTypeIdsLoaded) {
+            ContactsDatabaseHelper dbHelper = (ContactsDatabaseHelper)mContactsProvider
+                    .getDatabaseHelper();
+            mMimeTypeIdStructuredName = dbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
+            mMimeTypeIdOrganization = dbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
+            mMimeTypeIdPhone = dbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+            mMimeTypeIdEmail = dbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
+            mMimeTypeIdsLoaded = true;
+        }
     }
 
     public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String limit) {
@@ -243,11 +256,14 @@
         }
     }
 
-    public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, long contactId, String[] projection) {
+    public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, long contactId,
+            String[] projection) {
+        ensureMimetypeIdsLoaded();
         StringBuilder sb = new StringBuilder();
         sb.append(mContactsProvider.getContactsRestrictions());
-        sb.append(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
-        return buildCursorForSearchSuggestions(db, sb.toString(), projection);
+        appendMimeTypeFilter(sb);
+        sb.append(" AND " + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + contactId);
+        return buildCursorForSearchSuggestions(db, sb.toString(), projection, null);
     }
 
     private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) {
@@ -296,22 +312,46 @@
 
     private Cursor buildCursorForSearchSuggestionsBasedOnName(SQLiteDatabase db,
             String searchClause, String limit) {
-
+        ensureMimetypeIdsLoaded();
         StringBuilder sb = new StringBuilder();
         sb.append(mContactsProvider.getContactsRestrictions());
+        appendMimeTypeFilter(sb);
         sb.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN ");
         mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause, limit);
-        sb.append(" AND " + Contacts.IN_VISIBLE_GROUP + "=1");
 
-        return buildCursorForSearchSuggestions(db, sb.toString(), null);
+        /*
+         *  Prepending "+" to the IN_VISIBLE_GROUP column disables the index on the
+         *  that column.  The logic is this:  let's say we have 10,000 contacts
+         *  of which 500 are visible.  The first letter we type narrows this down
+         *  to 10,000/26 = 384, which is already less than 500 that we would get
+         *  from the IN_VISIBLE_GROUP index.  Typing the second letter will narrow
+         *  the search down to 10,000/26/26 = 14 contacts. And a lot of people
+         *  will have more that 5% of their contacts visible, while the alphabet
+         *  will always have 26 letters.
+         */
+        sb.append(" AND " + "+" + Contacts.IN_VISIBLE_GROUP + "=1");
+        String selection = sb.toString();
+
+        return buildCursorForSearchSuggestions(db, selection, null, limit);
     }
 
-    private Cursor buildCursorForSearchSuggestions(SQLiteDatabase db, String selection,
-            String[] projection) {
+    private void appendMimeTypeFilter(StringBuilder sb) {
+
+        /*
+         * The "+" syntax prevents the mime type index from being used - we just want
+         * to reduce the size of the result set, not actually search by mime types.
+         */
+        sb.append(" AND " + "+" + DataColumns.MIMETYPE_ID + " IN (" + mMimeTypeIdEmail + "," +
+                mMimeTypeIdOrganization + "," + mMimeTypeIdPhone + "," +
+                mMimeTypeIdStructuredName + ")");
+    }
+
+    private Cursor buildCursorForSearchSuggestions(SQLiteDatabase db,
+            String selection, String[] projection, String limit) {
         ArrayList<SearchSuggestion> suggestionList = new ArrayList<SearchSuggestion>();
         HashMap<Long, SearchSuggestion> suggestionMap = new HashMap<Long, SearchSuggestion>();
-        Cursor c = db.query(true, SearchSuggestionQuery.TABLE,
-                SearchSuggestionQuery.COLUMNS, selection, null, null, null, null, null);
+        Cursor c = db.query(false, SearchSuggestionQuery.TABLE,
+                SearchSuggestionQuery.COLUMNS, selection, null, null, null, null, limit);
         try {
             while (c.moveToNext()) {
 
@@ -330,18 +370,18 @@
                     suggestion.presence = c.getInt(SearchSuggestionQuery.PRESENCE_STATUS);
                 }
 
-                String mimetype = c.getString(SearchSuggestionQuery.MIMETYPE);
-                if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                long mimetype = c.getLong(SearchSuggestionQuery.MIMETYPE_ID);
+                if (mimetype == mMimeTypeIdStructuredName) {
                     suggestion.titleIsName = true;
-                } else if (Organization.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                } else if (mimetype == mMimeTypeIdOrganization) {
                     if (isSuperPrimary || suggestion.organization == null) {
                         suggestion.organization = c.getString(SearchSuggestionQuery.ORGANIZATION);
                     }
-                } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                } else if (mimetype == mMimeTypeIdEmail) {
                     if (isSuperPrimary || suggestion.email == null) {
                         suggestion.email = c.getString(SearchSuggestionQuery.EMAIL);
                     }
-                } else if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                } else if (mimetype == mMimeTypeIdPhone) {
                     if (isSuperPrimary || suggestion.phoneNumber == null) {
                         suggestion.phoneNumber = c.getString(SearchSuggestionQuery.PHONE);
                     }
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index 65120f9..703230d 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -1531,7 +1531,7 @@
         switch (match) {
             case PEOPLE:
             case PEOPLE_ID:
-                count = mContactsProvider.deleteRawContact(id, false);
+                count = mContactsProvider.deleteRawContact(id, mDbHelper.getContactId(id), false);
                 break;
 
             case PEOPLE_PHOTO:
diff --git a/src/com/android/providers/contacts/LegacyContactImporter.java b/src/com/android/providers/contacts/LegacyContactImporter.java
index 9c17e4a..2f63637 100644
--- a/src/com/android/providers/contacts/LegacyContactImporter.java
+++ b/src/com/android/providers/contacts/LegacyContactImporter.java
@@ -383,7 +383,7 @@
                 Contacts.SEND_TO_VOICEMAIL + "," +
                 Contacts.STARRED + "," +
                 Contacts.TIMES_CONTACTED + "," +
-                Contacts.DISPLAY_NAME +
+                Contacts.NAME_RAW_CONTACT_ID +
          ") VALUES (?,?,?,?,?,?,?)";
 
         int ID = 1;
@@ -392,7 +392,7 @@
         int SEND_TO_VOICEMAIL = 4;
         int STARRED = 5;
         int TIMES_CONTACTED = 6;
-        int DISPLAY_NAME = 7;
+        int NAME_RAW_CONTACT_ID = 7;
     }
 
     private interface StructuredNameInsert {
@@ -518,8 +518,7 @@
                 c.getLong(PeopleQuery.STARRED));
         insert.bindLong(ContactsInsert.TIMES_CONTACTED,
                 c.getLong(PeopleQuery.TIMES_CONTACTED));
-        bindString(insert, ContactsInsert.DISPLAY_NAME,
-                c.getString(PeopleQuery.NAME));
+        insert.bindLong(ContactsInsert.NAME_RAW_CONTACT_ID, id);
 
         insert(insert);
     }
@@ -814,12 +813,14 @@
         String INSERT_SQL = "INSERT INTO " + Tables.PHONE_LOOKUP + "(" +
                 PhoneLookupColumns.RAW_CONTACT_ID + "," +
                 PhoneLookupColumns.DATA_ID + "," +
-                PhoneLookupColumns.NORMALIZED_NUMBER +
-         ") VALUES (?,?,?)";
+                PhoneLookupColumns.NORMALIZED_NUMBER + "," +
+                PhoneLookupColumns.MIN_MATCH +
+         ") VALUES (?,?,?,?)";
 
         int RAW_CONTACT_ID = 1;
         int DATA_ID = 2;
         int NORMALIZED_NUMBER = 3;
+        int MIN_MATCH = 4;
     }
 
     private interface HasPhoneNumberUpdate {
@@ -868,6 +869,8 @@
             phoneLookupInsert.bindLong(PhoneLookupInsert.RAW_CONTACT_ID, id);
             phoneLookupInsert.bindLong(PhoneLookupInsert.DATA_ID, dataId);
             phoneLookupInsert.bindString(PhoneLookupInsert.NORMALIZED_NUMBER, normalizedNumber);
+            phoneLookupInsert.bindString(PhoneLookupInsert.MIN_MATCH,
+                    PhoneNumberUtils.toCallerIDMinMatch(number));
             insert(phoneLookupInsert);
 
             if (lastUpdatedContact != id) {
diff --git a/src/com/android/providers/contacts/SQLiteContentProvider.java b/src/com/android/providers/contacts/SQLiteContentProvider.java
index 4d1ec90..56903bf 100644
--- a/src/com/android/providers/contacts/SQLiteContentProvider.java
+++ b/src/com/android/providers/contacts/SQLiteContentProvider.java
@@ -44,6 +44,11 @@
     private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
     private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
 
+    /**
+     * Maximum number of operations allowed in a batch between yield points.
+     */
+    private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
+
     @Override
     public boolean onCreate() {
         Context context = getContext();
@@ -186,6 +191,8 @@
     @Override
     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
             throws OperationApplicationException {
+        int ypCount = 0;
+        int opCount = 0;
         mDb = mOpenHelper.getWritableDatabase();
         mDb.beginTransactionWithListener(this);
         try {
@@ -193,9 +200,18 @@
             final int numOperations = operations.size();
             final ContentProviderResult[] results = new ContentProviderResult[numOperations];
             for (int i = 0; i < numOperations; i++) {
+                if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
+                    throw new OperationApplicationException(
+                            "Too many content provider operations between yield points. "
+                                    + "The maximum number of operations per yield point is "
+                                    + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
+                }
                 final ContentProviderOperation operation = operations.get(i);
                 if (i > 0 && operation.isYieldAllowed()) {
-                    mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY);
+                    opCount = 0;
+                    if (mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
+                        ypCount++;
+                    }
                 }
                 results[i] = operation.apply(this, results, i);
             }
diff --git a/src/com/android/providers/contacts/SocialProvider.java b/src/com/android/providers/contacts/SocialProvider.java
index 349e1fc..8201d90 100644
--- a/src/com/android/providers/contacts/SocialProvider.java
+++ b/src/com/android/providers/contacts/SocialProvider.java
@@ -84,7 +84,8 @@
 
         // Contacts projection map
         columns = new HashMap<String, String>();
-        columns.put(Contacts.DISPLAY_NAME, ContactsColumns.CONCRETE_DISPLAY_NAME + " AS "
+        // TODO: fix display name reference (in fact, use the contacts view instead of the table)
+        columns.put(Contacts.DISPLAY_NAME, "contact." + Contacts.DISPLAY_NAME + " AS "
                 + Contacts.DISPLAY_NAME);
         sContactsProjectionMap = columns;
 
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index bcc7f6e..2117829 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -391,7 +391,6 @@
     }
 
     protected void assertAggregated(long rawContactId1, long rawContactId2) {
-        forceAggregation();
         long contactId1 = queryContactId(rawContactId1);
         long contactId2 = queryContactId(rawContactId2);
         assertTrue(contactId1 == contactId2);
@@ -399,8 +398,6 @@
 
     protected void assertAggregated(long rawContactId1, long rawContactId2,
             String expectedDisplayName) {
-        forceAggregation();
-
         long contactId1 = queryContactId(rawContactId1);
         long contactId2 = queryContactId(rawContactId2);
         assertTrue(contactId1 == contactId2);
@@ -410,8 +407,6 @@
     }
 
     protected void assertNotAggregated(long rawContactId1, long rawContactId2) {
-        forceAggregation();
-
         long contactId1 = queryContactId(rawContactId1);
         long contactId2 = queryContactId(rawContactId2);
         assertTrue(contactId1 != contactId2);
@@ -816,10 +811,6 @@
         c.close();
     }
 
-    protected void forceAggregation() {
-        ((SynchronousContactsProvider2) mActor.provider).aggregate();
-    }
-
     protected void assertNetworkNotified(boolean expected) {
         assertEquals(expected, ((SynchronousContactsProvider2)mActor.provider).isNetworkNotified());
     }
diff --git a/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java b/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
deleted file mode 100644
index 6c15e0e..0000000
--- a/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2009 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;
-
-import android.test.suitebuilder.annotation.SmallTest;
-
-import junit.framework.TestCase;
-
-/**
- * Tests from {@link ContactAggregationScheduler}.
- *
- * Run the test like this:
- * <code>
- * adb shell am instrument -e class com.android.providers.contacts.ContactAggregationSchedulerTest \
- *         -w com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
- * </code>
- */
-@SmallTest
-public class ContactAggregationSchedulerTest extends TestCase {
-
-    private TestContactAggregationScheduler mScheduler;
-
-    @Override
-    protected void setUp() throws Exception {
-        mScheduler = new TestContactAggregationScheduler();
-    }
-
-    public void testScheduleInitial() {
-        mScheduler.schedule();
-        assertEquals(1, mScheduler.mRunNow);
-        assertEquals(0, mScheduler.mRunDelayed);
-    }
-
-    public void testScheduleTwiceRapidly() {
-        mScheduler.schedule();
-
-        mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
-        mScheduler.schedule();
-        assertEquals(1, mScheduler.mRunNow);
-        assertEquals(1, mScheduler.mRunDelayed);
-    }
-
-    public void testScheduleThriceRapidly() {
-        mScheduler.schedule();
-
-        mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
-        mScheduler.schedule();
-
-        mScheduler.mTime += ContactAggregationScheduler.AGGREGATION_DELAY / 2;
-        mScheduler.schedule();
-
-        assertEquals(1, mScheduler.mRunNow);
-        assertEquals(2, mScheduler.mRunDelayed);
-    }
-
-    public void testScheduleThriceExceedingMaxDelay() {
-        mScheduler.schedule();
-
-        mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
-        mScheduler.schedule();
-
-        mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
-        mScheduler.schedule();
-        assertEquals(1, mScheduler.mRunDelayed);
-    }
-
-    public void testScheduleWhileRunning() {
-        mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
-            boolean mInterruptCalled;
-
-            public void interrupt() {
-                mInterruptCalled = true;
-            }
-
-            public void run() {
-                mScheduler.schedule();
-                assertTrue(mInterruptCalled);
-            }
-        });
-
-        mScheduler.run();
-        assertEquals(1, mScheduler.mRunDelayed);
-    }
-
-    public void testRepeatedInterruptions() {
-        mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
-            boolean mInterruptCalled;
-
-            public void interrupt() {
-                mInterruptCalled = true;
-            }
-
-            public void run() {
-                mScheduler.schedule();
-                assertTrue(mInterruptCalled);
-            }
-        });
-
-        mScheduler.run();
-        assertEquals(1, mScheduler.mRunDelayed);
-
-        mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
-        mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
-            boolean mInterruptCalled;
-
-            public void interrupt() {
-                mInterruptCalled = true;
-            }
-
-            public void run() {
-                mScheduler.schedule();
-                assertFalse(mInterruptCalled);
-            }
-        });
-
-        mScheduler.run();
-    }
-
-    public void testScheduleWhileRunningExceedingMaxDelay() {
-        mScheduler.schedule();
-
-        mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
-        mScheduler.schedule();
-
-        mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
-
-        mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
-            boolean mInterruptCalled;
-
-            public void interrupt() {
-                mInterruptCalled = true;
-            }
-
-            public void run() {
-                mScheduler.schedule();
-                assertFalse(mInterruptCalled);
-            }
-        });
-
-        mScheduler.run();
-        assertEquals(1, mScheduler.mRunNow);
-        assertEquals(2, mScheduler.mRunDelayed);
-    }
-
-    private static class TestContactAggregationScheduler extends ContactAggregationScheduler {
-
-        long mTime = 1000;
-        int mRunDelayed;
-        int mRunNow;
-
-        @Override
-        public void start() {
-        }
-
-        @Override
-        public void stop() {
-        }
-
-        @Override
-        long currentTime() {
-            return mTime;
-        }
-
-        @Override
-        void runNow() {
-            mRunNow++;
-        }
-
-        @Override
-        void runDelayed() {
-            mRunDelayed++;
-        }
-    }
-}
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
index e4dc629..a71c58b 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
@@ -95,7 +95,10 @@
         if (TRACE) {
             Debug.startMethodTracing("aggregation");
         }
-        provider.aggregate();
+
+        // TODO
+//        provider.aggregate();
+
         if (TRACE) {
             Debug.stopMethodTracing();
         }
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
index 04b8c47..f864641 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
@@ -22,6 +22,7 @@
 import android.net.Uri;
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.test.suitebuilder.annotation.LargeTest;
 
@@ -368,6 +369,20 @@
         assertNotAggregated(rawContactId1, rawContactId2);
     }
 
+    public void testNonAggregationOnOrganization() {
+        ContentValues values = new ContentValues();
+        values.put(Organization.TITLE, "Monsters, Inc");
+        long rawContactId1 = createRawContact();
+        insertOrganization(rawContactId1, values);
+        insertNickname(rawContactId1, "Boo");
+
+        long rawContactId2 = createRawContact();
+        insertOrganization(rawContactId2, values);
+        insertNickname(rawContactId2, "Rendall");   // To force reaggregation
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
     public void testAggregationExceptionKeepIn() {
         long rawContactId1 = createRawContact();
         insertStructuredName(rawContactId1, "Johnk", "Smithk");
@@ -627,8 +642,6 @@
     }
 
     private void assertSuggestions(long contactId, long... suggestions) {
-        forceAggregation();
-
         final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
         Uri uri = Uri.withAppendedPath(aggregateUri,
                 Contacts.AggregationSuggestions.CONTENT_DIRECTORY);
diff --git a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
index 1d57924..390b940 100644
--- a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
+++ b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
@@ -126,8 +126,6 @@
         long rawContactId3 = createRawContactWithName("John", "Doe");
         storeValue(RawContacts.CONTENT_URI, rawContactId3, RawContacts.SOURCE_ID, "3");
 
-        forceAggregation();
-
         String lookupKey = "0i1.0i2.0i3";
 
         long contactId = queryContactId(rawContactId1);
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index afeb377..1c175a4 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -18,8 +18,11 @@
 
 import com.android.internal.util.ArrayUtils;
 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
+import com.google.android.collect.Lists;
 
 import android.accounts.Account;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Entity;
@@ -174,8 +177,6 @@
         long rawContactId2 = createRawContactWithName("Hot", "Tamale");
         insertPhoneNumber(rawContactId2, "1-800-466-4411");
 
-        forceAggregation();
-
         Uri filterUri1 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "tamale");
         ContentValues values = new ContentValues();
         values.put(Contacts.DISPLAY_NAME, "Hot Tamale");
@@ -322,7 +323,6 @@
 
         long rawContactId2 = createRawContactWithName("Hot", "Tamale");
         insertEmail(rawContactId2, "tamale@acme.com");
-        forceAggregation();
 
         Uri filterUri1 = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "tam");
         ContentValues values = new ContentValues();
@@ -679,6 +679,23 @@
         assertStoredValue(uri, Contacts.DISPLAY_NAME, "Dog");
     }
 
+    public void testInsertDataWithContentProviderOperations() throws Exception {
+        ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+                .withValues(new ContentValues())
+                .build();
+        ContentProviderOperation cpo2 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+                .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+                .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+                .withValue(StructuredName.GIVEN_NAME, "John")
+                .withValue(StructuredName.FAMILY_NAME, "Doe")
+                .build();
+        ContentProviderResult[] results =
+                mResolver.applyBatch(ContactsContract.AUTHORITY, Lists.newArrayList(cpo1, cpo2));
+        long contactId = queryContactId(ContentUris.parseId(results[0].uri));
+        Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        assertStoredValue(uri, Contacts.DISPLAY_NAME, "John Doe");
+    }
+
     public void testSendToVoicemailDefault() {
         long rawContactId = createRawContactWithName();
         long contactId = queryContactId(rawContactId);
@@ -894,11 +911,53 @@
         values.put(Contacts.CONTACT_STATUS, "Available");
         assertStoredValuesWithProjection(contactUri, values);
 
-        mResolver.delete(StatusUpdates.CONTENT_URI, StatusUpdates.DATA_ID + "=" + statusId, null);
-        values.putNull(Contacts.CONTACT_PRESENCE);
+        // update status_updates table to set new values for
+        //     status_updates.status
+        //     status_updates.status_ts
+        //     presence
+        long updatedTs = 200;
+        String testUpdate = "test_update";
+        String selection = StatusUpdates.DATA_ID + "=" + statusId;
+        values.clear();
+        values.put(StatusUpdates.STATUS_TIMESTAMP, updatedTs);
+        values.put(StatusUpdates.STATUS, testUpdate);
+        values.put(StatusUpdates.PRESENCE, "presence_test");
+        mResolver.update(StatusUpdates.CONTENT_URI, values,
+                StatusUpdates.DATA_ID + "=" + statusId, null);
+        assertStoredValuesWithProjection(StatusUpdates.CONTENT_URI, values);
 
-        // Latest custom status update stays on the phone
-        values.put(Contacts.CONTACT_STATUS, "Available");
+        // update status_updates table to set new values for columns in status_updates table ONLY
+        // i.e., no rows in presence table are to be updated.
+        updatedTs = 300;
+        testUpdate = "test_update_new";
+        selection = StatusUpdates.DATA_ID + "=" + statusId;
+        values.clear();
+        values.put(StatusUpdates.STATUS_TIMESTAMP, updatedTs);
+        values.put(StatusUpdates.STATUS, testUpdate);
+        mResolver.update(StatusUpdates.CONTENT_URI, values,
+                StatusUpdates.DATA_ID + "=" + statusId, null);
+        // make sure the presence column value is still the old value
+        values.put(StatusUpdates.PRESENCE, "presence_test");
+        assertStoredValuesWithProjection(StatusUpdates.CONTENT_URI, values);
+
+        // update status_updates table to set new values for columns in presence table ONLY
+        // i.e., no rows in status_updates table are to be updated.
+        selection = StatusUpdates.DATA_ID + "=" + statusId;
+        values.clear();
+        values.put(StatusUpdates.PRESENCE, "presence_test_new");
+        mResolver.update(StatusUpdates.CONTENT_URI, values,
+                StatusUpdates.DATA_ID + "=" + statusId, null);
+        // make sure the status_updates table is not updated
+        values.put(StatusUpdates.STATUS_TIMESTAMP, updatedTs);
+        values.put(StatusUpdates.STATUS, testUpdate);
+        assertStoredValuesWithProjection(StatusUpdates.CONTENT_URI, values);
+
+        // effect "delete status_updates" operation and expect the following
+        //   data deleted from status_updates table
+        //   presence set to null
+        mResolver.delete(StatusUpdates.CONTENT_URI, StatusUpdates.DATA_ID + "=" + statusId, null);
+        values.clear();
+        values.putNull(Contacts.CONTACT_PRESENCE);
         assertStoredValuesWithProjection(contactUri, values);
     }
 
@@ -1334,7 +1393,6 @@
     public void testContactDeletion() {
         long rawContactId1 = createRawContactWithName("John", "Doe");
         long rawContactId2 = createRawContactWithName("John", "Doe");
-        forceAggregation();
 
         long contactId = queryContactId(rawContactId1);
 
@@ -1628,6 +1686,42 @@
         c.close();
     }
 
+    public void testReadBooleanQueryParameter() {
+        assertBooleanUriParameter("foo:bar", "bool", true, true);
+        assertBooleanUriParameter("foo:bar", "bool", false, false);
+        assertBooleanUriParameter("foo:bar?bool=0", "bool", true, false);
+        assertBooleanUriParameter("foo:bar?bool=1", "bool", false, true);
+        assertBooleanUriParameter("foo:bar?bool=false", "bool", true, false);
+        assertBooleanUriParameter("foo:bar?bool=true", "bool", false, true);
+        assertBooleanUriParameter("foo:bar?bool=FaLsE", "bool", true, false);
+        assertBooleanUriParameter("foo:bar?bool=false&some=some", "bool", true, false);
+        assertBooleanUriParameter("foo:bar?bool=1&some=some", "bool", false, true);
+        assertBooleanUriParameter("foo:bar?some=bool", "bool", true, true);
+        assertBooleanUriParameter("foo:bar?bool", "bool", true, true);
+    }
+
+    private void assertBooleanUriParameter(String uriString, String parameter,
+            boolean defaultValue, boolean expectedValue) {
+        assertEquals(expectedValue, ContactsProvider2.readBooleanQueryParameter(
+                Uri.parse(uriString), parameter, defaultValue));
+    }
+
+    public void testGetQueryParameter() {
+        assertQueryParameter("foo:bar", "param", null);
+        assertQueryParameter("foo:bar?param", "param", null);
+        assertQueryParameter("foo:bar?param=", "param", "");
+        assertQueryParameter("foo:bar?param=val", "param", "val");
+        assertQueryParameter("foo:bar?param=val&some=some", "param", "val");
+        assertQueryParameter("foo:bar?some=some&param=val", "param", "val");
+        assertQueryParameter("foo:bar?some=some&param=val&else=else", "param", "val");
+        assertQueryParameter("foo:bar?param=john%40doe.com", "param", "john@doe.com");
+    }
+
+    private void assertQueryParameter(String uriString, String parameter, String expectedValue) {
+        assertEquals(expectedValue, ContactsProvider2.getQueryParameter(
+                Uri.parse(uriString), parameter));
+    }
+
     private long createContact(ContentValues values, String firstName, String givenName,
             String phoneNumber, String email, int presenceStatus, int timesContacted, int starred,
             long groupId) {
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
index bdfab7a..3919770 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
@@ -77,8 +77,7 @@
                 targetContext, "perf_imp.");
         targetContextWrapper.makeExistingFilesAndDbsAccessible();
         IsolatedContext providerContext = new IsolatedContext(resolver, targetContextWrapper);
-        TestAggregationScheduler scheduler = new TestAggregationScheduler();
-        SynchronousContactsProvider2 provider = new SynchronousContactsProvider2(scheduler);
+        SynchronousContactsProvider2 provider = new SynchronousContactsProvider2();
         provider.setDataWipeEnabled(false);
         provider.attachInfo(providerContext, null);
         resolver.addProvider(ContactsContract.AUTHORITY, provider);
@@ -120,28 +119,4 @@
             return mDbHelper;
         }
     }
-
-    private static class TestAggregationScheduler extends ContactAggregationScheduler {
-
-        @Override
-        public void start() {
-        }
-
-        @Override
-        public void stop() {
-        }
-
-        @Override
-        long currentTime() {
-            return 0;
-        }
-
-        @Override
-        void runDelayed() {
-        }
-
-        void trigger() {
-            run();
-        }
-    }
 }
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
index af15b3d..08c7582 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
@@ -184,8 +184,6 @@
                 Calls.CACHED_NUMBER_TYPE,
         });
 
-        // Performing an aggregation pass should not change anything.
-        provider.scheduleContactAggregation();
         assertQueryResults("expected_contacts.txt", Contacts.CONTENT_URI, new String[]{
                 Contacts._ID,
                 Contacts.DISPLAY_NAME,
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index f5a3572..12b809d 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -31,14 +31,6 @@
     private Account mAccount;
     private boolean mNetworkNotified;
 
-    public SynchronousContactsProvider2() {
-        this(new SynchronousAggregationScheduler());
-    }
-
-    public SynchronousContactsProvider2(ContactAggregationScheduler scheduler) {
-        super(scheduler);
-    }
-
     @Override
     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
         if (mDbHelper == null) {
@@ -135,31 +127,4 @@
         // We have an explicit test for data conversion - no need to do it every time
         return false;
     }
-
-    public void aggregate() {
-        ContactAggregationScheduler scheduler = getContactAggregationScheduler();
-        scheduler.run();
-    }
-
-    private static class SynchronousAggregationScheduler extends ContactAggregationScheduler {
-
-        @Override
-        public void start() {
-        }
-
-        @Override
-        public void stop() {
-        }
-
-        @Override
-        long currentTime() {
-            return 0;
-        }
-
-        @Override
-        void runDelayed() {
-//            super.run();
-        }
-
-    }
 }