am b3c80626: am a72b3934: reconcile main tree with open-source eclair

Merge commit 'b3c80626606fd3cad8df7e34d48832bddaead8cd'

* commit 'b3c80626606fd3cad8df7e34d48832bddaead8cd':
  android-2.1_r1 snapshot
diff --git a/Android.mk b/Android.mk
index e963f99..79ff490 100644
--- a/Android.mk
+++ b/Android.mk
@@ -1,7 +1,7 @@
 LOCAL_PATH:= $(call my-dir)
 include $(CLEAR_VARS)
 
-LOCAL_MODULE_TAGS := user
+LOCAL_MODULE_TAGS := optional
 
 # Only compile source java files in this apk.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
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..e3a5142 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -20,10 +20,9 @@
 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.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 +35,15 @@
 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.DisplayNameSources;
 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();
+    private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
+
     /**
      * Captures a potential match for a given name. The matching algorithm
      * constructs a bunch of NameMatchCandidate objects for various potential matches
@@ -233,34 +169,33 @@
         }
     }
 
-    private class AggregationNameLookupBuilder extends NameLookupBuilder {
+    /**
+     * A convenience class used in the algorithm that figures out which of available
+     * display names to use for an aggregate contact.
+     */
+    private static class DisplayNameCandidate {
+        long rawContactId;
+        String displayName;
+        int displayNameSource;
+        boolean verified;
 
-        private final MatchCandidateList mCandidates;
-
-        public AggregationNameLookupBuilder(NameSplitter splitter, MatchCandidateList candidates) {
-            super(splitter);
-            mCandidates = candidates;
+        public DisplayNameCandidate() {
+            clear();
         }
 
-        @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);
+        public void clear() {
+            rawContactId = -1;
+            displayName = null;
+            displayNameSource = DisplayNameSources.UNDEFINED;
+            verified = false;
         }
     }
 
     /**
-     * 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 +238,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 +283,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 +308,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 +329,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 +365,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 +397,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 +426,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 +476,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 +502,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 +542,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 +618,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 +682,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 +938,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 +981,39 @@
         }
     }
 
-    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 + ","
+                        + RawContacts.NAME_VERIFIED + ","
+                        + 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;
@@ -1150,27 +1027,71 @@
         int TIMES_CONTACTED = 9;
         int STARRED = 10;
         int IS_RESTRICTED = 11;
-        int DATA_ID = 12;
-        int MIMETYPE_ID = 13;
-        int IS_SUPER_PRIMARY = 14;
+        int NAME_VERIFIED = 12;
+        int DATA_ID = 13;
+        int MIMETYPE_ID = 14;
+        int IS_SUPER_PRIMARY = 15;
+    }
+
+    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;
         boolean foundSuperPrimaryPhoto = false;
         String photoAccount = null;
@@ -1179,25 +1100,14 @@
         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);
+        mDisplayNameCandidate.clear();
 
-        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);
@@ -1208,20 +1118,10 @@
                     // Display name
                     String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
                     int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
-                    if (!TextUtils.isEmpty(displayName)) {
-                        if (bestDisplayName == null) {
-                            bestDisplayName = displayName;
-                            bestDisplayNameSource = displayNameSource;
-                        } else if (bestDisplayNameSource != displayNameSource) {
-                            if (bestDisplayNameSource < displayNameSource) {
-                                bestDisplayName = displayName;
-                                bestDisplayNameSource = displayNameSource;
-                            }
-                        } else if (NameNormalizer.compareComplexity(displayName,
-                                bestDisplayName) > 0) {
-                            bestDisplayName = displayName;
-                        }
-                    }
+                    int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED);
+                    processDisplayNameCanditate(rawContactId, displayName, displayNameSource,
+                            nameVerified != 0);
+
 
                     // Contact options
                     if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
@@ -1247,7 +1147,9 @@
                         contactTimesContacted = timesContacted;
                     }
 
-                    contactStarred |= (c.getInt(RawContactsQuery.STARRED) != 0);
+                    if (c.getInt(RawContactsQuery.STARRED) != 0) {
+                        contactStarred = 1;
+                    }
 
                     // Single restricted
                     if (totalRowCount > 1) {
@@ -1262,7 +1164,7 @@
                         }
                     }
 
-                    ContactLookupKey.appendToLookupKey(lookupKey,
+                    ContactLookupKey.appendToLookupKey(mSb,
                             c.getString(RawContactsQuery.ACCOUNT_TYPE),
                             c.getString(RawContactsQuery.ACCOUNT_NAME),
                             c.getString(RawContactsQuery.SOURCE_ID),
@@ -1273,7 +1175,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 +1187,7 @@
                             bestPhotoId = dataId;
                             foundSuperPrimaryPhoto |= superprimary;
                         }
-                    } else if (mimetypeId == phoneMimeType) {
+                    } else if (mimetypeId == mMimeTypeIdPhone) {
                         hasPhoneNumber = 1;
                     }
                 }
@@ -1295,28 +1197,68 @@
             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,
+                mDisplayNameCandidate.rawContactId);
 
         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()));
+    }
+
+    /**
+     * Uses the supplied values to determine if they represent a "better" display name
+     * for the aggregate contact currently evaluated.  If so, it updates
+     * {@link #mDisplayNameCandidate} with the new values.
+     */
+    private void processDisplayNameCanditate(long rawContactId, String displayName,
+            int displayNameSource, boolean verified) {
+
+        boolean replace = false;
+        if (mDisplayNameCandidate.rawContactId == -1) {
+            // No previous values available
+            replace = true;
+        } else if (!TextUtils.isEmpty(displayName)) {
+            if (!mDisplayNameCandidate.verified && verified) {
+                // A verified name is better than any other name
+                replace = true;
+            } else if (mDisplayNameCandidate.verified == verified) {
+                if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
+                    // New values come from an superior source, e.g. structured name vs phone number
+                    replace = true;
+                } else if (mDisplayNameCandidate.displayNameSource == displayNameSource
+                        && NameNormalizer.compareComplexity(displayName,
+                                mDisplayNameCandidate.displayName) > 0) {
+                    // New name is more complex than the previously found one
+                    replace = true;
+                }
+            }
+        }
+
+        if (replace) {
+            mDisplayNameCandidate.rawContactId = rawContactId;
+            mDisplayNameCandidate.displayName = displayName;
+            mDisplayNameCandidate.displayNameSource = displayNameSource;
+            mDisplayNameCandidate.verified = verified;
+        }
     }
 
     private interface PhotoIdQuery {
@@ -1348,8 +1290,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 +1329,52 @@
 
     private interface DisplayNameQuery {
         String[] COLUMNS = new String[] {
+            RawContacts._ID,
             RawContactsColumns.DISPLAY_NAME,
             RawContactsColumns.DISPLAY_NAME_SOURCE,
+            RawContacts.NAME_VERIFIED,
         };
 
-        int DISPLAY_NAME = 0;
-        int DISPLAY_NAME_SOURCE = 1;
+        int _ID = 0;
+        int DISPLAY_NAME = 1;
+        int DISPLAY_NAME_SOURCE = 2;
+        int NAME_VERIFIED = 3;
     }
 
-    public void updateDisplayName(SQLiteDatabase db, long rawContactId) {
-
+    public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
         long contactId = mDbHelper.getContactId(rawContactId);
         if (contactId == 0) {
             return;
         }
 
-        int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
-        String bestDisplayName = null;
+        updateDisplayNameForContact(db, contactId);
+    }
 
+    public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
+        mDisplayNameCandidate.clear();
 
+        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 rawContactId = 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;
-                    } else if (bestDisplayNameSource != displayNameSource) {
-                        if (bestDisplayNameSource < displayNameSource) {
-                            bestDisplayName = displayName;
-                            bestDisplayNameSource = displayNameSource;
-                        }
-                    } else if (NameNormalizer.compareComplexity(displayName,
-                            bestDisplayName) > 0) {
-                        bestDisplayName = displayName;
-                    }
-                }
+                int nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED);
+
+                processDisplayNameCanditate(rawContactId, displayName, displayNameSource,
+                        nameVerified != 0);
             }
         } finally {
             c.close();
         }
 
-        if (bestDisplayName == null) {
-            mDisplayNameUpdate.bindNull(1);
-        } else {
-            mDisplayNameUpdate.bindString(1, bestDisplayName);
+        if (mDisplayNameCandidate.rawContactId != -1) {
+            mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
+            mDisplayNameUpdate.bindLong(2, contactId);
+            mDisplayNameUpdate.execute();
         }
-        mDisplayNameUpdate.bindLong(2, contactId);
-        mDisplayNameUpdate.execute();
     }
 
     /**
@@ -1476,12 +1414,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 +1430,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 +1465,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 +1557,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 +1579,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 +1593,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/ContactLocaleUtils.java b/src/com/android/providers/contacts/ContactLocaleUtils.java
new file mode 100644
index 0000000..18d43cf
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactLocaleUtils.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2010 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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import android.provider.ContactsContract.FullNameStyle;
+import com.android.internal.util.HanziToPinyin;
+import com.android.internal.util.HanziToPinyin.Token;
+
+/**
+ * This utility class provides customized sort key and name lookup key according the locale.
+ */
+public class ContactLocaleUtils {
+
+    /**
+     * This class is the default implementation.
+     * <p>
+     * It should be the base class for other locales' implementation.
+     */
+    public static class ContactLocaleUtilsBase {
+        public String getSortKey(String displayName) {
+            return displayName;
+        }
+        public Iterator<String> getNameLookupKeys(String name) {
+            return null;
+        }
+    }
+
+    private static class ChineseContactUtils extends ContactLocaleUtilsBase {
+        @Override
+        public String getSortKey(String displayName) {
+            ArrayList<Token> tokens = HanziToPinyin.getInstance().get(displayName);
+            if (tokens != null && tokens.size() > 0) {
+                StringBuilder sb = new StringBuilder();
+                for (Token token : tokens) {
+                    // Put Chinese character's pinyin, then proceed with the
+                    // character itself.
+                    if (Token.PINYIN == token.type) {
+                        if (sb.length() > 0) {
+                            sb.append(' ');
+                        }
+                        sb.append(token.target);
+                        sb.append(' ');
+                        sb.append(token.source);
+                    } else {
+                        if (sb.length() > 0) {
+                            sb.append(' ');
+                        }
+                        sb.append(token.source);
+                    }
+                }
+                return sb.toString();
+            }
+            return super.getSortKey(displayName);
+        }
+
+        @Override
+        public Iterator<String> getNameLookupKeys(String name) {
+            // TODO : Reduce the object allocation.
+            HashSet<String> keys = new HashSet<String>();
+            ArrayList<Token> tokens = HanziToPinyin.getInstance().get(name);
+            final int tokenCount = tokens.size();
+            final StringBuilder keyPinyin = new StringBuilder();
+            final StringBuilder keyInitial = new StringBuilder();
+            // There is no space among the Chinese Characters, the variant name
+            // lookup key wouldn't work for Chinese. The keyOrignal is used to
+            // build the lookup keys for itself.
+            final StringBuilder keyOrignal = new StringBuilder();
+            for (int i = tokenCount - 1; i >= 0; i--) {
+                final Token token = tokens.get(i);
+                if (Token.PINYIN == token.type) {
+                    keyPinyin.insert(0, token.target);
+                    keyInitial.insert(0, token.target.charAt(0));
+                } else if (Token.LATIN == token.type) {
+                    // Avoid adding space at the end of String.
+                    if (keyPinyin.length() > 0) {
+                        keyPinyin.insert(0, ' ');
+                    }
+                    if (keyOrignal.length() > 0) {
+                        keyOrignal.insert(0, ' ');
+                    }
+                    keyPinyin.insert(0, token.source);
+                    keyInitial.insert(0, token.source.charAt(0));
+                }
+                keyOrignal.insert(0, token.source);
+                keys.add(keyOrignal.toString());
+                keys.add(keyPinyin.toString());
+                keys.add(keyInitial.toString());
+            }
+            return keys.iterator();
+        }
+    }
+
+    private static HashMap<Integer, ContactLocaleUtilsBase> mUtils =
+        new HashMap<Integer, ContactLocaleUtilsBase>();
+
+    private static ContactLocaleUtilsBase mBase = new ContactLocaleUtilsBase();
+    public static String getSortKey(String displayName, int nameStyle) {
+        return get(Integer.valueOf(nameStyle)).getSortKey(displayName);
+    }
+
+    public static Iterator<String> getNameLookupKeys(String name, int nameStyle) {
+        return get(Integer.valueOf(nameStyle)).getNameLookupKeys(name);
+    }
+
+    private synchronized static ContactLocaleUtilsBase get(Integer nameStyle) {
+        ContactLocaleUtilsBase utils = mUtils.get(nameStyle);
+        if (utils == null) {
+            if (nameStyle.intValue() == FullNameStyle.CHINESE) {
+                utils = new ChineseContactUtils();
+                mUtils.put(nameStyle, utils);
+            }
+        }
+        return (utils == null) ? mBase: utils;
+    }
+}
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 154f62f..5ae6c8a 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;
@@ -41,17 +42,23 @@
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.FullNameStyle;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.SocialContract.Activities;
 import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.util.HashMap;
+import java.util.Locale;
 
 /**
  * Database helper for contacts. Designed as a singleton to make sure that all
@@ -61,11 +68,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 = 206;
 
     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";
@@ -95,25 +105,6 @@
                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
 
-        public static final String DATA_JOIN_RAW_CONTACTS_GROUPS = "data "
-                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"
-                + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID
-                + ")";
-
-        public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS = "data "
-                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
-                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
-                + "LEFT OUTER JOIN packages ON (data.package_id = packages._id)";
-
-        public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
-                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
-                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
-                + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
-                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
-        public static final String RAW_CONTACTS_JOIN_CONTACTS = "raw_contacts "
-                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
         // NOTE: This requires late binding of GroupMembership MIME-type
         public static final String RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS = "raw_contacts "
                 + "LEFT OUTER JOIN settings ON ("
@@ -138,21 +129,6 @@
                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
                 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
 
-        public static final String DATA_INNER_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
-                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
-                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
-                + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
-        public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS_GROUPS =
-                "data "
-                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
-                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
-                + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
-                + "LEFT OUTER JOIN groups "
-                + "  ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' "
-                + "      AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") "
-                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
         public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS = "data "
                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
@@ -164,13 +140,6 @@
         public static final String GROUPS_JOIN_PACKAGES = "groups "
                 + "LEFT OUTER JOIN packages ON (groups.package_id = packages._id)";
 
-        public static final String GROUPS_JOIN_PACKAGES_DATA_RAW_CONTACTS_CONTACTS = "groups "
-                + "LEFT OUTER JOIN packages ON (groups.package_id = packages._id) "
-                + "LEFT OUTER JOIN data "
-                + "  ON (groups._id = data." + GroupMembership.GROUP_ROW_ID + ") "
-                + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
-                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
         public static final String ACTIVITIES = "activities";
 
         public static final String ACTIVITIES_JOIN_MIMETYPES = "activities "
@@ -181,7 +150,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 +220,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;
@@ -263,6 +230,8 @@
                 + Contacts.CUSTOM_RINGTONE;
         public static final String CONCRETE_SEND_TO_VOICEMAIL = Tables.CONTACTS + "."
                 + Contacts.SEND_TO_VOICEMAIL;
+        public static final String CONCRETE_LOOKUP_KEY = Tables.CONTACTS + "."
+                + Contacts.LOOKUP_KEY;
     }
 
     public interface RawContactsColumns {
@@ -293,22 +262,17 @@
         public static final String CONCRETE_IS_RESTRICTED =
                 Tables.RAW_CONTACTS + "." + RawContacts.IS_RESTRICTED;
 
-        public static final String DISPLAY_NAME = "display_name";
-        public static final String DISPLAY_NAME_SOURCE = "display_name_source";
+        public static final String DISPLAY_NAME = RawContacts.DISPLAY_NAME_PRIMARY;
+        public static final String DISPLAY_NAME_SOURCE = RawContacts.DISPLAY_NAME_SOURCE;
         public static final String AGGREGATION_NEEDED = "aggregation_needed";
-    }
+        public static final String CONTACT_IN_VISIBLE_GROUP = "contact_in_visible_group";
 
-    /**
-     * Types of data used to produce the display name for a contact. Listed in the order
-     * of increasing priority.
-     */
-    public interface DisplayNameSources {
-        int UNDEFINED = 0;
-        int EMAIL = 10;
-        int PHONE = 20;
-        int ORGANIZATION = 30;
-        int NICKNAME = 35;
-        int STRUCTURED_NAME = 40;
+        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;
+        public static final String CONCRETE_NAME_VERIFIED =
+                Tables.RAW_CONTACTS + "." + RawContacts.NAME_VERIFIED;
     }
 
     public interface DataColumns {
@@ -376,6 +340,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 {
@@ -392,9 +357,10 @@
         public static final int NICKNAME = 3;
         public static final int EMAIL_BASED_NICKNAME = 4;
         public static final int ORGANIZATION = 5;
+        public static final int NAME_SHORTHAND = 6;
 
         // This is the highest name lookup type code plus one
-        public static final int TYPE_COUNT = 6;
+        public static final int TYPE_COUNT = 7;
 
         public static boolean isBasedOnStructuredName(int nameLookupType) {
             return nameLookupType == NameLookupType.NAME_EXACT
@@ -494,14 +460,15 @@
 
 
     /** 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 +493,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",
@@ -562,13 +529,33 @@
                 + " FROM " + Tables.ACTIVITIES_JOIN_MIMETYPES + " WHERE " + Tables.ACTIVITIES + "."
                 + Activities._ID + "=?");
 
-        // Compile statements for updating visibility
-        final String visibleUpdate = "UPDATE " + Tables.CONTACTS + " SET "
-                + Contacts.IN_VISIBLE_GROUP + "=(" + Clauses.CONTACT_IS_VISIBLE + ")";
+        // Change visibility of a specific contact
+        mVisibleSpecificUpdate = db.compileStatement(
+                "UPDATE " + Tables.CONTACTS +
+                " SET " + Contacts.IN_VISIBLE_GROUP + "=(" + Clauses.CONTACT_IS_VISIBLE + ")" +
+                " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
 
-        mVisibleUpdate = db.compileStatement(visibleUpdate);
-        mVisibleSpecificUpdate = db.compileStatement(visibleUpdate + " WHERE "
-                + ContactsColumns.CONCRETE_ID + "=?");
+        // Return visibility of the aggregate contact joined with the raw contact
+        String contactVisibility =
+                "SELECT " + Contacts.IN_VISIBLE_GROUP +
+                " FROM " + Tables.CONTACTS +
+                " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID;
+
+        // Set visibility of raw contacts to the visibility of corresponding aggregate contacts
+        mVisibleUpdateRawContacts = db.compileStatement(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=("
+                        + contactVisibility + ")" +
+                " WHERE " + RawContacts.DELETED + "=0" +
+                " AND " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "!=("
+                        + contactVisibility + ")=1");
+
+        // Set visibility of a raw contact to the visibility of corresponding aggregate contact
+        mVisibleSpecificUpdateRawContacts = db.compileStatement(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=("
+                        + contactVisibility + ")" +
+                " WHERE " + RawContacts.DELETED + "=0 AND " + RawContacts.CONTACT_ID + "=?");
 
         db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
         db.execSQL("CREATE TABLE IF NOT EXISTS " + DATABASE_PRESENCE + "." + Tables.PRESENCE + " ("+
@@ -645,7 +632,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 +647,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 +658,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," +
@@ -691,24 +681,22 @@
                 RawContacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.LAST_TIME_CONTACTED + " INTEGER," +
                 RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
-                RawContactsColumns.DISPLAY_NAME + " TEXT," +
-                RawContactsColumns.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
+                RawContacts.DISPLAY_NAME_PRIMARY + " TEXT," +
+                RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT," +
+                RawContacts.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
                         DisplayNameSources.UNDEFINED + "," +
+                RawContacts.PHONETIC_NAME + " TEXT," +
+                RawContacts.PHONETIC_NAME_STYLE + " TEXT," +
+                RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE LOCALIZED," +
+                RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE LOCALIZED," +
+                RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0," +
+                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.SYNC1 + " TEXT, " +
                 RawContacts.SYNC2 + " TEXT, " +
                 RawContacts.SYNC3 + " TEXT, " +
                 RawContacts.SYNC4 + " TEXT " +
         ");");
 
-        db.execSQL("CREATE TRIGGER raw_contacts_times_contacted UPDATE OF " +
-                    RawContacts.LAST_TIME_CONTACTED + " ON " + Tables.RAW_CONTACTS + " " +
-                "BEGIN " +
-                    "UPDATE " + Tables.RAW_CONTACTS + " SET "
-                        + RawContacts.TIMES_CONTACTED + " = " + "" +
-                            "(new." + RawContacts.TIMES_CONTACTED + " + 1)"
-                        + " WHERE _id = new._id;" +
-                "END");
-
         db.execSQL("CREATE INDEX raw_contacts_contact_id_index ON " + Tables.RAW_CONTACTS + " (" +
                 RawContacts.CONTACT_ID +
         ");");
@@ -724,6 +712,16 @@
 //                RawContactsColumns.AGGREGATION_NEEDED +
 //        ");");
 
+        db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
+                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+                RawContacts.SORT_KEY_PRIMARY +
+        ");");
+
+        db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
+                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+                RawContacts.SORT_KEY_ALTERNATIVE +
+        ");");
+
         // Package name mapping table
         db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" +
                 PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -784,7 +782,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 +792,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
@@ -975,14 +980,6 @@
 
 
         db.execSQL("DROP TRIGGER IF EXISTS contacts_times_contacted;");
-        db.execSQL("CREATE TRIGGER contacts_times_contacted UPDATE OF " +
-                Contacts.LAST_TIME_CONTACTED + " ON " + Tables.CONTACTS + " " +
-            "BEGIN " +
-                "UPDATE " + Tables.CONTACTS + " SET "
-                    + Contacts.TIMES_CONTACTED + " = " + "" +
-                            "(new." + Contacts.TIMES_CONTACTED + " + 1)"
-                    + " WHERE _id = new._id;" +
-            "END");
 
         /*
          * Triggers that update {@link RawContacts#VERSION} when the contact is
@@ -991,7 +988,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 +998,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 +1025,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 "
@@ -1075,6 +1072,7 @@
                 RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
                 + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
                 + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
+                + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS " + RawContacts.NAME_VERIFIED + ","
                 + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
                 + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
                 + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ","
@@ -1094,17 +1092,34 @@
                 + ContactsColumns.CONCRETE_STARRED
                         + " AS " + RawContacts.STARRED;
 
+        String contactNameColumns =
+                "name_raw_contact." + RawContacts.DISPLAY_NAME_SOURCE
+                        + " AS " + Contacts.DISPLAY_NAME_SOURCE + ", "
+                + "name_raw_contact." + RawContacts.DISPLAY_NAME_PRIMARY
+                        + " AS " + Contacts.DISPLAY_NAME_PRIMARY + ", "
+                + "name_raw_contact." + RawContacts.DISPLAY_NAME_ALTERNATIVE
+                        + " AS " + Contacts.DISPLAY_NAME_ALTERNATIVE + ", "
+                + "name_raw_contact." + RawContacts.PHONETIC_NAME
+                        + " AS " + Contacts.PHONETIC_NAME + ", "
+                + "name_raw_contact." + RawContacts.PHONETIC_NAME_STYLE
+                        + " AS " + Contacts.PHONETIC_NAME_STYLE + ", "
+                + "name_raw_contact." + RawContacts.SORT_KEY_PRIMARY
+                        + " AS " + Contacts.SORT_KEY_PRIMARY + ", "
+                + "name_raw_contact." + RawContacts.SORT_KEY_ALTERNATIVE
+                        + " AS " + Contacts.SORT_KEY_ALTERNATIVE + ", "
+                + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
+                        + " AS " + Contacts.IN_VISIBLE_GROUP;
+
         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 + ", "
+                + contactNameColumns + ", "
                 + 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 +1128,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 +1140,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 + ","
@@ -1137,6 +1154,13 @@
                 + RawContacts.CONTACT_ID + ", "
                 + RawContacts.AGGREGATION_MODE + ", "
                 + RawContacts.DELETED + ", "
+                + RawContacts.DISPLAY_NAME_SOURCE  + ", "
+                + RawContacts.DISPLAY_NAME_PRIMARY  + ", "
+                + RawContacts.DISPLAY_NAME_ALTERNATIVE  + ", "
+                + RawContacts.PHONETIC_NAME  + ", "
+                + RawContacts.PHONETIC_NAME_STYLE  + ", "
+                + RawContacts.SORT_KEY_PRIMARY  + ", "
+                + RawContacts.SORT_KEY_ALTERNATIVE + ", "
                 + rawContactOptionColumns + ", "
                 + syncColumns
                 + " FROM " + Tables.RAW_CONTACTS;
@@ -1148,13 +1172,10 @@
         String contactsColumns =
                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
                         + " AS " + Contacts.CUSTOM_RINGTONE + ", "
-                + ContactsColumns.CONCRETE_DISPLAY_NAME
-                        + " AS " + Contacts.DISPLAY_NAME + ", "
-                + Contacts.IN_VISIBLE_GROUP + ", "
+                + contactNameColumns + ", "
                 + 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 +1189,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) {
@@ -1220,6 +1238,7 @@
                 + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
                 + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
                 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
+                + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS " + RawContacts.NAME_VERIFIED + ","
                 + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + ","
                 + RawContacts.CONTACT_ID + ", "
                 + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ", "
@@ -1304,8 +1323,10 @@
 
         Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion);
 
+        boolean upgradeViewsAndTriggers = false;
+
         if (oldVersion == 99) {
-            createContactEntitiesView(db);
+            upgradeViewsAndTriggers = true;
             oldVersion++;
         }
 
@@ -1317,37 +1338,451 @@
             updateIndexStats(db, Tables.MIMETYPES,
                     "mimetypes_mimetype_index", "50 1 1");
 
-            createContactsViews(db);
+            upgradeViewsAndTriggers = true;
             oldVersion++;
         }
 
         if (oldVersion == 101) {
-            createContactsTriggers(db);
+            upgradeViewsAndTriggers = true;
             oldVersion++;
         }
 
         if (oldVersion == 102) {
-            LegacyApiSupport.createViews(db);
+            upgradeViewsAndTriggers = true;
             oldVersion++;
         }
 
         if (oldVersion == 103) {
-            createContactEntitiesView(db);
+            upgradeViewsAndTriggers = true;
             oldVersion++;
         }
 
-        if (oldVersion == 104) {
-            LegacyApiSupport.createViews(db);
+        if (oldVersion == 104 || oldVersion == 201) {
             LegacyApiSupport.createSettingsTable(db);
+            upgradeViewsAndTriggers = true;
             oldVersion++;
         }
 
+        if (oldVersion == 105) {
+            upgradeToVersion202(db);
+            oldVersion = 202;
+        }
+
+        if (oldVersion == 202) {
+            upgradeToVersion203(db);
+            upgradeViewsAndTriggers = true;
+            oldVersion++;
+        }
+
+        if (oldVersion == 203) {
+            upgradeViewsAndTriggers = true;
+            oldVersion++;
+        }
+
+        if (oldVersion == 204) {
+            upgradeToVersion205(db);
+            upgradeViewsAndTriggers = true;
+            oldVersion++;
+        }
+
+        if (oldVersion == 205) {
+            upgrateToVersion206(db);
+            upgradeViewsAndTriggers = true;
+            oldVersion++;
+        }
+
+        if (upgradeViewsAndTriggers) {
+            createContactsViews(db);
+            createGroupsView(db);
+            createContactEntitiesView(db);
+            createContactsTriggers(db);
+            LegacyApiSupport.createViews(db);
+        }
+
         if (oldVersion != newVersion) {
             throw new IllegalStateException(
                     "error upgrading the database to version " + newVersion);
         }
     }
 
+    private void upgradeToVersion202(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 void upgradeToVersion203(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 + ")" +
+                " WHERE " + RawContacts.CONTACT_ID + " NOT NULL"
+        );
+
+        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 +
+        ");");
+    }
+
+    private void upgradeToVersion205(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT;");
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContacts.PHONETIC_NAME + " TEXT;");
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContacts.PHONETIC_NAME_STYLE + " INTEGER;");
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE LOCALIZED;");
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE LOCALIZED;");
+
+        final Locale locale = Locale.getDefault();
+
+        NameSplitter splitter = new NameSplitter(
+                mContext.getString(com.android.internal.R.string.common_name_prefixes),
+                mContext.getString(com.android.internal.R.string.common_last_name_prefixes),
+                mContext.getString(com.android.internal.R.string.common_name_suffixes),
+                mContext.getString(com.android.internal.R.string.common_name_conjunctions),
+                locale);
+
+        SQLiteStatement rawContactUpdate = db.compileStatement(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " +
+                        RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
+                        RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
+                        RawContacts.PHONETIC_NAME + "=?," +
+                        RawContacts.PHONETIC_NAME_STYLE + "=?," +
+                        RawContacts.SORT_KEY_PRIMARY + "=?," +
+                        RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
+                " WHERE " + RawContacts._ID + "=?");
+
+        upgradeStructuredNamesToVersion205(db, rawContactUpdate, splitter);
+        upgradeOrganizationsToVersion205(db, rawContactUpdate, splitter);
+
+        db.execSQL("DROP INDEX raw_contact_sort_key1_index");
+        db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
+                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+                RawContacts.SORT_KEY_PRIMARY +
+        ");");
+
+        db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
+                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+                RawContacts.SORT_KEY_ALTERNATIVE +
+        ");");
+    }
+
+    private interface StructName205Query {
+        String TABLE = Tables.DATA_JOIN_RAW_CONTACTS;
+
+        String COLUMNS[] = {
+                DataColumns.CONCRETE_ID,
+                Data.RAW_CONTACT_ID,
+                RawContacts.DISPLAY_NAME_SOURCE,
+                RawContacts.DISPLAY_NAME_PRIMARY,
+                StructuredName.PREFIX,
+                StructuredName.GIVEN_NAME,
+                StructuredName.MIDDLE_NAME,
+                StructuredName.FAMILY_NAME,
+                StructuredName.SUFFIX,
+                StructuredName.PHONETIC_FAMILY_NAME,
+                StructuredName.PHONETIC_MIDDLE_NAME,
+                StructuredName.PHONETIC_GIVEN_NAME,
+        };
+
+        int ID = 0;
+        int RAW_CONTACT_ID = 1;
+        int DISPLAY_NAME_SOURCE = 2;
+        int DISPLAY_NAME = 3;
+        int PREFIX = 4;
+        int GIVEN_NAME = 5;
+        int MIDDLE_NAME = 6;
+        int FAMILY_NAME = 7;
+        int SUFFIX = 8;
+        int PHONETIC_FAMILY_NAME = 9;
+        int PHONETIC_MIDDLE_NAME = 10;
+        int PHONETIC_GIVEN_NAME = 11;
+    }
+
+    private void upgradeStructuredNamesToVersion205(SQLiteDatabase db,
+            SQLiteStatement rawContactUpdate, NameSplitter splitter) {
+
+        // Process structured names to detect the style of the full name and phonetic name
+
+        long mMimeType;
+        try {
+            mMimeType = DatabaseUtils.longForQuery(db,
+                    "SELECT " + MimetypesColumns._ID +
+                    " FROM " + Tables.MIMETYPES +
+                    " WHERE " + MimetypesColumns.MIMETYPE
+                            + "='" + StructuredName.CONTENT_ITEM_TYPE + "'", null);
+        } catch (SQLiteDoneException e) {
+            // No structured names in the database
+            return;
+        }
+
+        SQLiteStatement structuredNameUpdate = db.compileStatement(
+                "UPDATE " + Tables.DATA +
+                " SET " +
+                        StructuredName.FULL_NAME_STYLE + "=?," +
+                        StructuredName.DISPLAY_NAME + "=?," +
+                        StructuredName.PHONETIC_NAME_STYLE + "=?" +
+                " WHERE " + Data._ID + "=?");
+
+        NameSplitter.Name name = new NameSplitter.Name();
+        StringBuilder sb = new StringBuilder();
+        Cursor cursor = db.query(StructName205Query.TABLE,
+                StructName205Query.COLUMNS,
+                DataColumns.MIMETYPE_ID + "=" + mMimeType, null, null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                long dataId = cursor.getLong(StructName205Query.ID);
+                long rawContactId = cursor.getLong(StructName205Query.RAW_CONTACT_ID);
+                int displayNameSource = cursor.getInt(StructName205Query.DISPLAY_NAME_SOURCE);
+                String displayName = cursor.getString(StructName205Query.DISPLAY_NAME);
+
+                name.clear();
+                name.prefix = cursor.getString(StructName205Query.PREFIX);
+                name.givenNames = cursor.getString(StructName205Query.GIVEN_NAME);
+                name.middleName = cursor.getString(StructName205Query.MIDDLE_NAME);
+                name.familyName = cursor.getString(StructName205Query.FAMILY_NAME);
+                name.suffix = cursor.getString(StructName205Query.SUFFIX);
+                name.phoneticFamilyName = cursor.getString(StructName205Query.PHONETIC_FAMILY_NAME);
+                name.phoneticMiddleName = cursor.getString(StructName205Query.PHONETIC_MIDDLE_NAME);
+                name.phoneticGivenName = cursor.getString(StructName205Query.PHONETIC_GIVEN_NAME);
+
+                upgradeNameToVersion205(dataId, rawContactId, displayNameSource, displayName, name,
+                        structuredNameUpdate, rawContactUpdate, splitter, sb);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void upgradeNameToVersion205(long dataId, long rawContactId, int displayNameSource,
+            String currentDisplayName, NameSplitter.Name name,
+            SQLiteStatement structuredNameUpdate, SQLiteStatement rawContactUpdate,
+            NameSplitter splitter, StringBuilder sb) {
+
+        splitter.guessNameStyle(name);
+        name.fullNameStyle = splitter.getAdjustedFullNameStyle(name.fullNameStyle);
+        String displayName = splitter.join(name, true);
+
+        structuredNameUpdate.bindLong(1, name.fullNameStyle);
+        DatabaseUtils.bindObjectToProgram(structuredNameUpdate, 2, displayName);
+        structuredNameUpdate.bindLong(3, name.phoneticNameStyle);
+        structuredNameUpdate.bindLong(4, dataId);
+        structuredNameUpdate.execute();
+
+        if (displayNameSource == DisplayNameSources.STRUCTURED_NAME) {
+            String displayNameAlternative = splitter.join(name, false);
+            String phoneticName = splitter.joinPhoneticName(name);
+            String sortKey = null;
+            String sortKeyAlternative = null;
+
+            if (phoneticName != null) {
+                sortKey = sortKeyAlternative = phoneticName;
+            } else if (name.fullNameStyle == FullNameStyle.CHINESE) {
+                sortKey = sortKeyAlternative = splitter.convertHanziToPinyin(displayName);
+            }
+
+            if (sortKey == null) {
+                sortKey = displayName;
+                sortKeyAlternative = displayNameAlternative;
+            }
+
+            updateRawContact205(rawContactUpdate, rawContactId, displayName,
+                    displayNameAlternative, name.phoneticNameStyle, phoneticName, sortKey,
+                    sortKeyAlternative);
+        }
+    }
+
+    private interface Organization205Query {
+        String TABLE = Tables.DATA_JOIN_RAW_CONTACTS;
+
+        String COLUMNS[] = {
+                DataColumns.CONCRETE_ID,
+                Data.RAW_CONTACT_ID,
+                Organization.COMPANY,
+                Organization.PHONETIC_NAME,
+        };
+
+        int ID = 0;
+        int RAW_CONTACT_ID = 1;
+        int COMPANY = 2;
+        int PHONETIC_NAME = 3;
+    }
+
+    private void upgradeOrganizationsToVersion205(SQLiteDatabase db,
+            SQLiteStatement rawContactUpdate, NameSplitter splitter) {
+
+        final long mMimeType;
+        try {
+            mMimeType = DatabaseUtils.longForQuery(db,
+                    "SELECT " + MimetypesColumns._ID +
+                    " FROM " + Tables.MIMETYPES +
+                    " WHERE " + MimetypesColumns.MIMETYPE
+                            + "='" + Organization.CONTENT_ITEM_TYPE + "'", null);
+        } catch (SQLiteDoneException e) {
+            // No organizations in the database
+            return;
+        }
+
+        SQLiteStatement organizationUpdate = db.compileStatement(
+                "UPDATE " + Tables.DATA +
+                " SET " +
+                        Organization.PHONETIC_NAME_STYLE + "=?" +
+                " WHERE " + Data._ID + "=?");
+
+        Cursor cursor = db.query(Organization205Query.TABLE, Organization205Query.COLUMNS,
+                DataColumns.MIMETYPE_ID + "=" + mMimeType + " AND "
+                        + RawContacts.DISPLAY_NAME_SOURCE + "=" + DisplayNameSources.ORGANIZATION,
+                null, null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                long dataId = cursor.getLong(Organization205Query.ID);
+                long rawContactId = cursor.getLong(Organization205Query.RAW_CONTACT_ID);
+                String company = cursor.getString(Organization205Query.COMPANY);
+                String phoneticName = cursor.getString(Organization205Query.PHONETIC_NAME);
+
+                int phoneticNameStyle = splitter.guessPhoneticNameStyle(phoneticName);
+
+                organizationUpdate.bindLong(1, phoneticNameStyle);
+                organizationUpdate.bindLong(2, dataId);
+                organizationUpdate.execute();
+
+                String sortKey = null;
+                if (phoneticName == null && company != null) {
+                    int nameStyle = splitter.guessFullNameStyle(company);
+                    nameStyle = splitter.getAdjustedFullNameStyle(nameStyle);
+                    if (nameStyle == FullNameStyle.CHINESE) {
+                        sortKey = splitter.convertHanziToPinyin(company);
+                    }
+                }
+
+                if (sortKey == null) {
+                    sortKey = company;
+                }
+
+                updateRawContact205(rawContactUpdate, rawContactId, company,
+                        company, phoneticNameStyle, phoneticName, sortKey, sortKey);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void updateRawContact205(SQLiteStatement rawContactUpdate, long rawContactId,
+            String displayName, String displayNameAlternative, int phoneticNameStyle,
+            String phoneticName, String sortKeyPrimary, String sortKeyAlternative) {
+        bindString(rawContactUpdate, 1, displayName);
+        bindString(rawContactUpdate, 2, displayNameAlternative);
+        bindString(rawContactUpdate, 3, phoneticName);
+        rawContactUpdate.bindLong(4, phoneticNameStyle);
+        bindString(rawContactUpdate, 5, sortKeyPrimary);
+        bindString(rawContactUpdate, 6, sortKeyAlternative);
+        rawContactUpdate.bindLong(7, rawContactId);
+        rawContactUpdate.execute();
+    }
+
+    private void upgrateToVersion206(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0;");
+    }
+
+    private void bindString(SQLiteStatement stmt, int index, String value) {
+        if (value == null) {
+            stmt.bindNull(index);
+        } else {
+            stmt.bindString(index, value);
+        }
+    }
+
     /**
      * Adds index stats into the SQLite database to force it to always use the lookup indexes.
      */
@@ -1376,6 +1811,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");
@@ -1400,7 +1837,8 @@
      * the table.  The following integer(s) are the expected number of records selected with the
      * index.  There should be one integer per indexed column.
      */
-    private void updateIndexStats(SQLiteDatabase db, String table, String index, String stats) {
+    private void updateIndexStats(SQLiteDatabase db, String table, String index,
+            String stats) {
         db.execSQL("DELETE FROM sqlite_stat1 WHERE tbl='" + table + "' AND idx='" + index + "';");
         db.execSQL("INSERT INTO sqlite_stat1 (tbl,idx,stat)"
                 + " VALUES ('" + table + "','" + index + "','" + stats + "');");
@@ -1435,8 +1873,6 @@
         db.execSQL("DELETE FROM " + Tables.CALLS + ";");
 
         // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
-
-        db.execSQL("VACUUM;");
     }
 
     /**
@@ -1510,6 +1946,10 @@
     public long getMimeTypeId(String mimetype) {
         // Make sure compiled statements are ready by opening database
         getReadableDatabase();
+        return getMimeTypeIdNoDbCheck(mimetype);
+    }
+
+    private long getMimeTypeIdNoDbCheck(String mimetype) {
         return getCachedId(mMimetypeQuery, mMimetypeInsert, mimetype, mMimetypeCache);
     }
 
@@ -1551,19 +1991,67 @@
      * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts.
      */
     public void updateAllVisible() {
+        SQLiteDatabase db = getWritableDatabase();
         final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
-        mVisibleUpdate.bindLong(1, groupMembershipMimetypeId);
-        mVisibleUpdate.execute();
+        String[] selectionArgs = new String[]{String.valueOf(groupMembershipMimetypeId)};
+
+        // There are a couple questions that can be asked regarding the
+        // following two update statements:
+        //
+        // Q: Why do we run these two queries separately? They seem like they could be combined.
+        // A: This is a result of painstaking experimentation.  Turns out that the most
+        // important optimization is to make sure we never update a value to its current value.
+        // Changing 0 to 0 is unexpectedly expensive - SQLite actually writes the unchanged
+        // rows back to disk.  The other consideration is that the CONTACT_IS_VISIBLE condition
+        // is very complex and executing it twice in the same statement ("if contact_visible !=
+        // CONTACT_IS_VISIBLE change it to CONTACT_IS_VISIBLE") is more expensive than running
+        // two update statements.
+        //
+        // Q: How come we are using db.update instead of compiled statements?
+        // A: This is a limitation of the compiled statement API. It does not return the
+        // number of rows changed.  As you will see later in this method we really need
+        // to know how many rows have been changed.
+
+        // First update contacts that are currently marked as invisible, but need to be visible
+        ContentValues values = new ContentValues();
+        values.put(Contacts.IN_VISIBLE_GROUP, 1);
+        int countMadeVisible = db.update(Tables.CONTACTS, values,
+                Contacts.IN_VISIBLE_GROUP + "=0" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1",
+                selectionArgs);
+
+        // Next update contacts that are currently marked as visible, but need to be invisible
+        values.put(Contacts.IN_VISIBLE_GROUP, 0);
+        int countMadeInvisible = db.update(Tables.CONTACTS, values,
+                Contacts.IN_VISIBLE_GROUP + "=1" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=0",
+                selectionArgs);
+
+        if (countMadeVisible != 0 || countMadeInvisible != 0) {
+            // TODO break out the fields (contact_in_visible_group, sort_key, sort_key_alt) into
+            // a separate table.
+            // Rationale: The following statement will take a very long time on
+            // a large database even though we are only changing one field from 0 to 1 or from
+            // 1 to 0.  The reason for the slowness is that SQLite will need to write the whole
+            // page even when only one bit on it changes. Changing the visibility of a
+            // significant number of contacts will likely read and write almost the entire
+            // raw_contacts table.  So, the solution is to break out into a separate table
+            // the changing field along with the sort keys used for index-based sorting.
+            // That table will occupy a smaller number of pages, so rewriting it would
+            // not be as expensive.
+            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 +2080,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 +2108,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/ContactsProvider.java b/src/com/android/providers/contacts/ContactsProvider.java
deleted file mode 100644
index 6a3e6dd..0000000
--- a/src/com/android/providers/contacts/ContactsProvider.java
+++ /dev/null
@@ -1,4642 +0,0 @@
-/*
- * Copyright (C) 2006 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.app.SearchManager;
-import android.content.AbstractSyncableContentProvider;
-import android.content.AbstractTableMerger;
-import android.content.ContentProvider;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.UriMatcher;
-import android.content.res.AssetFileDescriptor;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.CursorJoiner;
-import android.database.DatabaseUtils;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteContentHelper;
-import android.database.sqlite.SQLiteCursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteDoneException;
-import android.database.sqlite.SQLiteException;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.database.sqlite.SQLiteStatement;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Debug;
-import android.os.MemoryFile;
-import android.os.ParcelFileDescriptor;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
-import android.provider.Contacts;
-import android.provider.Contacts.ContactMethods;
-import android.provider.Contacts.Extensions;
-import android.provider.Contacts.GroupMembership;
-import android.provider.Contacts.Groups;
-import android.provider.Contacts.GroupsColumns;
-import android.provider.Contacts.Intents;
-import android.provider.Contacts.Organizations;
-import android.provider.Contacts.People;
-import android.provider.Contacts.PeopleColumns;
-import android.provider.Contacts.Phones;
-import android.provider.Contacts.Photos;
-import android.provider.Contacts.Presence;
-import android.provider.Contacts.PresenceColumns;
-import android.provider.LiveFolders;
-import android.provider.SyncConstValue;
-import android.provider.Calendar;
-import android.telephony.PhoneNumberUtils;
-import android.text.TextUtils;
-import android.util.Config;
-import android.util.Log;
-import android.accounts.Account;
-
-import com.android.internal.database.ArrayListCursor;
-import com.google.android.collect.Maps;
-import com.google.android.collect.Sets;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-public class ContactsProvider extends AbstractSyncableContentProvider {
-    private static final String STREQUENT_ORDER_BY = "times_contacted DESC, display_name ASC";
-    private static final String STREQUENT_LIMIT =
-            "(SELECT COUNT(*) FROM people WHERE starred = 1) + 25";
-
-    private static final String PEOPLE_PHONES_JOIN =
-            "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
-            + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id)";
-
-    private static final String PEOPLE_PHONES_PHOTOS_JOIN =
-            "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
-            + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id) "
-            + "LEFT OUTER JOIN photos ON (photos." + Photos.PERSON_ID + "=people._id)";
-
-    private static final String PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN =
-            "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
-            + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id) "
-            + "LEFT OUTER JOIN photos ON (photos." + Photos.PERSON_ID + "=people._id) "
-            + "LEFT OUTER JOIN organizations ON (organizations._id=people.primary_organization)";
-
-    private static final String GTALK_PROTOCOL_STRING =
-            ContactMethods.encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
-
-    private static final String[] ID_TYPE_PROJECTION = new String[]{"_id", "type"};
-
-    private static final String[] sIsPrimaryProjectionWithoutKind =
-            new String[]{"isprimary", "person", "_id"};
-    private static final String[] sIsPrimaryProjectionWithKind =
-            new String[]{"isprimary", "person", "_id", "kind"};
-
-    private static final String WHERE_ID = "_id=?";
-
-    private static final String sGroupsJoinString;
-
-    private static final String PREFS_NAME_OWNER = "owner-info";
-    private static final String PREF_OWNER_ID = "owner-id";
-
-    /** this is suitable for use by insert/update/delete/query and may be passed
-     * as a method call parameter. Only insert/update/delete/query should call .clear() on it */
-    private final ContentValues mValues = new ContentValues();
-
-    /** this is suitable for local use in methods and should never be passed as a parameter to
-     * other methods (other than the DB layer) */
-    private final ContentValues mValuesLocal = new ContentValues();
-
-    private Account[] mAccounts = new Account[0];
-    private final Object mAccountsLock = new Object();
-
-    private DatabaseUtils.InsertHelper mDeletedPeopleInserter;
-    private DatabaseUtils.InsertHelper mPeopleInserter;
-    private int mIndexPeopleSyncId;
-    private int mIndexPeopleSyncTime;
-    private int mIndexPeopleSyncVersion;
-    private int mIndexPeopleSyncDirty;
-    private int mIndexPeopleSyncAccountName;
-    private int mIndexPeopleSyncAccountType;
-    private int mIndexPeopleName;
-    private int mIndexPeoplePhoneticName;
-    private int mIndexPeopleNotes;
-    private DatabaseUtils.InsertHelper mGroupsInserter;
-    private DatabaseUtils.InsertHelper mPhotosInserter;
-    private int mIndexPhotosPersonId;
-    private int mIndexPhotosSyncId;
-    private int mIndexPhotosSyncTime;
-    private int mIndexPhotosSyncVersion;
-    private int mIndexPhotosSyncDirty;
-    private int mIndexPhotosSyncAccountName;
-    private int mIndexPhotosSyncAccountType;
-    private int mIndexPhotosExistsOnServer;
-    private int mIndexPhotosSyncError;
-    private DatabaseUtils.InsertHelper mContactMethodsInserter;
-    private int mIndexContactMethodsPersonId;
-    private int mIndexContactMethodsLabel;
-    private int mIndexContactMethodsKind;
-    private int mIndexContactMethodsType;
-    private int mIndexContactMethodsData;
-    private int mIndexContactMethodsAuxData;
-    private int mIndexContactMethodsIsPrimary;
-    private DatabaseUtils.InsertHelper mOrganizationsInserter;
-    private int mIndexOrganizationsPersonId;
-    private int mIndexOrganizationsLabel;
-    private int mIndexOrganizationsType;
-    private int mIndexOrganizationsCompany;
-    private int mIndexOrganizationsTitle;
-    private int mIndexOrganizationsIsPrimary;
-    private DatabaseUtils.InsertHelper mExtensionsInserter;
-    private int mIndexExtensionsPersonId;
-    private int mIndexExtensionsName;
-    private int mIndexExtensionsValue;
-    private DatabaseUtils.InsertHelper mGroupMembershipInserter;
-    private int mIndexGroupMembershipPersonId;
-    private int mIndexGroupMembershipGroupSyncAccountName;
-    private int mIndexGroupMembershipGroupSyncAccountType;
-    private int mIndexGroupMembershipGroupSyncId;
-    private DatabaseUtils.InsertHelper mCallsInserter;
-    private DatabaseUtils.InsertHelper mPhonesInserter;
-    private int mIndexPhonesPersonId;
-    private int mIndexPhonesLabel;
-    private int mIndexPhonesType;
-    private int mIndexPhonesNumber;
-    private int mIndexPhonesNumberKey;
-    private int mIndexPhonesIsPrimary;
-    private boolean mUseStrictPhoneNumberComparation;
-
-    public ContactsProvider() {
-        super(DATABASE_NAME, DATABASE_VERSION, Contacts.CONTENT_URI);
-        mUseStrictPhoneNumberComparation =
-            getContext().getResources().getBoolean(
-                    com.android.internal.R.bool.config_use_strict_phone_number_comparation);
-    }
-
-    @Override
-    protected void onDatabaseOpened(SQLiteDatabase db) {
-        maybeCreatePresenceTable(db);
-
-        // Mark all the tables as syncable
-        db.markTableSyncable(sPeopleTable, sDeletedPeopleTable);
-        db.markTableSyncable(sPhonesTable, Phones.PERSON_ID, sPeopleTable);
-        db.markTableSyncable(sContactMethodsTable, ContactMethods.PERSON_ID, sPeopleTable);
-        db.markTableSyncable(sOrganizationsTable, Organizations.PERSON_ID, sPeopleTable);
-        db.markTableSyncable(sGroupmembershipTable, GroupMembership.PERSON_ID, sPeopleTable);
-        db.markTableSyncable(sExtensionsTable, Extensions.PERSON_ID, sPeopleTable);
-        db.markTableSyncable(sGroupsTable, sDeletedGroupsTable);
-
-        mDeletedPeopleInserter = new DatabaseUtils.InsertHelper(db, sDeletedPeopleTable);
-        mPeopleInserter = new DatabaseUtils.InsertHelper(db, sPeopleTable);
-        mIndexPeopleSyncId = mPeopleInserter.getColumnIndex(People._SYNC_ID);
-        mIndexPeopleSyncTime = mPeopleInserter.getColumnIndex(People._SYNC_TIME);
-        mIndexPeopleSyncVersion = mPeopleInserter.getColumnIndex(People._SYNC_VERSION);
-        mIndexPeopleSyncDirty = mPeopleInserter.getColumnIndex(People._SYNC_DIRTY);
-        mIndexPeopleSyncAccountName = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT);
-        mIndexPeopleSyncAccountType = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT_TYPE);
-        mIndexPeopleName = mPeopleInserter.getColumnIndex(People.NAME);
-        mIndexPeoplePhoneticName = mPeopleInserter.getColumnIndex(People.PHONETIC_NAME);
-        mIndexPeopleNotes = mPeopleInserter.getColumnIndex(People.NOTES);
-
-        mGroupsInserter = new DatabaseUtils.InsertHelper(db, sGroupsTable);
-
-        mPhotosInserter = new DatabaseUtils.InsertHelper(db, sPhotosTable);
-        mIndexPhotosPersonId = mPhotosInserter.getColumnIndex(Photos.PERSON_ID);
-        mIndexPhotosSyncId = mPhotosInserter.getColumnIndex(Photos._SYNC_ID);
-        mIndexPhotosSyncTime = mPhotosInserter.getColumnIndex(Photos._SYNC_TIME);
-        mIndexPhotosSyncVersion = mPhotosInserter.getColumnIndex(Photos._SYNC_VERSION);
-        mIndexPhotosSyncDirty = mPhotosInserter.getColumnIndex(Photos._SYNC_DIRTY);
-        mIndexPhotosSyncAccountName = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT);
-        mIndexPhotosSyncAccountType = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT_TYPE);
-        mIndexPhotosSyncError = mPhotosInserter.getColumnIndex(Photos.SYNC_ERROR);
-        mIndexPhotosExistsOnServer = mPhotosInserter.getColumnIndex(Photos.EXISTS_ON_SERVER);
-
-        mContactMethodsInserter = new DatabaseUtils.InsertHelper(db, sContactMethodsTable);
-        mIndexContactMethodsPersonId =
-                mContactMethodsInserter.getColumnIndex(ContactMethods.PERSON_ID);
-        mIndexContactMethodsLabel = mContactMethodsInserter.getColumnIndex(ContactMethods.LABEL);
-        mIndexContactMethodsKind = mContactMethodsInserter.getColumnIndex(ContactMethods.KIND);
-        mIndexContactMethodsType = mContactMethodsInserter.getColumnIndex(ContactMethods.TYPE);
-        mIndexContactMethodsData = mContactMethodsInserter.getColumnIndex(ContactMethods.DATA);
-        mIndexContactMethodsAuxData =
-                mContactMethodsInserter.getColumnIndex(ContactMethods.AUX_DATA);
-        mIndexContactMethodsIsPrimary =
-                mContactMethodsInserter.getColumnIndex(ContactMethods.ISPRIMARY);
-
-        mOrganizationsInserter = new DatabaseUtils.InsertHelper(db, sOrganizationsTable);
-        mIndexOrganizationsPersonId =
-                mOrganizationsInserter.getColumnIndex(Organizations.PERSON_ID);
-        mIndexOrganizationsLabel = mOrganizationsInserter.getColumnIndex(Organizations.LABEL);
-        mIndexOrganizationsType = mOrganizationsInserter.getColumnIndex(Organizations.TYPE);
-        mIndexOrganizationsCompany = mOrganizationsInserter.getColumnIndex(Organizations.COMPANY);
-        mIndexOrganizationsTitle = mOrganizationsInserter.getColumnIndex(Organizations.TITLE);
-        mIndexOrganizationsIsPrimary =
-                mOrganizationsInserter.getColumnIndex(Organizations.ISPRIMARY);
-
-        mExtensionsInserter = new DatabaseUtils.InsertHelper(db, sExtensionsTable);
-        mIndexExtensionsPersonId = mExtensionsInserter.getColumnIndex(Extensions.PERSON_ID);
-        mIndexExtensionsName = mExtensionsInserter.getColumnIndex(Extensions.NAME);
-        mIndexExtensionsValue = mExtensionsInserter.getColumnIndex(Extensions.VALUE);
-
-        mGroupMembershipInserter = new DatabaseUtils.InsertHelper(db, sGroupmembershipTable);
-        mIndexGroupMembershipPersonId =
-                mGroupMembershipInserter.getColumnIndex(GroupMembership.PERSON_ID);
-        mIndexGroupMembershipGroupSyncAccountName =
-                mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT);
-        mIndexGroupMembershipGroupSyncAccountType =
-                mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
-        mIndexGroupMembershipGroupSyncId =
-                mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ID);
-
-        mCallsInserter = new DatabaseUtils.InsertHelper(db, sCallsTable);
-
-        mPhonesInserter = new DatabaseUtils.InsertHelper(db, sPhonesTable);
-        mIndexPhonesPersonId = mPhonesInserter.getColumnIndex(Phones.PERSON_ID);
-        mIndexPhonesLabel = mPhonesInserter.getColumnIndex(Phones.LABEL);
-        mIndexPhonesType = mPhonesInserter.getColumnIndex(Phones.TYPE);
-        mIndexPhonesNumber = mPhonesInserter.getColumnIndex(Phones.NUMBER);
-        mIndexPhonesNumberKey = mPhonesInserter.getColumnIndex(Phones.NUMBER_KEY);
-        mIndexPhonesIsPrimary = mPhonesInserter.getColumnIndex(Phones.ISPRIMARY);
-    }
-
-    @Override
-    protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
-        boolean upgradeWasLossless = true;
-        if (oldVersion < 71) {
-            Log.w(TAG, "Upgrading database from version " + oldVersion + " to " +
-                    newVersion + ", which will destroy all old data");
-            dropTables(db);
-            bootstrapDatabase(db);
-            return false; // this was lossy
-        }
-        if (oldVersion == 71) {
-            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
-                    newVersion + ", which will preserve existing data");
-
-            db.delete("_sync_state", null, null);
-            mValuesLocal.clear();
-            mValuesLocal.putNull(Photos._SYNC_VERSION);
-            mValuesLocal.putNull(Photos._SYNC_TIME);
-            db.update(sPhotosTable, mValuesLocal, null, null);
-            ContentResolver.requestSync(null /* account */, Contacts.AUTHORITY, new Bundle());
-            oldVersion = 72;
-        }
-        if (oldVersion == 72) {
-            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
-                    newVersion + ", which will preserve existing data");
-
-            // use new token format from 73
-            db.execSQL("delete from peopleLookup");
-            try {
-                DatabaseUtils.longForQuery(db,
-                        "SELECT _TOKENIZE('peopleLookup', _id, name, ' ') from people;",
-                        null);
-            } catch (SQLiteDoneException ex) {
-                // it is ok to throw this,
-                // it just means you don't have data in people table
-            }
-            oldVersion = 73;
-        }
-        // There was a bug for a while in the upgrade logic where going from 72 to 74 would skip
-        // the step from 73 to 74, so 74 to 75 just tries the same steps, and gracefully handles
-        // errors in case the device was started freshly at 74.
-        if (oldVersion == 73 || oldVersion == 74) {
-            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
-                    newVersion + ", which will preserve existing data");
-
-            try {
-                db.execSQL("ALTER TABLE calls ADD name TEXT;");
-                db.execSQL("ALTER TABLE calls ADD numbertype INTEGER;");
-                db.execSQL("ALTER TABLE calls ADD numberlabel TEXT;");
-            } catch (SQLiteException sqle) {
-                // Maybe the table was altered already... Shouldn't be an issue.
-            }
-            oldVersion = 75;
-        }
-        // There were some indices added in version 76
-        if (oldVersion == 75) {
-            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
-                    newVersion + ", which will preserve existing data");
-
-            // add the new indices
-            db.execSQL("CREATE INDEX IF NOT EXISTS groupsSyncDirtyIndex"
-                    + " ON groups (" + Groups._SYNC_DIRTY + ");");
-            db.execSQL("CREATE INDEX IF NOT EXISTS photosSyncDirtyIndex"
-                    + " ON photos (" + Photos._SYNC_DIRTY + ");");
-            db.execSQL("CREATE INDEX IF NOT EXISTS peopleSyncDirtyIndex"
-                    + " ON people (" + People._SYNC_DIRTY + ");");
-            oldVersion = 76;
-        }
-
-        if (oldVersion == 76 || oldVersion == 77) {
-            db.execSQL("DELETE FROM people");
-            db.execSQL("DELETE FROM groups");
-            db.execSQL("DELETE FROM photos");
-            db.execSQL("DELETE FROM _deleted_people");
-            db.execSQL("DELETE FROM _deleted_groups");
-            upgradeWasLossless = false;
-            oldVersion = 78;
-        }
-
-        if (oldVersion == 78) {
-            db.execSQL("UPDATE photos SET _sync_dirty=0 where _sync_dirty is null;");
-            oldVersion = 79;
-        }
-
-        if (oldVersion == 79) {
-            try {
-                db.execSQL("ALTER TABLE people ADD phonetic_name TEXT COLLATE LOCALIZED;");
-            } catch (SQLiteException sqle) {
-                // Maybe the table was altered already... Shouldn't be an issue.
-            }
-            oldVersion = 80;
-        }
-
-        if (oldVersion == 80) {
-
-            // We will not do this upgrade work, because we are deprecating this content provider
-            oldVersion = 81;
-        }
-
-        if (oldVersion == 81) {
-            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
-                    newVersion + ", which will preserve existing data");
-            // 81 adds the token_index column
-            db.execSQL("DELETE FROM peopleLookup");
-            db.execSQL("ALTER TABLE peopleLookup ADD token_index INTEGER;");
-            String[] tokenize = {"_TOKENIZE('peopleLookup', _id, name, ' ', 1)"};
-            Cursor cursor = db.query("people", tokenize, null, null, null, null, null);
-            int rows = cursor.getCount();
-            cursor.close();
-            Log.i(TAG, "Processed " + rows + " contacts.");
-            oldVersion = 82;
-        }
-
-        if (oldVersion == 82) {
-            Log.w(TAG, "Upgrading database from version " + oldVersion + " to " +
-                    newVersion + ", which will preserve all old data");
-
-            try {
-                db.execSQL("ALTER TABLE people ADD COLUMN _sync_account_type TEXT;");
-            } catch (SQLException e) {
-                // in some upgrade paths, these column might already exists
-            }
-            try {
-                db.execSQL("ALTER TABLE _deleted_people ADD COLUMN _sync_account_type TEXT;");
-            } catch (SQLException e) {
-                // in some upgrade paths, these column might already exists
-            }
-            try {
-                db.execSQL("ALTER TABLE groups ADD COLUMN _sync_account_type TEXT;");
-            } catch (SQLException e) {
-                // in some upgrade paths, these column might already exists
-            }
-            try {
-                db.execSQL("ALTER TABLE _deleted_groups ADD COLUMN _sync_account_type TEXT;");
-            } catch (SQLException e) {
-                // in some upgrade paths, these column might already exists
-            }
-            try {
-                db.execSQL("ALTER TABLE settings ADD COLUMN _sync_account_type TEXT;");
-            } catch (SQLException e) {
-                // in some upgrade paths, these column might already exists
-            }
-            try {
-                db.execSQL("ALTER TABLE photos ADD COLUMN _sync_account_type TEXT;");
-            } catch (SQLException e) {
-                // in some upgrade paths, these column might already exists
-            }
-            try {
-                db.execSQL("ALTER TABLE groupmembership ADD COLUMN group_sync_account_type TEXT;");
-            } catch (SQLException e) {
-                // in some upgrade paths, these column might already exists
-            }
-            db.execSQL("UPDATE people"
-                    + " SET _sync_account_type='com.google'"
-                    + " WHERE _sync_account IS NOT NULL");
-            db.execSQL("UPDATE _deleted_people"
-                    + " SET _sync_account_type='com.google'"
-                    + " WHERE _sync_account IS NOT NULL");
-            db.execSQL("UPDATE groups"
-                    + " SET _sync_account_type='com.google'"
-                    + " WHERE _sync_account IS NOT NULL");
-            db.execSQL("UPDATE _deleted_groups"
-                    + " SET _sync_account_type='com.google'"
-                    + " WHERE _sync_account IS NOT NULL");
-            db.execSQL("UPDATE settings"
-                    + " SET _sync_account_type='com.google'"
-                    + " WHERE _sync_account IS NOT NULL");
-            db.execSQL("UPDATE photos"
-                    + " SET _sync_account_type='com.google'"
-                    + " WHERE _sync_account IS NOT NULL");
-            db.execSQL("UPDATE groupmembership"
-                    + " SET group_sync_account_type='com.google'"
-                    + " WHERE group_sync_account IS NOT NULL");
-
-            db.execSQL("CREATE INDEX groupTempIndex ON groups ("
-                    + Groups.NAME + ","  + Groups.SYSTEM_ID + "," + Groups._SYNC_ACCOUNT + ","
-                    + Groups._SYNC_ACCOUNT_TYPE + ");");
-
-            db.execSQL("DROP INDEX groupmembershipIndex3");
-            db.execSQL("CREATE INDEX groupmembershipIndex3 ON groupmembership "
-                    + "(group_sync_account_type, group_sync_account, group_sync_id);");
-
-            // Trigger to move an account_people row to _deleted_account_people when it is deleted
-            db.execSQL("DROP TRIGGER groups_to_deleted");
-            db.execSQL("CREATE TRIGGER groups_to_deleted DELETE ON groups " +
-                        "WHEN old._sync_id is not null " +
-                        "BEGIN " +
-                            "INSERT INTO _deleted_groups " +
-                                "(_sync_id, _sync_account, _sync_account_type, _sync_version) " +
-                                "VALUES (old._sync_id, old._sync_account, old._sync_account_type," +
-                                "old._sync_version);" +
-                        "END");
-
-            oldVersion++;
-        }
-
-        return upgradeWasLossless;
-    }
-
-    protected void dropTables(SQLiteDatabase db) {
-        db.execSQL("DROP TABLE IF EXISTS people");
-        db.execSQL("DROP TABLE IF EXISTS peopleLookup");
-        db.execSQL("DROP TABLE IF EXISTS _deleted_people");
-        db.execSQL("DROP TABLE IF EXISTS phones");
-        db.execSQL("DROP TABLE IF EXISTS contact_methods");
-        db.execSQL("DROP TABLE IF EXISTS calls");
-        db.execSQL("DROP TABLE IF EXISTS organizations");
-        db.execSQL("DROP TABLE IF EXISTS voice_dialer_timestamp");
-        db.execSQL("DROP TABLE IF EXISTS groups");
-        db.execSQL("DROP TABLE IF EXISTS _deleted_groups");
-        db.execSQL("DROP TABLE IF EXISTS groupmembership");
-        db.execSQL("DROP TABLE IF EXISTS photos");
-        db.execSQL("DROP TABLE IF EXISTS extensions");
-        db.execSQL("DROP TABLE IF EXISTS settings");
-    }
-
-    @Override
-    protected void bootstrapDatabase(SQLiteDatabase db) {
-        super.bootstrapDatabase(db);
-        db.execSQL("CREATE TABLE people (" +
-                    People._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    People._SYNC_ACCOUNT + " TEXT," + // From the sync source
-                    People._SYNC_ACCOUNT_TYPE + " TEXT," + // From the sync source
-                    People._SYNC_ID + " TEXT," + // From the sync source
-                    People._SYNC_TIME + " TEXT," + // From the sync source
-                    People._SYNC_VERSION + " TEXT," + // From the sync source
-                    People._SYNC_LOCAL_ID + " INTEGER," + // Used while syncing, not persistent
-                    People._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
-                                                       // if syncable, non-zero if the record
-                                                       // has local, unsynced, changes
-                    People._SYNC_MARK + " INTEGER," + // Used to filter out new rows
-
-                    People.NAME + " TEXT COLLATE LOCALIZED," +
-                    People.NOTES + " TEXT COLLATE LOCALIZED," +
-                    People.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
-                    People.LAST_TIME_CONTACTED + " INTEGER," +
-                    People.STARRED + " INTEGER NOT NULL DEFAULT 0," +
-                    People.PRIMARY_PHONE_ID + " INTEGER REFERENCES phones(_id)," +
-                    People.PRIMARY_ORGANIZATION_ID + " INTEGER REFERENCES organizations(_id)," +
-                    People.PRIMARY_EMAIL_ID + " INTEGER REFERENCES contact_methods(_id)," +
-                    People.PHOTO_VERSION + " TEXT," +
-                    People.CUSTOM_RINGTONE + " TEXT," +
-                    People.SEND_TO_VOICEMAIL + " INTEGER," +
-                    People.PHONETIC_NAME + " TEXT COLLATE LOCALIZED" +
-                    ");");
-
-        db.execSQL("CREATE INDEX peopleNameIndex ON people (" + People.NAME + ");");
-        db.execSQL("CREATE INDEX peopleSyncDirtyIndex ON people (" + People._SYNC_DIRTY + ");");
-        db.execSQL("CREATE INDEX peopleSyncIdIndex ON people (" + People._SYNC_ID + ");");
-
-        db.execSQL("CREATE TRIGGER people_timesContacted UPDATE OF last_time_contacted ON people " +
-                    "BEGIN " +
-                        "UPDATE people SET "
-                            + People.TIMES_CONTACTED + " = (new." + People.TIMES_CONTACTED + " + 1)"
-                            + " WHERE _id = new._id;" +
-                    "END");
-
-        // table of all the groups that exist for an account
-        db.execSQL("CREATE TABLE groups (" +
-                Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                Groups._SYNC_ACCOUNT + " TEXT," + // From the sync source
-                Groups._SYNC_ACCOUNT_TYPE + " TEXT," + // From the sync source
-                Groups._SYNC_ID + " TEXT," + // From the sync source
-                Groups._SYNC_TIME + " TEXT," + // From the sync source
-                Groups._SYNC_VERSION + " TEXT," + // From the sync source
-                Groups._SYNC_LOCAL_ID + " INTEGER," + // Used while syncing, not persistent
-                Groups._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
-                                                          // if syncable, non-zero if the record
-                                                          // has local, unsynced, changes
-                Groups._SYNC_MARK + " INTEGER," + // Used to filter out new rows
-
-                Groups.NAME + " TEXT NOT NULL," +
-                Groups.NOTES + " TEXT," +
-                Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 0," +
-                Groups.SYSTEM_ID + " TEXT," +
-                "UNIQUE(" +
-                Groups.NAME + ","  + Groups.SYSTEM_ID + "," + Groups._SYNC_ACCOUNT + "," +
-                Groups._SYNC_ACCOUNT_TYPE + ")" +
-                ");");
-
-        db.execSQL("CREATE INDEX groupsSyncDirtyIndex ON groups (" + Groups._SYNC_DIRTY + ");");
-
-        if (!isTemporary()) {
-            // Add the system groups, since we always need them.
-            db.execSQL("INSERT INTO groups (" + Groups.NAME + ", " + Groups.SYSTEM_ID + ") VALUES "
-                    + "('" + Groups.GROUP_MY_CONTACTS + "', '" + Groups.GROUP_MY_CONTACTS + "')");
-        }
-
-        db.execSQL("CREATE TABLE peopleLookup (" +
-                    "token TEXT," +
-                    "source INTEGER REFERENCES people(_id)," +
-                    "token_index INTEGER"+
-                    ");");
-        db.execSQL("CREATE INDEX peopleLookupIndex ON peopleLookup (" +
-                    "token," +
-                    "source" +
-                    ");");
-
-        db.execSQL("CREATE TABLE photos ("
-                + Photos._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
-                + Photos.EXISTS_ON_SERVER + " INTEGER NOT NULL DEFAULT 0,"
-                + Photos.PERSON_ID + " INTEGER REFERENCES people(_id), "
-                + Photos.LOCAL_VERSION + " TEXT,"
-                + Photos.DATA + " BLOB,"
-                + Photos.SYNC_ERROR + " TEXT,"
-                + Photos._SYNC_ACCOUNT + " TEXT,"
-                + Photos._SYNC_ACCOUNT_TYPE + " TEXT,"
-                + Photos._SYNC_ID + " TEXT,"
-                + Photos._SYNC_TIME + " TEXT,"
-                + Photos._SYNC_VERSION + " TEXT,"
-                + Photos._SYNC_LOCAL_ID + " INTEGER,"
-                + Photos._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0,"
-                + Photos._SYNC_MARK + " INTEGER,"
-                + "UNIQUE(" + Photos.PERSON_ID + ") "
-                + ")");
-
-        db.execSQL("CREATE INDEX photosSyncDirtyIndex ON photos (" + Photos._SYNC_DIRTY + ");");
-        db.execSQL("CREATE INDEX photoPersonIndex ON photos (person);");
-
-        // Delete the photo row when the people row is deleted
-        db.execSQL(""
-                + " CREATE TRIGGER peopleDeleteAndPhotos DELETE ON people "
-                + " BEGIN"
-                + "   DELETE FROM photos WHERE person=OLD._id;"
-                + " END");
-
-        db.execSQL("CREATE TABLE _deleted_people (" +
-                    "_sync_version TEXT," + // From the sync source
-                    "_sync_id TEXT," +
-                    (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
-                    "_sync_account TEXT," +
-                    "_sync_account_type TEXT," +
-                    "_sync_mark INTEGER)"); // Used to filter out new rows
-
-        db.execSQL("CREATE TABLE _deleted_groups (" +
-                    "_sync_version TEXT," + // From the sync source
-                    "_sync_id TEXT," +
-                    (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
-                    "_sync_account TEXT," +
-                    "_sync_account_type TEXT," +
-                    "_sync_mark INTEGER)"); // Used to filter out new rows
-
-        db.execSQL("CREATE TABLE phones (" +
-                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    "person INTEGER REFERENCES people(_id)," +
-                    "type INTEGER NOT NULL," + // kind specific (home, work, etc)
-                    "number TEXT," +
-                    "number_key TEXT," +
-                    "label TEXT," +
-                    "isprimary INTEGER NOT NULL DEFAULT 0" +
-                    ");");
-        db.execSQL("CREATE INDEX phonesIndex1 ON phones (person);");
-        db.execSQL("CREATE INDEX phonesIndex2 ON phones (number_key);");
-
-        db.execSQL("CREATE TABLE contact_methods (" +
-                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    "person INTEGER REFERENCES people(_id)," +
-                    "kind INTEGER NOT NULL," + // the kind of contact method
-                    "data TEXT," +
-                    "aux_data TEXT," +
-                    "type INTEGER NOT NULL," + // kind specific (home, work, etc)
-                    "label TEXT," +
-                    "isprimary INTEGER NOT NULL DEFAULT 0" +
-                    ");");
-        db.execSQL("CREATE INDEX contactMethodsPeopleIndex "
-                + "ON contact_methods (person);");
-
-        // The table for recent calls is here so we can do table joins
-        // on people, phones, and calls all in one place.
-        db.execSQL("CREATE TABLE calls (" +
-                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    "number TEXT," +
-                    "date INTEGER," +
-                    "duration INTEGER," +
-                    "type INTEGER," +
-                    "new INTEGER," +
-                    "name TEXT," +
-                    "numbertype INTEGER," +
-                    "numberlabel TEXT" +
-                    ");");
-
-        // Various settings for the contacts sync adapter. The _sync_account column may
-        // be null, but it must not be the empty string.
-        db.execSQL("CREATE TABLE settings (" +
-                    "_id INTEGER PRIMARY KEY," +
-                    "_sync_account TEXT," +
-                    "_sync_account_type TEXT," +
-                    "key STRING NOT NULL," +
-                    "value STRING " +
-                    ");");
-
-        // The table for the organizations of a person.
-        db.execSQL("CREATE TABLE organizations (" +
-                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    "company TEXT," +
-                    "title TEXT," +
-                    "isprimary INTEGER NOT NULL DEFAULT 0," +
-                    "type INTEGER NOT NULL," + // kind specific (home, work, etc)
-                    "label TEXT," +
-                    "person INTEGER REFERENCES people(_id)" +
-                    ");");
-        db.execSQL("CREATE INDEX organizationsIndex1 ON organizations (person);");
-
-        // The table for the extensions of a person.
-        db.execSQL("CREATE TABLE extensions (" +
-                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    "name TEXT NOT NULL," +
-                    "value TEXT NOT NULL," +
-                    "person INTEGER REFERENCES people(_id)," +
-                    "UNIQUE(person, name)" +
-                    ");");
-        db.execSQL("CREATE INDEX extensionsIndex1 ON extensions (person, name);");
-
-        // The table for the groups of a person.
-        db.execSQL("CREATE TABLE groupmembership (" +
-                "_id INTEGER PRIMARY KEY," +
-                "person INTEGER REFERENCES people(_id)," +
-                "group_id INTEGER REFERENCES groups(_id)," +
-                "group_sync_account STRING," +
-                "group_sync_account_type STRING," +
-                "group_sync_id STRING" +
-                ");");
-        db.execSQL("CREATE INDEX groupmembershipIndex1 ON groupmembership (person, group_id);");
-        db.execSQL("CREATE INDEX groupmembershipIndex2 ON groupmembership (group_id, person);");
-        db.execSQL("CREATE INDEX groupmembershipIndex3 ON groupmembership "
-                + "(group_sync_account, group_sync_account_type, group_sync_id);");
-
-        // Trigger to completely remove a contacts data when they're deleted
-        db.execSQL("CREATE TRIGGER contact_cleanup DELETE ON people " +
-                    "BEGIN " +
-                        "DELETE FROM peopleLookup WHERE source = old._id;" +
-                        "DELETE FROM phones WHERE person = old._id;" +
-                        "DELETE FROM contact_methods WHERE person = old._id;" +
-                        "DELETE FROM organizations WHERE person = old._id;" +
-                        "DELETE FROM groupmembership WHERE person = old._id;" +
-                        "DELETE FROM extensions WHERE person = old._id;" +
-                    "END");
-
-        // Trigger to disassociate the groupmembership from the groups when an
-        // groups entry is deleted
-        db.execSQL("CREATE TRIGGER groups_cleanup DELETE ON groups " +
-                    "BEGIN " +
-                        "UPDATE groupmembership SET group_id = null WHERE group_id = old._id;" +
-                    "END");
-
-        // Trigger to move an account_people row to _deleted_account_people when it is deleted
-        db.execSQL("CREATE TRIGGER groups_to_deleted DELETE ON groups " +
-                    "WHEN old._sync_id is not null " +
-                    "BEGIN " +
-                        "INSERT INTO _deleted_groups " +
-                            "(_sync_id, _sync_account, _sync_account_type, _sync_version) " +
-                            "VALUES (old._sync_id, old._sync_account, old._sync_account_type, " +
-                            "old._sync_version);" +
-                    "END");
-
-        // Triggers to keep the peopleLookup table up to date
-        db.execSQL("CREATE TRIGGER peopleLookup_update UPDATE OF name ON people " +
-                    "BEGIN " +
-                        "DELETE FROM peopleLookup WHERE source = new._id;" +
-                        "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
-                    "END");
-        db.execSQL("CREATE TRIGGER peopleLookup_insert AFTER INSERT ON people " +
-                    "BEGIN " +
-                        "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
-                    "END");
-
-        // Triggers to set the _sync_dirty flag when a phone is changed,
-        // inserted or deleted
-        db.execSQL("CREATE TRIGGER phones_update UPDATE ON phones " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
-                    "END");
-        db.execSQL("CREATE TRIGGER phones_insert INSERT ON phones " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person;" +
-                    "END");
-        db.execSQL("CREATE TRIGGER phones_delete DELETE ON phones " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
-                    "END");
-
-        // Triggers to set the _sync_dirty flag when a contact_method is
-        // changed, inserted or deleted
-        db.execSQL("CREATE TRIGGER contact_methods_update UPDATE ON contact_methods " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
-                    "END");
-        db.execSQL("CREATE TRIGGER contact_methods_insert INSERT ON contact_methods " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person;" +
-                    "END");
-        db.execSQL("CREATE TRIGGER contact_methods_delete DELETE ON contact_methods " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
-                    "END");
-
-        // Triggers for when an organization is changed, inserted or deleted
-        db.execSQL("CREATE TRIGGER organizations_update AFTER UPDATE ON organizations " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " +
-                    "END");
-        db.execSQL("CREATE TRIGGER organizations_insert INSERT ON organizations " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " +
-                    "END");
-        db.execSQL("CREATE TRIGGER organizations_delete DELETE ON organizations " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
-                    "END");
-
-        // Triggers for when an groupmembership is changed, inserted or deleted
-        db.execSQL("CREATE TRIGGER groupmembership_update AFTER UPDATE ON groupmembership " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " +
-                    "END");
-        db.execSQL("CREATE TRIGGER groupmembership_insert INSERT ON groupmembership " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " +
-                    "END");
-        db.execSQL("CREATE TRIGGER groupmembership_delete DELETE ON groupmembership " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
-                    "END");
-
-        // Triggers for when an extension is changed, inserted or deleted
-        db.execSQL("CREATE TRIGGER extensions_update AFTER UPDATE ON extensions " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " +
-                    "END");
-        db.execSQL("CREATE TRIGGER extensions_insert INSERT ON extensions " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " +
-                    "END");
-        db.execSQL("CREATE TRIGGER extensions_delete DELETE ON extensions " +
-                    "BEGIN " +
-                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
-                    "END");
-
-        createTypeLabelTrigger(db, sPhonesTable, "INSERT");
-        createTypeLabelTrigger(db, sPhonesTable, "UPDATE");
-        createTypeLabelTrigger(db, sOrganizationsTable, "INSERT");
-        createTypeLabelTrigger(db, sOrganizationsTable, "UPDATE");
-        createTypeLabelTrigger(db, sContactMethodsTable, "INSERT");
-        createTypeLabelTrigger(db, sContactMethodsTable, "UPDATE");
-
-        // Temporary table that holds a time stamp of the last time data the voice
-        // dialer is interested in has changed so the grammar won't need to be
-        // recompiled when unused data is changed.
-        db.execSQL("CREATE TABLE voice_dialer_timestamp (" +
-                   "_id INTEGER PRIMARY KEY," +
-                   "timestamp INTEGER" +
-                   ");");
-        db.execSQL("INSERT INTO voice_dialer_timestamp (_id, timestamp) VALUES " +
-                       "(1, strftime('%s', 'now'));");
-        db.execSQL("CREATE TRIGGER timestamp_trigger1 AFTER UPDATE ON phones " +
-                   "BEGIN " +
-                       "UPDATE voice_dialer_timestamp SET timestamp=strftime('%s', 'now') "+
-                           "WHERE _id=1;" +
-                   "END");
-        db.execSQL("CREATE TRIGGER timestamp_trigger2 AFTER UPDATE OF name ON people " +
-                   "BEGIN " +
-                       "UPDATE voice_dialer_timestamp SET timestamp=strftime('%s', 'now') " +
-                           "WHERE _id=1;" +
-                   "END");
-    }
-
-    private void createTypeLabelTrigger(SQLiteDatabase db, String table, String operation) {
-        final String name = table + "_" + operation + "_typeAndLabel";
-        db.execSQL("CREATE TRIGGER " + name + " AFTER " + operation + " ON " + table
-                + "   WHEN (NEW.type != 0 AND NEW.label IS NOT NULL) OR "
-                + "        (NEW.type = 0 AND NEW.label IS NULL)"
-                + "   BEGIN "
-                + "     SELECT RAISE (ABORT, 'exactly one of type or label must be set'); "
-                + "   END");
-    }
-
-    private void maybeCreatePresenceTable(SQLiteDatabase db) {
-        // Load the presence table from the presence_db. Just create the table
-        // if we are
-        String cpDbName;
-        if (!isTemporary()) {
-            db.execSQL("ATTACH DATABASE ':memory:' AS presence_db;");
-            cpDbName = "presence_db.";
-        } else {
-            cpDbName = "";
-        }
-        db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + "presence ("+
-                    Presence._ID + " INTEGER PRIMARY KEY," +
-                    Presence.PERSON_ID + " INTEGER REFERENCES people(_id)," +
-                    Presence.IM_PROTOCOL + " TEXT," +
-                    Presence.IM_HANDLE + " TEXT," +
-                    Presence.IM_ACCOUNT + " TEXT," +
-                    Presence.PRESENCE_STATUS + " INTEGER," +
-                    Presence.PRESENCE_CUSTOM_STATUS + " TEXT," +
-                    "UNIQUE(" + Presence.IM_PROTOCOL + ", " + Presence.IM_HANDLE + ", "
-                            + Presence.IM_ACCOUNT + ")" +
-                    ");");
-
-        db.execSQL("CREATE INDEX IF NOT EXISTS " + cpDbName + "presenceIndex ON presence ("
-                + Presence.PERSON_ID + ");");
-    }
-
-    @SuppressWarnings("deprecation")
-    private String buildPeopleLookupWhereClause(String filterParam) {
-        StringBuilder filter = new StringBuilder(
-                "people._id IN (SELECT source FROM peopleLookup WHERE token GLOB ");
-        // NOTE: Query parameters won't work here since the SQL compiler
-        // needs to parse the actual string to know that it can use the
-        // index to do a prefix scan.
-        DatabaseUtils.appendEscapedSQLString(filter,
-                DatabaseUtils.getHexCollationKey(filterParam) + "*");
-        filter.append(')');
-        return filter.toString();
-    }
-
-    @Override
-    public Cursor queryInternal(Uri url, String[] projectionIn,
-            String selection, String[] selectionArgs, String sort) {
-
-        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        Uri notificationUri = Contacts.CONTENT_URI;
-        String limit = getLimit(url);
-        StringBuilder whereClause;
-        String groupBy = null;
-
-        // Generate the body of the query
-        int match = sURIMatcher.match(url);
-
-        if (Config.LOGV) Log.v(TAG, "ContactsProvider.query: url=" + url + ", match is " + match);
-
-        switch (match) {
-            case DELETED_GROUPS:
-                if (!isTemporary()) {
-                    throw new UnsupportedOperationException();
-                }
-
-                qb.setTables(sDeletedGroupsTable);
-                break;
-
-            case GROUPS_ID:
-                qb.appendWhere("_id=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                // fall through
-            case GROUPS:
-                qb.setTables(sGroupsTable);
-                qb.setProjectionMap(sGroupsProjectionMap);
-                break;
-
-            case SETTINGS:
-                qb.setTables(sSettingsTable);
-                break;
-
-            case PEOPLE_GROUPMEMBERSHIP_ID:
-                qb.appendWhere("groupmembership._id=");
-                qb.appendWhere(url.getPathSegments().get(3));
-                qb.appendWhere(" AND ");
-                // fall through
-            case PEOPLE_GROUPMEMBERSHIP:
-                qb.appendWhere(sGroupsJoinString + " AND ");
-                qb.appendWhere("person=" + url.getPathSegments().get(1));
-                qb.setTables("groups, groupmembership");
-                qb.setProjectionMap(sGroupMembershipProjectionMap);
-                break;
-
-            case GROUPMEMBERSHIP_ID:
-                qb.appendWhere("groupmembership._id=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                qb.appendWhere(" AND ");
-                // fall through
-            case GROUPMEMBERSHIP:
-                qb.setTables("groups, groupmembership");
-                qb.setProjectionMap(sGroupMembershipProjectionMap);
-                qb.appendWhere(sGroupsJoinString);
-                break;
-
-            case GROUPMEMBERSHIP_RAW:
-                qb.setTables("groupmembership");
-                break;
-
-            case GROUP_NAME_MEMBERS_FILTER:
-                if (url.getPathSegments().size() > 5) {
-                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
-                    qb.appendWhere(" AND ");
-                }
-                // fall through
-            case GROUP_NAME_MEMBERS:
-                qb.setTables(PEOPLE_PHONES_JOIN);
-                qb.setProjectionMap(sPeopleProjectionMap);
-                qb.appendWhere(buildGroupNameMatchWhereClause(url.getPathSegments().get(2)));
-                break;
-
-            case GROUP_SYSTEM_ID_MEMBERS_FILTER:
-                if (url.getPathSegments().size() > 5) {
-                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
-                    qb.appendWhere(" AND ");
-                }
-                // fall through
-            case GROUP_SYSTEM_ID_MEMBERS:
-                qb.setTables(PEOPLE_PHONES_JOIN);
-                qb.setProjectionMap(sPeopleProjectionMap);
-                qb.appendWhere(buildGroupSystemIdMatchWhereClause(url.getPathSegments().get(2)));
-                break;
-
-            case PEOPLE:
-                qb.setTables(PEOPLE_PHONES_JOIN);
-                qb.setProjectionMap(sPeopleProjectionMap);
-                break;
-            case PEOPLE_RAW:
-                qb.setTables(sPeopleTable);
-                break;
-
-            case PEOPLE_OWNER:
-                return queryOwner(projectionIn);
-
-            case PEOPLE_WITH_PHONES_FILTER:
-
-                qb.appendWhere("number IS NOT NULL AND ");
-
-                // Fall through.
-
-            case PEOPLE_FILTER: {
-                qb.setTables(PEOPLE_PHONES_JOIN);
-                qb.setProjectionMap(sPeopleProjectionMap);
-                if (url.getPathSegments().size() > 2) {
-                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
-                }
-                break;
-            }
-
-            case PEOPLE_WITH_EMAIL_OR_IM_FILTER:
-                String email = url.getPathSegments().get(2);
-                whereClause = new StringBuilder();
-
-                // Match any E-mail or IM contact methods where data exactly
-                // matches the provided string.
-                whereClause.append(ContactMethods.DATA);
-                whereClause.append("=");
-                DatabaseUtils.appendEscapedSQLString(whereClause, email);
-                whereClause.append(" AND (kind = " + Contacts.KIND_EMAIL +
-                        " OR kind = " + Contacts.KIND_IM + ")");
-                qb.appendWhere(whereClause.toString());
-
-                qb.setTables("people INNER JOIN contact_methods on (people._id = contact_methods.person)");
-                qb.setProjectionMap(sPeopleWithEmailOrImProjectionMap);
-
-                // Prevent returning the same person for multiple matches
-                groupBy = "contact_methods.person";
-
-                qb.setDistinct(true);
-                break;
-
-            case PHOTOS_ID:
-                qb.appendWhere("_id="+url.getPathSegments().get(1));
-                // Fall through.
-            case PHOTOS:
-                qb.setTables(sPhotosTable);
-                qb.setProjectionMap(sPhotosProjectionMap);
-                break;
-
-            case PEOPLE_PHOTO:
-                qb.appendWhere("person="+url.getPathSegments().get(1));
-                qb.setTables(sPhotosTable);
-                qb.setProjectionMap(sPhotosProjectionMap);
-                break;
-
-            case SEARCH_SUGGESTIONS: {
-                // Force the default sort order, since the SearchManage doesn't ask for things
-                // sorted, though they should be
-                if (sort != null && !People.DEFAULT_SORT_ORDER.equals(sort)) {
-                    throw new IllegalArgumentException("Sort ordering not allowed for this URI");
-                }
-                sort = SearchManager.SUGGEST_COLUMN_TEXT_1 + " COLLATE LOCALIZED ASC";
-
-                // This will either setup the query builder so we can run the proper query below
-                // and return null, or it will return a cursor with the results already in it.
-                Cursor c = handleSearchSuggestionsQuery(url, qb);
-                if (c != null) {
-                    return c;
-                }
-                break;
-            }
-            case SEARCH_SHORTCUT: {
-                qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN);
-                qb.setProjectionMap(sSearchSuggestionsProjectionMap);
-                qb.appendWhere(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID + "=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                break;
-            }
-            case PEOPLE_STREQUENT: {
-                // Build the first query for starred
-                qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
-                qb.setProjectionMap(sStrequentStarredProjectionMap);
-                final String starredQuery = qb.buildQuery(projectionIn, "starred = 1",
-                        null, null, null, null,
-                        null /* limit */);
-
-                // Build the second query for frequent
-                qb = new SQLiteQueryBuilder();
-                qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
-                qb.setProjectionMap(sPeopleWithPhotoProjectionMap);
-                final String frequentQuery = qb.buildQuery(projectionIn,
-                        "times_contacted > 0 AND starred = 0", null, null, null, null, null);
-
-                // Put them together
-                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
-                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
-                final SQLiteDatabase db = getDatabase();
-                Cursor c = db.rawQueryWithFactory(null, query, null, sPeopleTable);
-                if ((c != null) && !isTemporary()) {
-                    c.setNotificationUri(getContext().getContentResolver(), notificationUri);
-                }
-                return c;
-            }
-            case PEOPLE_STREQUENT_FILTER: {
-                // Build the first query for starred
-                qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
-                qb.setProjectionMap(sStrequentStarredProjectionMap);
-                if (url.getPathSegments().size() > 3) {
-                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
-                }
-                final String starredQuery = qb.buildQuery(projectionIn, "starred = 1",
-                        null, null, null, null,
-                        null /* limit */);
-
-                // Build the second query for frequent
-                qb = new SQLiteQueryBuilder();
-                qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
-                qb.setProjectionMap(sPeopleWithPhotoProjectionMap);
-                if (url.getPathSegments().size() > 3) {
-                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
-                }
-                final String frequentQuery = qb.buildQuery(projectionIn,
-                        "times_contacted > 0 AND starred = 0", null, null, null, null, null);
-
-                // Put them together
-                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
-                        STREQUENT_ORDER_BY, null);
-                final SQLiteDatabase db = getDatabase();
-                Cursor c = db.rawQueryWithFactory(null, query, null, sPeopleTable);
-                if ((c != null) && !isTemporary()) {
-                    c.setNotificationUri(getContext().getContentResolver(), notificationUri);
-                }
-                return c;
-            }
-            case DELETED_PEOPLE:
-                if (isTemporary()) {
-                    qb.setTables("_deleted_people");
-                    break;
-                }
-                throw new UnsupportedOperationException();
-            case PEOPLE_ID:
-                qb.setTables("people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
-                        + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID
-                        + "=people._id)");
-                qb.setProjectionMap(sPeopleProjectionMap);
-                qb.appendWhere("people._id=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                break;
-            case PEOPLE_PHONES:
-                qb.setTables("phones, people");
-                qb.setProjectionMap(sPhonesProjectionMap);
-                qb.appendWhere("people._id = phones.person AND person=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                break;
-            case PEOPLE_PHONES_ID:
-                qb.setTables("phones, people");
-                qb.setProjectionMap(sPhonesProjectionMap);
-                qb.appendWhere("people._id = phones.person AND person=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                qb.appendWhere(" AND phones._id=");
-                qb.appendWhere(url.getPathSegments().get(3));
-                break;
-
-            case PEOPLE_PHONES_WITH_PRESENCE:
-                qb.appendWhere("people._id=?");
-                selectionArgs = appendSelectionArg(selectionArgs, url.getPathSegments().get(1));
-                // Fall through.
-
-            case PHONES_WITH_PRESENCE:
-                qb.setTables("phones JOIN people ON (phones.person = people._id)"
-                        + " LEFT OUTER JOIN presence ON (presence.person = people._id)");
-                qb.setProjectionMap(sPhonesWithPresenceProjectionMap);
-                break;
-
-            case PEOPLE_CONTACTMETHODS:
-                qb.setTables("contact_methods, people");
-                qb.setProjectionMap(sContactMethodsProjectionMap);
-                qb.appendWhere("people._id = contact_methods.person AND person=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                break;
-            case PEOPLE_CONTACTMETHODS_ID:
-                qb.setTables("contact_methods, people");
-                qb.setProjectionMap(sContactMethodsProjectionMap);
-                qb.appendWhere("people._id = contact_methods.person AND person=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                qb.appendWhere(" AND contact_methods._id=");
-                qb.appendWhere(url.getPathSegments().get(3));
-                break;
-            case PEOPLE_ORGANIZATIONS:
-                qb.setTables("organizations, people");
-                qb.setProjectionMap(sOrganizationsProjectionMap);
-                qb.appendWhere("people._id = organizations.person AND person=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                break;
-            case PEOPLE_ORGANIZATIONS_ID:
-                qb.setTables("organizations, people");
-                qb.setProjectionMap(sOrganizationsProjectionMap);
-                qb.appendWhere("people._id = organizations.person AND person=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                qb.appendWhere(" AND organizations._id=");
-                qb.appendWhere(url.getPathSegments().get(3));
-                break;
-            case PHONES:
-                qb.setTables("phones, people");
-                qb.appendWhere("people._id = phones.person");
-                qb.setProjectionMap(sPhonesProjectionMap);
-                break;
-            case PHONES_ID:
-                qb.setTables("phones, people");
-                qb.appendWhere("people._id = phones.person AND phones._id="
-                        + url.getPathSegments().get(1));
-                qb.setProjectionMap(sPhonesProjectionMap);
-                break;
-            case ORGANIZATIONS:
-                qb.setTables("organizations, people");
-                qb.appendWhere("people._id = organizations.person");
-                qb.setProjectionMap(sOrganizationsProjectionMap);
-                break;
-            case ORGANIZATIONS_ID:
-                qb.setTables("organizations, people");
-                qb.appendWhere("people._id = organizations.person AND organizations._id="
-                        + url.getPathSegments().get(1));
-                qb.setProjectionMap(sOrganizationsProjectionMap);
-                break;
-            case PHONES_MOBILE_FILTER_NAME:
-                qb.appendWhere("type=" + Contacts.PhonesColumns.TYPE_MOBILE + " AND ");
-
-                // Fall through.
-
-            case PHONES_FILTER_NAME:
-                qb.setTables("phones JOIN people ON (people._id = phones.person)");
-                qb.setProjectionMap(sPhonesProjectionMap);
-                if (url.getPathSegments().size() > 2) {
-                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
-                }
-                break;
-
-            case PHONES_FILTER: {
-                String phoneNumber = url.getPathSegments().get(2);
-                String indexable = PhoneNumberUtils.toCallerIDMinMatch(phoneNumber);
-                StringBuilder subQuery = new StringBuilder();
-                if (TextUtils.isEmpty(sort)) {
-                    // Default the sort order to something reasonable so we get consistent
-                    // results when callers don't request an ordering
-                    sort = People.DEFAULT_SORT_ORDER;
-                }
-
-                subQuery.append("people, (SELECT * FROM phones WHERE (phones.number_key GLOB '");
-                subQuery.append(indexable);
-                subQuery.append("*')) AS phones");
-                qb.setTables(subQuery.toString());
-                qb.appendWhere("phones.person=people._id AND PHONE_NUMBERS_EQUAL(phones.number, ");
-                qb.appendWhereEscapeString(phoneNumber);
-                qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
-                qb.setProjectionMap(sPhonesProjectionMap);
-                break;
-            }
-            case CONTACTMETHODS:
-                qb.setTables("contact_methods, people");
-                qb.setProjectionMap(sContactMethodsProjectionMap);
-                qb.appendWhere("people._id = contact_methods.person");
-                break;
-            case CONTACTMETHODS_ID:
-                qb.setTables("contact_methods LEFT OUTER JOIN people ON contact_methods.person = people._id");
-                qb.setProjectionMap(sContactMethodsProjectionMap);
-                qb.appendWhere("contact_methods._id=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                break;
-            case CONTACTMETHODS_EMAIL_FILTER:
-                String pattern = url.getPathSegments().get(2);
-                whereClause = new StringBuilder();
-
-                // TODO This is going to be REALLY slow.  Come up with
-                // something faster.
-                whereClause.append(ContactMethods.KIND);
-                whereClause.append('=');
-                whereClause.append('\'');
-                whereClause.append(Contacts.KIND_EMAIL);
-                whereClause.append("' AND (UPPER(");
-                whereClause.append(ContactMethods.NAME);
-                whereClause.append(") GLOB ");
-                DatabaseUtils.appendEscapedSQLString(whereClause, pattern + "*");
-                whereClause.append(" OR UPPER(");
-                whereClause.append(ContactMethods.NAME);
-                whereClause.append(") GLOB ");
-                DatabaseUtils.appendEscapedSQLString(whereClause, "* " + pattern + "*");
-                whereClause.append(") AND ");
-                qb.appendWhere(whereClause.toString());
-
-                // Fall through.
-
-            case CONTACTMETHODS_EMAIL:
-                qb.setTables("contact_methods INNER JOIN people on (contact_methods.person = people._id)");
-                qb.setProjectionMap(sEmailSearchProjectionMap);
-                qb.appendWhere("kind = " + Contacts.KIND_EMAIL);
-                qb.setDistinct(true);
-                break;
-
-            case PEOPLE_CONTACTMETHODS_WITH_PRESENCE:
-                qb.appendWhere("people._id=?");
-                selectionArgs = appendSelectionArg(selectionArgs, url.getPathSegments().get(1));
-                // Fall through.
-
-            case CONTACTMETHODS_WITH_PRESENCE:
-                qb.setTables("contact_methods JOIN people ON (contact_methods.person = people._id)"
-                        + " LEFT OUTER JOIN presence ON "
-                        // Match gtalk presence items
-                        + "((kind=" + Contacts.KIND_EMAIL +
-                            " AND im_protocol='"
-                            + ContactMethods.encodePredefinedImProtocol(
-                                    ContactMethods.PROTOCOL_GOOGLE_TALK)
-                            + "' AND data=im_handle)"
-                        + " OR "
-                        // Match IM presence items
-                        + "(kind=" + Contacts.KIND_IM
-                            + " AND data=im_handle AND aux_data=im_protocol))");
-                qb.setProjectionMap(sContactMethodsWithPresenceProjectionMap);
-                break;
-
-            case CALLS:
-                qb.setTables("calls");
-                qb.setProjectionMap(sCallsProjectionMap);
-                notificationUri = CallLog.CONTENT_URI;
-                break;
-            case CALLS_ID:
-                qb.setTables("calls");
-                qb.setProjectionMap(sCallsProjectionMap);
-                qb.appendWhere("calls._id=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                notificationUri = CallLog.CONTENT_URI;
-                break;
-            case CALLS_FILTER: {
-                qb.setTables("calls");
-                qb.setProjectionMap(sCallsProjectionMap);
-
-                String phoneNumber = url.getPathSegments().get(2);
-                qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
-                qb.appendWhereEscapeString(phoneNumber);
-                qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
-                notificationUri = CallLog.CONTENT_URI;
-                break;
-            }
-
-            case PRESENCE:
-                qb.setTables("presence LEFT OUTER JOIN people on (presence." + Presence.PERSON_ID
-                        + "= people._id)");
-                qb.setProjectionMap(sPresenceProjectionMap);
-                break;
-            case PRESENCE_ID:
-                qb.setTables("presence LEFT OUTER JOIN people on (presence." + Presence.PERSON_ID
-                        + "= people._id)");
-                qb.appendWhere("presence._id=");
-                qb.appendWhere(url.getLastPathSegment());
-                break;
-            case VOICE_DIALER_TIMESTAMP:
-                qb.setTables("voice_dialer_timestamp");
-                qb.appendWhere("_id=1");
-                break;
-
-            case PEOPLE_EXTENSIONS_ID:
-                qb.appendWhere("extensions._id=" + url.getPathSegments().get(3) + " AND ");
-                // fall through
-            case PEOPLE_EXTENSIONS:
-                qb.appendWhere("person=" + url.getPathSegments().get(1));
-                qb.setTables(sExtensionsTable);
-                qb.setProjectionMap(sExtensionsProjectionMap);
-                break;
-
-            case EXTENSIONS_ID:
-                qb.appendWhere("extensions._id=" + url.getPathSegments().get(1));
-                // fall through
-            case EXTENSIONS:
-                qb.setTables(sExtensionsTable);
-                qb.setProjectionMap(sExtensionsProjectionMap);
-                break;
-
-            case LIVE_FOLDERS_PEOPLE:
-                qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
-                qb.setProjectionMap(sLiveFoldersProjectionMap);
-                break;
-
-            case LIVE_FOLDERS_PEOPLE_WITH_PHONES:
-                qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
-                qb.setProjectionMap(sLiveFoldersProjectionMap);
-                qb.appendWhere(People.PRIMARY_PHONE_ID + " IS NOT NULL");
-                break;
-
-            case LIVE_FOLDERS_PEOPLE_FAVORITES:
-                qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
-                qb.setProjectionMap(sLiveFoldersProjectionMap);
-                qb.appendWhere(People.STARRED + " <> 0");
-                break;
-
-            case LIVE_FOLDERS_PEOPLE_GROUP_NAME:
-                qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
-                qb.setProjectionMap(sLiveFoldersProjectionMap);
-                qb.appendWhere(buildGroupNameMatchWhereClause(url.getLastPathSegment()));
-                break;
-
-            default:
-                throw new IllegalArgumentException("Unknown URL " + url);
-        }
-
-        // run the query
-        final SQLiteDatabase db = getDatabase();
-        Cursor c = qb.query(db, projectionIn, selection, selectionArgs,
-                groupBy, null, sort, limit);
-        if ((c != null) && !isTemporary()) {
-            c.setNotificationUri(getContext().getContentResolver(), notificationUri);
-        }
-        return c;
-    }
-
-    /**
-     * Gets the value of the "limit" URI query parameter.
-     *
-     * @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 limit = url.getQueryParameter("limit");
-        if (limit == null) {
-            return null;
-        }
-        // make sure that the limit is a non-negative integer
-        try {
-            int l = Integer.parseInt(limit);
-            if (l < 0) {
-                Log.w(TAG, "Invalid limit parameter: " + limit);
-                return null;
-            }
-            return String.valueOf(l);
-        } catch (NumberFormatException ex) {
-            Log.w(TAG, "Invalid limit parameter: " + limit);
-            return null;
-        }
-    }
-
-    /**
-     * Build a WHERE clause that restricts the query to match people that are a member of
-     * a particular system group.  The projection map of the query must include {@link People#_ID}.
-     *
-     * @param groupSystemId The system group id (e.g {@link Groups#GROUP_MY_CONTACTS})
-     * @return The where clause.
-     */
-    private CharSequence buildGroupSystemIdMatchWhereClause(String groupSystemId) {
-        return "people._id IN (SELECT person FROM groupmembership JOIN groups " +
-                "ON (group_id=groups._id OR " +
-                "(group_sync_id = groups._sync_id AND " +
-                    "group_sync_account = groups._sync_account)) "+
-                "WHERE " + Groups.SYSTEM_ID + "="
-                + DatabaseUtils.sqlEscapeString(groupSystemId) + ")";
-    }
-
-    /**
-     * Build a WHERE clause that restricts the query to match people that are a member of
-     * a group with a particular name. The projection map of the query must include
-     * {@link People#_ID}.
-     *
-     * @param groupName The name of the group
-     * @return The where clause.
-     */
-    private CharSequence buildGroupNameMatchWhereClause(String groupName) {
-        return "people._id IN (SELECT person FROM groupmembership JOIN groups " +
-                "ON (group_id=groups._id OR " +
-                "(group_sync_id = groups._sync_id AND " +
-                    "group_sync_account = groups._sync_account AND " +
-                    "group_sync_account_type = groups._sync_account_type)) "+
-                "WHERE " + Groups.NAME + "="
-                + DatabaseUtils.sqlEscapeString(groupName) + ")";
-    }
-
-    private Cursor queryOwner(String[] projection) {
-        // Check the permissions
-        getContext().enforceCallingPermission("android.permission.READ_OWNER_DATA",
-                "No permission to access owner info");
-
-        // Read the owner id
-        SharedPreferences prefs = getContext().getSharedPreferences(PREFS_NAME_OWNER,
-                Context.MODE_PRIVATE);
-        long ownerId = prefs.getLong(PREF_OWNER_ID, 0);
-
-        // Run the query
-        return queryInternal(ContentUris.withAppendedId(People.CONTENT_URI, ownerId), projection,
-                null, null, null);
-    }
-
-    /**
-     * Append a string to a selection args array
-     *
-     * @param selectionArgs the old arg
-     * @param newArg the new arg to append
-     * @return a new string array with all of the args
-     */
-    private String[] appendSelectionArg(String[] selectionArgs, String newArg) {
-        if (selectionArgs == null || selectionArgs.length == 0) {
-            return new String[] { newArg };
-        } else {
-            int length = selectionArgs.length;
-            String[] newArgs = new String[length + 1];
-            System.arraycopy(selectionArgs, 0, newArgs, 0, length);
-            newArgs[length] = newArg;
-            return newArgs;
-        }
-    }
-
-    /**
-     * Either sets up the query builder so we can run the proper query against the database
-     * and returns null, or returns a cursor with the results already in it.
-     *
-     * @param url the URL passed for the suggestion
-     * @param qb the query builder to use if a query needs to be run on the database
-     * @return null with qb configured for a query, a cursor with the results already in it.
-     */
-    private Cursor handleSearchSuggestionsQuery(Uri url, SQLiteQueryBuilder qb) {
-        qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN);
-        qb.setProjectionMap(sSearchSuggestionsProjectionMap);
-        if (url.getPathSegments().size() > 1) {
-            // A search term was entered, use it to filter
-
-            // only match within 'my contacts'
-            // TODO: match the 'display group' instead of hard coding 'my contacts'
-            // once that information is factored out of the shared prefs of the contacts
-            // app into this content provider.
-            qb.appendWhere(buildGroupSystemIdMatchWhereClause(Groups.GROUP_MY_CONTACTS));
-            qb.appendWhere(" AND ");
-
-            // match the query
-            final String searchClause = url.getLastPathSegment();
-            if (!TextUtils.isDigitsOnly(searchClause)) {
-                qb.appendWhere(buildPeopleLookupWhereClause(searchClause));
-            } else {
-                final String[] columnNames = new String[] {
-                        "_id",
-                        SearchManager.SUGGEST_COLUMN_TEXT_1,
-                        SearchManager.SUGGEST_COLUMN_TEXT_2,
-                        SearchManager.SUGGEST_COLUMN_ICON_1,
-                        SearchManager.SUGGEST_COLUMN_INTENT_DATA,
-                        SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
-                        SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
-                };
-
-                Resources r = getContext().getResources();
-                String s;
-                int i;
-
-                ArrayList<Object> dialNumber = new ArrayList<Object>();
-                dialNumber.add(0);  // _id
-                s = r.getString(com.android.internal.R.string.dial_number_using, searchClause);
-                i = s.indexOf('\n');
-                if (i < 0) {
-                    dialNumber.add(s);
-                    dialNumber.add("");
-                } else {
-                    dialNumber.add(s.substring(0, i));
-                    dialNumber.add(s.substring(i + 1));
-                }
-                dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact));
-                dialNumber.add("tel:" + searchClause);
-                dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
-                dialNumber.add(null);
-
-                ArrayList<Object> createContact = new ArrayList<Object>();
-                createContact.add(1);  // _id
-                s = r.getString(com.android.internal.R.string.create_contact_using, searchClause);
-                i = s.indexOf('\n');
-                if (i < 0) {
-                    createContact.add(s);
-                    createContact.add("");
-                } else {
-                    createContact.add(s.substring(0, i));
-                    createContact.add(s.substring(i + 1));
-                }
-                createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact));
-                createContact.add("tel:" + searchClause);
-                createContact.add(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
-                createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
-
-                ArrayList<ArrayList> rows = new ArrayList<ArrayList>();
-                rows.add(dialNumber);
-                rows.add(createContact);
-
-                ArrayListCursor cursor = new ArrayListCursor(columnNames, rows);
-                return cursor;
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public String getType(Uri url) {
-        int match = sURIMatcher.match(url);
-        switch (match) {
-            case EXTENSIONS:
-            case PEOPLE_EXTENSIONS:
-                return Extensions.CONTENT_TYPE;
-            case EXTENSIONS_ID:
-            case PEOPLE_EXTENSIONS_ID:
-                return Extensions.CONTENT_ITEM_TYPE;
-            case PEOPLE:
-                return "vnd.android.cursor.dir/person";
-            case PEOPLE_ID:
-                return "vnd.android.cursor.item/person";
-            case PEOPLE_PHONES:
-                return "vnd.android.cursor.dir/phone";
-            case PEOPLE_PHONES_ID:
-                return "vnd.android.cursor.item/phone";
-            case PEOPLE_CONTACTMETHODS:
-                return "vnd.android.cursor.dir/contact-methods";
-            case PEOPLE_CONTACTMETHODS_ID:
-                return getContactMethodType(url);
-            case PHONES:
-                return "vnd.android.cursor.dir/phone";
-            case PHONES_ID:
-                return "vnd.android.cursor.item/phone";
-            case PHONES_FILTER:
-            case PHONES_FILTER_NAME:
-            case PHONES_MOBILE_FILTER_NAME:
-                return "vnd.android.cursor.dir/phone";
-            case PHOTOS_ID:
-                return "vnd.android.cursor.item/photo";
-            case PHOTOS:
-                return "vnd.android.cursor.dir/photo";
-            case PEOPLE_PHOTO:
-                return "vnd.android.cursor.item/photo";
-            case PEOPLE_PHOTO_DATA:
-                return "image/png";
-            case CONTACTMETHODS:
-                return "vnd.android.cursor.dir/contact-methods";
-            case CONTACTMETHODS_ID:
-                return getContactMethodType(url);
-            case CONTACTMETHODS_EMAIL:
-            case CONTACTMETHODS_EMAIL_FILTER:
-                return "vnd.android.cursor.dir/email";
-            case CALLS:
-                return "vnd.android.cursor.dir/calls";
-            case CALLS_ID:
-                return "vnd.android.cursor.item/calls";
-            case ORGANIZATIONS:
-                return "vnd.android.cursor.dir/organizations";
-            case ORGANIZATIONS_ID:
-                return "vnd.android.cursor.item/organization";
-            case CALLS_FILTER:
-                return "vnd.android.cursor.dir/calls";
-            case SEARCH_SUGGESTIONS:
-                return SearchManager.SUGGEST_MIME_TYPE;
-            case SEARCH_SHORTCUT:
-                return SearchManager.SHORTCUT_MIME_TYPE;
-            default:
-                throw new IllegalArgumentException("Unknown URL");
-        }
-    }
-
-    private String getContactMethodType(Uri url)
-    {
-        String mime = null;
-
-        Cursor c = query(url, new String[] {ContactMethods.KIND}, null, null, null);
-        if (c != null) {
-            try {
-                if (c.moveToFirst()) {
-                    int kind = c.getInt(0);
-                    switch (kind) {
-                    case Contacts.KIND_EMAIL:
-                        mime = "vnd.android.cursor.item/email";
-                        break;
-
-                    case Contacts.KIND_IM:
-                        mime = "vnd.android.cursor.item/jabber-im";
-                        break;
-
-                    case Contacts.KIND_POSTAL:
-                        mime = "vnd.android.cursor.item/postal-address";
-                        break;
-                    }
-                }
-            } finally {
-                c.close();
-            }
-        }
-        return mime;
-    }
-
-    private ContentValues queryAndroidStarredGroupId(Account account) {
-        String whereString;
-        String[] whereArgs;
-        if (account != null) {
-            whereString = "_sync_account=? AND _sync_account_type=? AND name=?";
-            whereArgs = new String[]{account.name, account.type, Groups.GROUP_ANDROID_STARRED};
-        } else {
-            whereString = "_sync_account is null AND name=?";
-            whereArgs = new String[]{Groups.GROUP_ANDROID_STARRED};
-        }
-        Cursor cursor = getDatabase().query(sGroupsTable,
-                new String[]{Groups._ID, Groups._SYNC_ID, Groups._SYNC_ACCOUNT,
-                        Groups._SYNC_ACCOUNT_TYPE},
-                whereString, whereArgs, null, null, null);
-        try {
-            if (cursor.moveToNext()) {
-                ContentValues result = new ContentValues();
-                result.put(Groups._ID, cursor.getLong(0));
-                result.put(Groups._SYNC_ID, cursor.getString(1));
-                result.put(Groups._SYNC_ACCOUNT, cursor.getString(2));
-                result.put(Groups._SYNC_ACCOUNT_TYPE, cursor.getString(3));
-                return result;
-            }
-            return null;
-        } finally {
-            cursor.close();
-        }
-    }
-
-    @Override
-    public Uri insertInternal(Uri url, ContentValues initialValues) {
-        Uri resultUri = null;
-        long rowID;
-
-        final SQLiteDatabase db = getDatabase();
-        int match = sURIMatcher.match(url);
-        switch (match) {
-            case PEOPLE_GROUPMEMBERSHIP:
-            case GROUPMEMBERSHIP: {
-                mValues.clear();
-                mValues.putAll(initialValues);
-                if (match == PEOPLE_GROUPMEMBERSHIP) {
-                    mValues.put(GroupMembership.PERSON_ID,
-                            Long.valueOf(url.getPathSegments().get(1)));
-                }
-                resultUri = insertIntoGroupmembership(mValues);
-            }
-            break;
-
-            case PEOPLE_OWNER:
-                return insertOwner(initialValues);
-
-            case PEOPLE_EXTENSIONS:
-            case EXTENSIONS: {
-                ContentValues newMap = new ContentValues(initialValues);
-                if (match == PEOPLE_EXTENSIONS) {
-                    newMap.put(Extensions.PERSON_ID,
-                            Long.valueOf(url.getPathSegments().get(1)));
-                }
-                rowID = mExtensionsInserter.insert(newMap);
-                if (rowID > 0) {
-                    resultUri = ContentUris.withAppendedId(Extensions.CONTENT_URI, rowID);
-                }
-            }
-            break;
-
-            case PHOTOS: {
-                if (!isTemporary()) {
-                    throw new UnsupportedOperationException();
-                }
-                rowID = mPhotosInserter.insert(initialValues);
-                if (rowID > 0) {
-                    resultUri = ContentUris.withAppendedId(Photos.CONTENT_URI, rowID);
-                }
-            }
-            break;
-
-            case GROUPS: {
-                ContentValues newMap = new ContentValues(initialValues);
-                ensureSyncAccountIsSet(newMap);
-                newMap.put(Groups._SYNC_DIRTY, 1);
-                // Insert into the groups table
-                rowID = mGroupsInserter.insert(newMap);
-                if (rowID > 0) {
-                    resultUri = ContentUris.withAppendedId(Groups.CONTENT_URI, rowID);
-                    if (!isTemporary() && newMap.containsKey(Groups.SHOULD_SYNC)) {
-                        final String accountName = newMap.getAsString(Groups._SYNC_ACCOUNT);
-                        final String accountType = newMap.getAsString(Groups._SYNC_ACCOUNT_TYPE);
-                        if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
-                            final ContentResolver cr = getContext().getContentResolver();
-                            final Account account = new Account(accountName, accountType);
-                            onLocalChangesForAccount(cr, account, false);
-                        }
-                    }
-                }
-            }
-            break;
-
-            case PEOPLE_RAW:
-            case PEOPLE: {
-                mValues.clear();
-                mValues.putAll(initialValues);
-                ensureSyncAccountIsSet(mValues);
-                mValues.put(People._SYNC_DIRTY, 1);
-                // Insert into the people table
-                rowID = mPeopleInserter.insert(mValues);
-                if (rowID > 0) {
-                    resultUri = ContentUris.withAppendedId(People.CONTENT_URI, rowID);
-                    if (!isTemporary()) {
-                        String accountName = mValues.getAsString(People._SYNC_ACCOUNT);
-                        String accountType = mValues.getAsString(People._SYNC_ACCOUNT_TYPE);
-                        Account account = null;
-                        if (accountName != null || accountType != null) {
-                            account = new Account(accountName, accountType);
-                        }
-                        Long starredValue = mValues.getAsLong(People.STARRED);
-                        final String syncId = mValues.getAsString(People._SYNC_ID);
-                        boolean isStarred = starredValue != null && starredValue != 0;
-                        fixupGroupMembershipAfterPeopleUpdate(account, rowID, isStarred);
-                        // create a photo row for this person
-                        mDb.delete(sPhotosTable, "person=" + rowID, null);
-                        mValues.clear();
-                        mValues.put(Photos.PERSON_ID, rowID);
-                        mValues.put(Photos._SYNC_ACCOUNT, accountName);
-                        mValues.put(Photos._SYNC_ACCOUNT_TYPE, accountType);
-                        mValues.put(Photos._SYNC_ID, syncId);
-                        mValues.put(Photos._SYNC_DIRTY, 0);
-                        mPhotosInserter.insert(mValues);
-                    }
-                }
-            }
-            break;
-
-            case DELETED_PEOPLE: {
-                if (isTemporary()) {
-                    // Insert into the people table
-                    rowID = db.insert("_deleted_people", "_sync_id", initialValues);
-                    if (rowID > 0) {
-                        resultUri = Uri.parse("content://contacts/_deleted_people/" + rowID);
-                    }
-                } else {
-                    throw new UnsupportedOperationException();
-                }
-            }
-            break;
-
-            case DELETED_GROUPS: {
-                if (isTemporary()) {
-                    rowID = db.insert(sDeletedGroupsTable, Groups._SYNC_ID,
-                            initialValues);
-                    if (rowID > 0) {
-                        resultUri =ContentUris.withAppendedId(
-                                Groups.DELETED_CONTENT_URI, rowID);
-                    }
-                } else {
-                    throw new UnsupportedOperationException();
-                }
-            }
-            break;
-
-            case PEOPLE_PHONES:
-            case PHONES: {
-                mValues.clear();
-                mValues.putAll(initialValues);
-                if (match == PEOPLE_PHONES) {
-                    mValues.put(Contacts.Phones.PERSON_ID,
-                            Long.valueOf(url.getPathSegments().get(1)));
-                }
-                String number = mValues.getAsString(Contacts.Phones.NUMBER);
-                if (number != null) {
-                    mValues.put("number_key", PhoneNumberUtils.getStrippedReversed(number));
-                }
-
-                rowID = insertAndFixupPrimary(Contacts.KIND_PHONE, mValues);
-                resultUri = ContentUris.withAppendedId(Phones.CONTENT_URI, rowID);
-            }
-            break;
-
-            case CONTACTMETHODS:
-            case PEOPLE_CONTACTMETHODS: {
-                mValues.clear();
-                mValues.putAll(initialValues);
-                if (match == PEOPLE_CONTACTMETHODS) {
-                    mValues.put("person", url.getPathSegments().get(1));
-                }
-                Integer kind = mValues.getAsInteger(ContactMethods.KIND);
-                if (kind == null) {
-                    throw new IllegalArgumentException("you must specify the ContactMethods.KIND");
-                }
-                rowID = insertAndFixupPrimary(kind, mValues);
-                if (rowID > 0) {
-                    resultUri = ContentUris.withAppendedId(ContactMethods.CONTENT_URI, rowID);
-                }
-            }
-            break;
-
-            case CALLS: {
-                rowID = mCallsInserter.insert(initialValues);
-                if (rowID > 0) {
-                    resultUri = Uri.parse("content://call_log/calls/" + rowID);
-                }
-            }
-            break;
-
-            case PRESENCE: {
-                final String handle = initialValues.getAsString(Presence.IM_HANDLE);
-                final String protocol = initialValues.getAsString(Presence.IM_PROTOCOL);
-                if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
-                    throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required");
-                }
-
-                // Look for the contact for this presence update
-                StringBuilder query = new StringBuilder("SELECT ");
-                query.append(ContactMethods.PERSON_ID);
-                query.append(" FROM contact_methods WHERE (kind=");
-                query.append(Contacts.KIND_IM);
-                query.append(" AND ");
-                query.append(ContactMethods.DATA);
-                query.append("=? AND ");
-                query.append(ContactMethods.AUX_DATA);
-                query.append("=?)");
-
-                String[] selectionArgs;
-                if (GTALK_PROTOCOL_STRING.equals(protocol)) {
-                    // For gtalk accounts we usually don't have an explicit IM
-                    // entry, so also look for the email address as well
-                    query.append(" OR (");
-                    query.append("kind=");
-                    query.append(Contacts.KIND_EMAIL);
-                    query.append(" AND ");
-                    query.append(ContactMethods.DATA);
-                    query.append("=?)");
-                    selectionArgs = new String[] { handle, protocol, handle };
-                } else {
-                    selectionArgs = new String[] { handle, protocol };
-                }
-
-                Cursor c = db.rawQueryWithFactory(null, query.toString(), selectionArgs, null);
-
-                long personId = 0;
-                try {
-                    if (c.moveToFirst()) {
-                        personId = c.getLong(0);
-                    } else {
-                        // No contact found, return a null URI
-                        return null;
-                    }
-                } finally {
-                    c.close();
-                }
-
-                mValues.clear();
-                mValues.putAll(initialValues);
-                mValues.put(Presence.PERSON_ID, personId);
-
-                // Insert the presence update
-                rowID = db.replace("presence", null, mValues);
-                if (rowID > 0) {
-                    resultUri = Uri.parse("content://contacts/presence/" + rowID);
-                }
-            }
-            break;
-
-            case PEOPLE_ORGANIZATIONS:
-            case ORGANIZATIONS: {
-                ContentValues newMap = new ContentValues(initialValues);
-                if (match == PEOPLE_ORGANIZATIONS) {
-                    newMap.put(Contacts.Phones.PERSON_ID,
-                            Long.valueOf(url.getPathSegments().get(1)));
-                }
-                rowID = insertAndFixupPrimary(Contacts.KIND_ORGANIZATION, newMap);
-                if (rowID > 0) {
-                    resultUri = Uri.parse("content://contacts/organizations/" + rowID);
-                }
-            }
-            break;
-            default:
-                throw new UnsupportedOperationException("Cannot insert into URL: " + url);
-        }
-
-        return resultUri;
-    }
-
-    @Override
-    protected void onAccountsChanged(Account[] accountsArray) {
-        super.onAccountsChanged(accountsArray);
-        synchronized (mAccountsLock) {
-            mAccounts = new Account[accountsArray.length];
-            System.arraycopy(accountsArray, 0, mAccounts, 0, mAccounts.length);
-        }
-    }
-
-    private void ensureSyncAccountIsSet(ContentValues values) {
-        synchronized (mAccountsLock) {
-            final String accountName = values.getAsString(SyncConstValue._SYNC_ACCOUNT);
-            final String accountType = values.getAsString(SyncConstValue._SYNC_ACCOUNT_TYPE);
-            Account account = null;
-            if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
-                account = new Account(accountName, accountType);
-            }
-            if (account == null && mAccounts.length > 0) {
-                // TODO(fredq) change this to pick the account that is syncable for contacts
-                values.put(SyncConstValue._SYNC_ACCOUNT, mAccounts[0].name);
-                values.put(SyncConstValue._SYNC_ACCOUNT_TYPE, mAccounts[0].type);
-            }
-        }
-    }
-
-    private Uri insertOwner(ContentValues values) {
-        // Check the permissions
-        getContext().enforceCallingPermission("android.permission.WRITE_OWNER_DATA",
-                "No permission to set owner info");
-
-        // Insert the owner info
-        Uri uri = insertInternal(People.CONTENT_URI, values);
-
-        // Record which person is the owner
-        long id = ContentUris.parseId(uri);
-        SharedPreferences.Editor prefs = getContext().getSharedPreferences(PREFS_NAME_OWNER,
-                Context.MODE_PRIVATE).edit();
-        prefs.putLong(PREF_OWNER_ID, id);
-        prefs.commit();
-        return uri;
-    }
-
-    private Uri insertIntoGroupmembership(ContentValues values) {
-        String groupSyncAccountName = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT);
-        String groupSyncAccountType = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
-        String groupSyncId = values.getAsString(GroupMembership.GROUP_SYNC_ID);
-        final Long personId = values.getAsLong(GroupMembership.PERSON_ID);
-        if (!values.containsKey(GroupMembership.GROUP_ID)) {
-            if (TextUtils.isEmpty(groupSyncAccountName) || TextUtils.isEmpty(groupSyncAccountType)
-                    || TextUtils.isEmpty(groupSyncId)) {
-                throw new IllegalArgumentException(
-                        "insertIntoGroupmembership: no GROUP_ID wasn't specified and non-empty "
-                        + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT and GROUP_SYNC_ACCOUNT_TYPE fields "
-                        + "weren't specifid, "
-                        + values);
-            }
-            if (0 != DatabaseUtils.longForQuery(getDatabase(), ""
-                    + "SELECT COUNT(*) "
-                    + "FROM groupmembership "
-                    + "WHERE group_sync_id=? "
-                    + "  AND group_sync_account=? "
-                    + "  AND group_sync_account_type=? "
-                    + "  AND person=?",
-                    new String[]{groupSyncId, groupSyncAccountName, groupSyncAccountType,
-                            String.valueOf(personId)})) {
-                final String errorMessage =
-                        "insertIntoGroupmembership: a row with this server key already exists, "
-                                + values;
-                if (Config.LOGD) Log.d(TAG, errorMessage);
-                return null;
-            }
-        } else {
-            long groupId = values.getAsLong(GroupMembership.GROUP_ID);
-            if (!TextUtils.isEmpty(groupSyncAccountName) || !TextUtils.isEmpty(groupSyncAccountType)
-                    || !TextUtils.isEmpty(groupSyncId)) {
-                throw new IllegalArgumentException(
-                        "insertIntoGroupmembership: GROUP_ID was specified but "
-                        + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT and GROUP_SYNC_ACCOUNT_TYPE fields "
-                        + "were also specifid, "
-                        + values);
-            }
-            if (0 != DatabaseUtils.longForQuery(getDatabase(),
-                    "SELECT COUNT(*) FROM groupmembership where group_id=? AND person=?",
-                    new String[]{String.valueOf(groupId), String.valueOf(personId)})) {
-                final String errorMessage =
-                        "insertIntoGroupmembership: a row with this local key already exists, "
-                                + values;
-                if (Config.LOGD) Log.d(TAG, errorMessage);
-                return null;
-            }
-        }
-
-        long rowId = mGroupMembershipInserter.insert(values);
-        if (rowId <= 0) {
-            final String errorMessage = "insertIntoGroupmembership: the insert failed, values are "
-                    + values;
-            if (Config.LOGD) Log.d(TAG, errorMessage);
-            return null;
-        }
-
-        // set the STARRED column in the people row if this group is the GROUP_ANDROID_STARRED
-        if (!isTemporary() && queryGroupMembershipContainsStarred(personId)) {
-            fixupPeopleStarred(personId, true);
-        }
-
-        return ContentUris.withAppendedId(GroupMembership.CONTENT_URI, rowId);
-    }
-
-    private void fixupGroupMembershipAfterPeopleUpdate(Account account, long personId,
-            boolean makeStarred) {
-        ContentValues starredGroupInfo = queryAndroidStarredGroupId(account);
-        if (makeStarred) {
-            if (starredGroupInfo == null) {
-                // we need to add the starred group
-                mValuesLocal.clear();
-                mValuesLocal.put(Groups.NAME, Groups.GROUP_ANDROID_STARRED);
-                mValuesLocal.put(Groups._SYNC_DIRTY, 1);
-                mValuesLocal.put(Groups._SYNC_ACCOUNT, account == null ? null : account.name);
-                mValuesLocal.put(Groups._SYNC_ACCOUNT_TYPE, account == null ? null : account.type);
-                long groupId = mGroupsInserter.insert(mValuesLocal);
-                starredGroupInfo = new ContentValues();
-                starredGroupInfo.put(Groups._ID, groupId);
-                starredGroupInfo.put(Groups._SYNC_ACCOUNT,
-                        mValuesLocal.getAsString(Groups._SYNC_ACCOUNT));
-                starredGroupInfo.put(Groups._SYNC_ACCOUNT_TYPE,
-                        mValuesLocal.getAsString(Groups._SYNC_ACCOUNT_TYPE));
-                // don't put the _SYNC_ID in here since we don't know it yet
-            }
-
-            final Long groupId = starredGroupInfo.getAsLong(Groups._ID);
-            final String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID);
-            final String syncAccountName = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
-            final String syncAccountType = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT_TYPE);
-
-            // check that either groupId is set or the syncId/Account is set
-            final boolean hasSyncId = !TextUtils.isEmpty(syncId);
-            final boolean hasGroupId = groupId != null;
-            if (!hasGroupId && !hasSyncId) {
-                throw new IllegalStateException("at least one of the groupId or "
-                        + "the syncId must be set, " + starredGroupInfo);
-            }
-
-            // now add this person to the group
-            mValuesLocal.clear();
-            mValuesLocal.put(GroupMembership.PERSON_ID, personId);
-            mValuesLocal.put(GroupMembership.GROUP_ID, groupId);
-            mValuesLocal.put(GroupMembership.GROUP_SYNC_ID, syncId);
-            mValuesLocal.put(GroupMembership.GROUP_SYNC_ACCOUNT, syncAccountName);
-            mValuesLocal.put(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, syncAccountType);
-            mGroupMembershipInserter.insert(mValuesLocal);
-        } else {
-            if (starredGroupInfo != null) {
-                // delete the groupmembership rows for this person that match the starred group id
-                String syncAccountName = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
-                String syncAccountType = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT_TYPE);
-                String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID);
-                if (!TextUtils.isEmpty(syncId)) {
-                    mDb.delete(sGroupmembershipTable,
-                            "person=? AND group_sync_id=? AND group_sync_account=?"
-                                    + " AND group_sync_account_type=?",
-                            new String[]{String.valueOf(personId), syncId,
-                                    syncAccountName, syncAccountType});
-                } else {
-                    mDb.delete(sGroupmembershipTable, "person=? AND group_id=?",
-                            new String[]{
-                                    Long.toString(personId),
-                                    Long.toString(starredGroupInfo.getAsLong(Groups._ID))});
-                }
-            }
-        }
-    }
-
-    private int fixupPeopleStarred(long personId, boolean inStarredGroup) {
-        mValuesLocal.clear();
-        mValuesLocal.put(People.STARRED, inStarredGroup ? 1 : 0);
-        return getDatabase().update(sPeopleTable, mValuesLocal, WHERE_ID,
-                new String[]{String.valueOf(personId)});
-    }
-
-    private String kindToTable(int kind) {
-        switch (kind) {
-            case Contacts.KIND_EMAIL: return sContactMethodsTable;
-            case Contacts.KIND_POSTAL: return sContactMethodsTable;
-            case Contacts.KIND_IM: return sContactMethodsTable;
-            case Contacts.KIND_PHONE: return sPhonesTable;
-            case Contacts.KIND_ORGANIZATION: return sOrganizationsTable;
-            default: throw new IllegalArgumentException("unknown kind, " + kind);
-        }
-    }
-
-    private DatabaseUtils.InsertHelper kindToInserter(int kind) {
-        switch (kind) {
-            case Contacts.KIND_EMAIL: return mContactMethodsInserter;
-            case Contacts.KIND_POSTAL: return mContactMethodsInserter;
-            case Contacts.KIND_IM: return mContactMethodsInserter;
-            case Contacts.KIND_PHONE: return mPhonesInserter;
-            case Contacts.KIND_ORGANIZATION: return mOrganizationsInserter;
-            default: throw new IllegalArgumentException("unknown kind, " + kind);
-        }
-    }
-
-    private long insertAndFixupPrimary(int kind, ContentValues values) {
-        final String table = kindToTable(kind);
-        boolean isPrimary = false;
-        Long personId = null;
-
-        if (!isTemporary()) {
-            // when you add a item, if isPrimary or if there is no primary,
-            // make this it, set the isPrimary flag, and clear other primary flags
-            isPrimary = values.containsKey("isprimary")
-                    && (values.getAsInteger("isprimary") != 0);
-            personId = values.getAsLong("person");
-            if (!isPrimary) {
-                // make it primary anyway if this person doesn't have any rows of this type yet
-                StringBuilder sb = new StringBuilder("person=" + personId);
-                if (sContactMethodsTable.equals(table)) {
-                    sb.append(" AND kind=");
-                    sb.append(kind);
-                }
-                final boolean isFirstRowOfType = DatabaseUtils.longForQuery(getDatabase(),
-                        "SELECT count(*) FROM " + table + " where " + sb.toString(), null) == 0;
-                isPrimary = isFirstRowOfType;
-            }
-
-            values.put("isprimary", isPrimary ? 1 : 0);
-        }
-
-        // do the actual insert
-        long newRowId = kindToInserter(kind).insert(values);
-
-        if (newRowId <= 0) {
-            throw new RuntimeException("error while inserting into " + table + ", " + values);
-        }
-
-        if (!isTemporary()) {
-            // If this row was made the primary then clear the other isprimary flags and update
-            // corresponding people row, if necessary.
-            if (isPrimary) {
-                clearOtherIsPrimary(kind, personId, newRowId);
-                if (kind == Contacts.KIND_PHONE) {
-                    updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newRowId);
-                } else if (kind == Contacts.KIND_EMAIL) {
-                    updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newRowId);
-                } else if (kind == Contacts.KIND_ORGANIZATION) {
-                    updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newRowId);
-                }
-            }
-        }
-
-        return newRowId;
-    }
-
-    @Override
-    public int deleteInternal(Uri url, String userWhere, String[] whereArgs) {
-        String tableToChange;
-        String changedItemId;
-
-        final int matchedUriId = sURIMatcher.match(url);
-        switch (matchedUriId) {
-            case GROUPMEMBERSHIP_ID:
-                return deleteFromGroupMembership(Long.parseLong(url.getPathSegments().get(1)),
-                        userWhere, whereArgs);
-            case GROUPS:
-                return deleteFromGroups(userWhere, whereArgs);
-            case GROUPS_ID:
-                changedItemId = url.getPathSegments().get(1);
-                return deleteFromGroups(addIdToWhereClause(changedItemId, userWhere), whereArgs);
-            case EXTENSIONS:
-                tableToChange = sExtensionsTable;
-                changedItemId = null;
-                break;
-            case EXTENSIONS_ID:
-                tableToChange = sExtensionsTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-            case PEOPLE_RAW:
-            case PEOPLE:
-                return deleteFromPeople(null, userWhere, whereArgs);
-            case PEOPLE_ID:
-                return deleteFromPeople(url.getPathSegments().get(1), userWhere, whereArgs);
-            case PEOPLE_PHONES_ID:
-                tableToChange = sPhonesTable;
-                changedItemId = url.getPathSegments().get(3);
-                break;
-            case PEOPLE_CONTACTMETHODS_ID:
-                tableToChange = sContactMethodsTable;
-                changedItemId = url.getPathSegments().get(3);
-                break;
-            case PHONES_ID:
-                tableToChange = sPhonesTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-            case ORGANIZATIONS_ID:
-                tableToChange = sOrganizationsTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-            case CONTACTMETHODS_ID:
-                tableToChange = sContactMethodsTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-            case PRESENCE:
-                tableToChange = "presence";
-                changedItemId = null;
-                break;
-            case CALLS:
-                tableToChange = "calls";
-                changedItemId = null;
-                break;
-            default:
-                throw new UnsupportedOperationException("Cannot delete that URL: " + url);
-        }
-
-        String where = addIdToWhereClause(changedItemId, userWhere);
-        IsPrimaryInfo oldPrimaryInfo = null;
-        switch (matchedUriId) {
-            case PEOPLE_PHONES_ID:
-            case PHONES_ID:
-            case ORGANIZATIONS_ID:
-                oldPrimaryInfo = lookupIsPrimaryInfo(tableToChange,
-                        sIsPrimaryProjectionWithoutKind, where, whereArgs);
-                break;
-
-            case PEOPLE_CONTACTMETHODS_ID:
-            case CONTACTMETHODS_ID:
-                oldPrimaryInfo = lookupIsPrimaryInfo(tableToChange,
-                        sIsPrimaryProjectionWithKind, where, whereArgs);
-                break;
-        }
-
-        final SQLiteDatabase db = getDatabase();
-        int count = db.delete(tableToChange, where, whereArgs);
-        if (count > 0) {
-            if (oldPrimaryInfo != null && oldPrimaryInfo.isPrimary) {
-                fixupPrimaryAfterDelete(oldPrimaryInfo.kind,
-                        oldPrimaryInfo.id, oldPrimaryInfo.person);
-            }
-        }
-
-        return count;
-    }
-
-    @Override
-    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
-        int match = sURIMatcher.match(uri);
-        switch (match) {
-            case PEOPLE_PHOTO_DATA:
-                if (!"r".equals(mode)) {
-                    throw new FileNotFoundException("Mode " + mode + " not supported.");
-                }
-                String person = uri.getPathSegments().get(1);
-                String sql = "SELECT " + Photos.DATA + " FROM " + sPhotosTable
-                        + " WHERE " + Photos.PERSON_ID + "=?";
-                String[] selectionArgs = { person };
-                return SQLiteContentHelper.getBlobColumnAsAssetFile(getDatabase(), sql,
-                        selectionArgs);
-            default:
-                throw new FileNotFoundException("No file at: " + uri);
-        }
-    }
-
-    @Override
-    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
-        int match = sURIMatcher.match(uri);
-        switch (match) {
-            default:
-                throw new UnsupportedOperationException(uri.toString());
-        }
-    }
-
-    private int deleteFromGroupMembership(long rowId, String where, String[] whereArgs) {
-        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        qb.setTables("groups, groupmembership");
-        qb.setProjectionMap(sGroupMembershipProjectionMap);
-        qb.appendWhere(sGroupsJoinString);
-        qb.appendWhere(" AND groupmembership._id=" + rowId);
-        Cursor cursor = qb.query(getDatabase(), null, where, whereArgs, null, null, null);
-        try {
-            final int indexPersonId = cursor.getColumnIndexOrThrow(GroupMembership.PERSON_ID);
-            final int indexName = cursor.getColumnIndexOrThrow(GroupMembership.NAME);
-            while (cursor.moveToNext()) {
-                if (Groups.GROUP_ANDROID_STARRED.equals(cursor.getString(indexName))) {
-                    fixupPeopleStarred(cursor.getLong(indexPersonId), false);
-                }
-            }
-        } finally {
-            cursor.close();
-        }
-
-        return mDb.delete(sGroupmembershipTable,
-                addIdToWhereClause(String.valueOf(rowId), where),
-                whereArgs);
-    }
-
-    private int deleteFromPeople(String rowId, String where, String[] whereArgs) {
-        final SQLiteDatabase db = getDatabase();
-        where = addIdToWhereClause(rowId, where);
-        Cursor cursor = db.query(sPeopleTable, null, where, whereArgs, null, null, null);
-        try {
-            final int idxSyncId = cursor.getColumnIndexOrThrow(People._SYNC_ID);
-            final int idxSyncAccountName = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
-            final int idxSyncAccountType = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT_TYPE);
-            final int idxSyncVersion = cursor.getColumnIndexOrThrow(People._SYNC_VERSION);
-            final int dstIdxSyncId = mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ID);
-            final int dstIdxSyncAccountName =
-                    mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ACCOUNT);
-            final int dstIdxSyncAccountType =
-                    mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ACCOUNT_TYPE);
-            final int dstIdxSyncVersion =
-                    mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_VERSION);
-            while (cursor.moveToNext()) {
-                final String syncId = cursor.getString(idxSyncId);
-                if (TextUtils.isEmpty(syncId)) continue;
-                // insert into deleted table
-                mDeletedPeopleInserter.prepareForInsert();
-                mDeletedPeopleInserter.bind(dstIdxSyncId, syncId);
-                mDeletedPeopleInserter.bind(dstIdxSyncAccountName,
-                        cursor.getString(idxSyncAccountName));
-                mDeletedPeopleInserter.bind(dstIdxSyncAccountType,
-                        cursor.getString(idxSyncAccountType));
-                mDeletedPeopleInserter.bind(dstIdxSyncVersion, cursor.getString(idxSyncVersion));
-                mDeletedPeopleInserter.execute();
-            }
-        } finally {
-            cursor.close();
-        }
-
-        // perform the actual delete
-        return db.delete(sPeopleTable, where, whereArgs);
-    }
-
-    private int deleteFromGroups(String where, String[] whereArgs) {
-        HashSet<Account> modifiedAccounts = Sets.newHashSet();
-        Cursor cursor = getDatabase().query(sGroupsTable, null, where, whereArgs,
-                null, null, null);
-        try {
-            final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME);
-            final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT);
-            final int indexSyncAccountType =
-                    cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE);
-            final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID);
-            final int indexId = cursor.getColumnIndexOrThrow(Groups._ID);
-            final int indexShouldSync = cursor.getColumnIndexOrThrow(Groups.SHOULD_SYNC);
-            while (cursor.moveToNext()) {
-                String oldName = cursor.getString(indexName);
-                String syncAccountName = cursor.getString(indexSyncAccount);
-                String syncAccountType = cursor.getString(indexSyncAccountType);
-                String syncId = cursor.getString(indexSyncId);
-                boolean shouldSync = cursor.getLong(indexShouldSync) != 0;
-                long id = cursor.getLong(indexId);
-                fixupPeopleStarredOnGroupRename(oldName, null, id);
-                if (!TextUtils.isEmpty(syncAccountName) && !TextUtils.isEmpty(syncId)) {
-                    fixupPeopleStarredOnGroupRename(oldName, null,
-                            new Account(syncAccountName, syncAccountType), syncId);
-                }
-                if (!TextUtils.isEmpty(syncAccountName) && shouldSync) {
-                    modifiedAccounts.add(new Account(syncAccountName, syncAccountType));
-                }
-            }
-        } finally {
-            cursor.close();
-        }
-
-        int numRows = mDb.delete(sGroupsTable, where, whereArgs);
-        if (numRows > 0) {
-            if (!isTemporary()) {
-                final ContentResolver cr = getContext().getContentResolver();
-                for (Account account : modifiedAccounts) {
-                    onLocalChangesForAccount(cr, account, true);
-                }
-            }
-        }
-        return numRows;
-    }
-
-    /**
-     * Called when local changes are made, so subclasses have
-     * an opportunity to react as they see fit.
-     *
-     * @param resolver the content resolver to use
-     * @param account the account the changes are tied to
-     */
-    protected void onLocalChangesForAccount(final ContentResolver resolver, Account account,
-            boolean groupsModified) {
-        // Do nothing
-    }
-
-    private void fixupPrimaryAfterDelete(int kind, Long itemId, Long personId) {
-        final String table = kindToTable(kind);
-        // when you delete an item with isPrimary,
-        // select a new one as isPrimary and clear the primary if no more items
-        Long newPrimaryId = findNewPrimary(kind, personId, itemId);
-
-        // we found a new primary, set its isprimary flag
-        if (newPrimaryId != null) {
-            mValuesLocal.clear();
-            mValuesLocal.put("isprimary", 1);
-            if (getDatabase().update(table, mValuesLocal, "_id=" + newPrimaryId, null) != 1) {
-                throw new RuntimeException("error updating " + table + ", _id "
-                        + newPrimaryId + ", values " + mValuesLocal);
-            }
-        }
-
-        // if this kind's primary status should be reflected in the people row, update it
-        if (kind == Contacts.KIND_PHONE) {
-            updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newPrimaryId);
-        } else if (kind == Contacts.KIND_EMAIL) {
-            updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newPrimaryId);
-        } else if (kind == Contacts.KIND_ORGANIZATION) {
-            updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newPrimaryId);
-        }
-    }
-
-    @Override
-    public int updateInternal(Uri url, ContentValues values, String userWhere, String[] whereArgs) {
-        final SQLiteDatabase db = getDatabase();
-        String tableToChange;
-        String changedItemId;
-        final int matchedUriId = sURIMatcher.match(url);
-        switch (matchedUriId) {
-            case GROUPS_ID:
-                changedItemId = url.getPathSegments().get(1);
-                return updateGroups(values,
-                        addIdToWhereClause(changedItemId, userWhere), whereArgs);
-
-            case PEOPLE_EXTENSIONS_ID:
-                tableToChange = sExtensionsTable;
-                changedItemId = url.getPathSegments().get(3);
-                break;
-
-            case EXTENSIONS_ID:
-                tableToChange = sExtensionsTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-
-            case PEOPLE_UPDATE_CONTACT_TIME:
-                if (values.size() != 1 || !values.containsKey(People.LAST_TIME_CONTACTED)) {
-                    throw new IllegalArgumentException(
-                            "You may only use " + url + " to update People.LAST_TIME_CONTACTED");
-                }
-                tableToChange = sPeopleTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-
-            case PEOPLE_ID:
-                mValues.clear();
-                mValues.putAll(values);
-                mValues.put(Photos._SYNC_DIRTY, 1);
-                values = mValues;
-                tableToChange = sPeopleTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-
-            case PEOPLE_PHONES_ID:
-                tableToChange = sPhonesTable;
-                changedItemId = url.getPathSegments().get(3);
-                break;
-
-            case PEOPLE_CONTACTMETHODS_ID:
-                tableToChange = sContactMethodsTable;
-                changedItemId = url.getPathSegments().get(3);
-                break;
-
-            case PHONES_ID:
-                tableToChange = sPhonesTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-
-            case PEOPLE_PHOTO:
-            case PHOTOS_ID:
-                mValues.clear();
-                mValues.putAll(values);
-
-                // The _SYNC_DIRTY flag should only be set if the data was modified and if
-                // it isn't already provided.
-                if (!mValues.containsKey(Photos._SYNC_DIRTY) && mValues.containsKey(Photos.DATA)) {
-                    mValues.put(Photos._SYNC_DIRTY, 1);
-                }
-                StringBuilder where;
-                if (matchedUriId == PEOPLE_PHOTO) {
-                    where = new StringBuilder("_id=" + url.getPathSegments().get(1));
-                } else {
-                    where = new StringBuilder("person=" + url.getPathSegments().get(1));
-                }
-                if (!TextUtils.isEmpty(userWhere)) {
-                    where.append(" AND (");
-                    where.append(userWhere);
-                    where.append(')');
-                }
-                return db.update(sPhotosTable, mValues, where.toString(), whereArgs);
-
-            case ORGANIZATIONS_ID:
-                tableToChange = sOrganizationsTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-
-            case CONTACTMETHODS_ID:
-                tableToChange = sContactMethodsTable;
-                changedItemId = url.getPathSegments().get(1);
-                break;
-
-            case SETTINGS:
-                if (whereArgs != null) {
-                    throw new IllegalArgumentException(
-                            "you aren't allowed to specify where args when updating settings");
-                }
-                if (userWhere != null) {
-                    throw new IllegalArgumentException(
-                            "you aren't allowed to specify a where string when updating settings");
-                }
-                return updateSettings(values);
-
-            case CALLS:
-                tableToChange = "calls";
-                changedItemId = null;
-                break;
-
-            case CALLS_ID:
-                tableToChange = "calls";
-                changedItemId = url.getPathSegments().get(1);
-                break;
-
-            default:
-                throw new UnsupportedOperationException("Cannot update URL: " + url);
-        }
-
-        String where = addIdToWhereClause(changedItemId, userWhere);
-        int numRowsUpdated = db.update(tableToChange, values, where, whereArgs);
-
-        if (numRowsUpdated > 0 && changedItemId != null) {
-            long itemId = Long.parseLong(changedItemId);
-            switch (matchedUriId) {
-                case ORGANIZATIONS_ID:
-                    fixupPrimaryAfterUpdate(
-                            Contacts.KIND_ORGANIZATION, null, itemId,
-                            values.getAsInteger(Organizations.ISPRIMARY));
-                    break;
-
-                case PHONES_ID:
-                case PEOPLE_PHONES_ID:
-                    fixupPrimaryAfterUpdate(
-                            Contacts.KIND_PHONE, matchedUriId == PEOPLE_PHONES_ID
-                                    ? Long.parseLong(url.getPathSegments().get(1))
-                                    : null, itemId,
-                            values.getAsInteger(Phones.ISPRIMARY));
-                    break;
-
-                case CONTACTMETHODS_ID:
-                case PEOPLE_CONTACTMETHODS_ID:
-                    IsPrimaryInfo isPrimaryInfo = lookupIsPrimaryInfo(sContactMethodsTable,
-                            sIsPrimaryProjectionWithKind, where, whereArgs);
-                    fixupPrimaryAfterUpdate(
-                            isPrimaryInfo.kind, isPrimaryInfo.person, itemId,
-                            values.getAsInteger(ContactMethods.ISPRIMARY));
-                    break;
-
-                case PEOPLE_ID:
-                    boolean hasStarred = values.containsKey(People.STARRED);
-                    boolean hasPrimaryPhone = values.containsKey(People.PRIMARY_PHONE_ID);
-                    boolean hasPrimaryOrganization =
-                            values.containsKey(People.PRIMARY_ORGANIZATION_ID);
-                    boolean hasPrimaryEmail = values.containsKey(People.PRIMARY_EMAIL_ID);
-                    if (hasStarred || hasPrimaryPhone || hasPrimaryOrganization
-                            || hasPrimaryEmail) {
-                        Cursor c = mDb.query(sPeopleTable, null,
-                                where, whereArgs, null, null, null);
-                        try {
-                            int indexAccountName = c.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
-                            int indexAccountType =
-                                    c.getColumnIndexOrThrow(People._SYNC_ACCOUNT_TYPE);
-                            int indexId = c.getColumnIndexOrThrow(People._ID);
-                            Long starredValue = values.getAsLong(People.STARRED);
-                            Long primaryPhone = values.getAsLong(People.PRIMARY_PHONE_ID);
-                            Long primaryOrganization =
-                                    values.getAsLong(People.PRIMARY_ORGANIZATION_ID);
-                            Long primaryEmail = values.getAsLong(People.PRIMARY_EMAIL_ID);
-                            while (c.moveToNext()) {
-                                final long personId = c.getLong(indexId);
-                                if (hasStarred) {
-                                    final String accountName = c.getString(indexAccountName);
-                                    final String accountType = c.getString(indexAccountType);
-                                    final Account account =
-                                            (accountName != null || accountType != null)
-                                                    ? new Account(accountName, accountType)
-                                                    : null;
-                                    fixupGroupMembershipAfterPeopleUpdate(account,
-                                            personId, starredValue != null && starredValue != 0);
-                                }
-
-                                if (hasPrimaryPhone) {
-                                    if (primaryPhone == null) {
-                                        throw new IllegalArgumentException(
-                                                "the value of PRIMARY_PHONE_ID must not be null");
-                                    }
-                                    setIsPrimary(Contacts.KIND_PHONE, personId, primaryPhone);
-                                }
-                                if (hasPrimaryOrganization) {
-                                    if (primaryOrganization == null) {
-                                        throw new IllegalArgumentException(
-                                                "the value of PRIMARY_ORGANIZATION_ID must "
-                                                        + "not be null");
-                                    }
-                                    setIsPrimary(Contacts.KIND_ORGANIZATION, personId,
-                                            primaryOrganization);
-                                }
-                                if (hasPrimaryEmail) {
-                                    if (primaryEmail == null) {
-                                        throw new IllegalArgumentException(
-                                                "the value of PRIMARY_EMAIL_ID must not be null");
-                                    }
-                                    setIsPrimary(Contacts.KIND_EMAIL, personId, primaryEmail);
-                                }
-                            }
-                        } finally {
-                            c.close();
-                        }
-                    }
-                    break;
-            }
-        }
-
-        return numRowsUpdated;
-    }
-
-    private int updateSettings(ContentValues values) {
-        final SQLiteDatabase db = getDatabase();
-        final String accountName = values.getAsString(Contacts.Settings._SYNC_ACCOUNT);
-        final String accountType = values.getAsString(Contacts.Settings._SYNC_ACCOUNT_TYPE);
-        final String key = values.getAsString(Contacts.Settings.KEY);
-        if (key == null) {
-            throw new IllegalArgumentException("you must specify the key when updating settings");
-        }
-        Account account = null;
-        if (accountName != null || accountType != null) {
-            account = new Account(accountName, accountType);
-        }
-        if (account == null) {
-            db.delete(sSettingsTable, "_sync_account IS NULL AND key=?", new String[]{key});
-        } else {
-            db.delete(sSettingsTable, "_sync_account=? AND _sync_account_type=? AND key=?",
-                    new String[]{account.name, account.type, key});
-        }
-        long rowId = db.insert(sSettingsTable, Contacts.Settings.KEY, values);
-        if (rowId < 0) {
-            throw new SQLException("error updating settings with " + values);
-        }
-        return 1;
-    }
-
-    private int updateGroups(ContentValues values, String where, String[] whereArgs) {
-        for (Map.Entry<String, Object> entry : values.valueSet()) {
-            final String column = entry.getKey();
-            if (!Groups.NAME.equals(column) && !Groups.NOTES.equals(column)
-                    && !Groups.SYSTEM_ID.equals(column) && !Groups.SHOULD_SYNC.equals(column)) {
-                throw new IllegalArgumentException(
-                        "you are not allowed to change column " + column);
-            }
-        }
-
-        Set<Account> modifiedAccounts = Sets.newHashSet();
-        final SQLiteDatabase db = getDatabase();
-        if (values.containsKey(Groups.NAME) || values.containsKey(Groups.SHOULD_SYNC)) {
-            String newName = values.getAsString(Groups.NAME);
-            Cursor cursor = db.query(sGroupsTable, null, where, whereArgs, null, null, null);
-            try {
-                final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME);
-                final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT);
-                final int indexSyncAccountType =
-                        cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE);
-                final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID);
-                final int indexId = cursor.getColumnIndexOrThrow(Groups._ID);
-                while (cursor.moveToNext()) {
-                    String accountName = cursor.getString(indexSyncAccount);
-                    String accountType = cursor.getString(indexSyncAccountType);
-                    if (values.containsKey(Groups.NAME)) {
-                        String oldName = cursor.getString(indexName);
-                        String syncId = cursor.getString(indexSyncId);
-                        long id = cursor.getLong(indexId);
-                        fixupPeopleStarredOnGroupRename(oldName, newName, id);
-                        if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(syncId)) {
-                            fixupPeopleStarredOnGroupRename(oldName, newName,
-                                    new Account(accountName, accountType), syncId);
-                        }
-                    }
-                    if (!TextUtils.isEmpty(accountName) && values.containsKey(Groups.SHOULD_SYNC)) {
-                        modifiedAccounts.add(new Account(accountName, accountType));
-                    }
-                }
-            } finally {
-                cursor.close();
-            }
-        }
-
-        int numRows = db.update(sGroupsTable, values, where, whereArgs);
-        if (numRows > 0) {
-            if (!isTemporary()) {
-                final ContentResolver cr = getContext().getContentResolver();
-                for (Account account : modifiedAccounts) {
-                    onLocalChangesForAccount(cr, account, true);
-                }
-            }
-        }
-        return numRows;
-    }
-
-    void fixupPeopleStarredOnGroupRename(String oldName, String newName,
-            String where, String[] whereArgs) {
-        if (TextUtils.equals(oldName, newName)) return;
-
-        int starredValue;
-        if (Groups.GROUP_ANDROID_STARRED.equals(newName)) {
-            starredValue = 1;
-        } else if (Groups.GROUP_ANDROID_STARRED.equals(oldName)) {
-            starredValue = 0;
-        } else {
-            return;
-        }
-
-        getDatabase().execSQL("UPDATE people SET starred=" + starredValue + " WHERE _id in ("
-                + "SELECT person "
-                + "FROM groups, groupmembership "
-                + "WHERE " + where + " AND " + sGroupsJoinString + ")",
-                whereArgs);
-    }
-
-    void fixupPeopleStarredOnGroupRename(String oldName, String newName,
-            Account syncAccount, String syncId) {
-        fixupPeopleStarredOnGroupRename(oldName, newName,
-                "_sync_account=? AND _sync_account_type=? AND _sync_id=?",
-                new String[]{syncAccount.name, syncAccount.type, syncId});
-    }
-
-    void fixupPeopleStarredOnGroupRename(String oldName, String newName, long groupId) {
-        fixupPeopleStarredOnGroupRename(oldName, newName, "group_id=?",
-                new String[]{String.valueOf(groupId)});
-    }
-
-    private void fixupPrimaryAfterUpdate(int kind, Long personId, Long changedItemId,
-            Integer isPrimaryValue) {
-        final String table = kindToTable(kind);
-
-        // - when you update isPrimary to true,
-        //   make the changed item the primary, clear others
-        // - when you update isPrimary to false,
-        //   select a new one as isPrimary, clear the primary if no more phones
-        if (isPrimaryValue != null) {
-            if (personId == null) {
-                personId = lookupPerson(table, changedItemId);
-            }
-
-            boolean isPrimary = isPrimaryValue != 0;
-            Long newPrimary = changedItemId;
-            if (!isPrimary) {
-                newPrimary = findNewPrimary(kind, personId, changedItemId);
-            }
-            clearOtherIsPrimary(kind, personId, changedItemId);
-
-            if (kind == Contacts.KIND_PHONE) {
-                updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newPrimary);
-            } else if (kind == Contacts.KIND_EMAIL) {
-                updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newPrimary);
-            } else if (kind == Contacts.KIND_ORGANIZATION) {
-                updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newPrimary);
-            }
-        }
-    }
-
-    /**
-     * Queries table to find the value of the person column for the row with _id. There must
-     * be exactly one row that matches this id.
-     * @param table the table to query
-     * @param id the id of the row to query
-     * @return the value of the person column for the specified row, returned as a String.
-     */
-    private long lookupPerson(String table, long id) {
-        return DatabaseUtils.longForQuery(
-                getDatabase(),
-                "SELECT person FROM " + table + " where _id=" + id,
-                null);
-    }
-
-    /**
-     * Used to pass around information about a row that has the isprimary column.
-     */
-    private class IsPrimaryInfo {
-        boolean isPrimary;
-        Long person;
-        Long id;
-        Integer kind;
-    }
-
-    /**
-     * Queries the table to determine the state of the row's isprimary column and the kind.
-     * The where and whereArgs must be sufficient to match either 0 or 1 row.
-     * @param table the table of rows to consider, supports "phones" and "contact_methods"
-     * @param projection the projection to use to get the columns that pertain to table
-     * @param where used in conjunction with the whereArgs to identify the row
-     * @param where used in conjunction with the where string to identify the row
-     * @return the IsPrimaryInfo about the matched row, or null if no row was matched
-     */
-    private IsPrimaryInfo lookupIsPrimaryInfo(String table, String[] projection, String where,
-            String[] whereArgs) {
-        Cursor cursor = getDatabase().query(table, projection, where, whereArgs, null, null, null);
-        try {
-            if (!(cursor.getCount() <= 1)) {
-                throw new IllegalArgumentException("expected only zero or one rows, got "
-                        + DatabaseUtils.dumpCursorToString(cursor));
-            }
-            if (!cursor.moveToFirst()) return null;
-            IsPrimaryInfo info = new IsPrimaryInfo();
-            info.isPrimary = cursor.getInt(0) != 0;
-            info.person = cursor.getLong(1);
-            info.id = cursor.getLong(2);
-            if (projection == sIsPrimaryProjectionWithKind) {
-                info.kind = cursor.getInt(3);
-            } else {
-                if (sPhonesTable.equals(table)) {
-                    info.kind = Contacts.KIND_PHONE;
-                } else if (sOrganizationsTable.equals(table)) {
-                    info.kind = Contacts.KIND_ORGANIZATION;
-                } else {
-                    throw new IllegalArgumentException("unexpected table, " + table);
-                }
-            }
-            return info;
-        } finally {
-            cursor.close();
-        }
-    }
-
-    /**
-     * Returns the rank of the table-specific type, used when deciding which row
-     * should be primary when none are primary. The lower the rank the better the type.
-     * @param table supports "phones", "contact_methods" and "organizations"
-     * @param type the table-specific type from the TYPE column
-     * @return the rank of the table-specific type, the lower the better
-     */
-    private int getRankOfType(String table, int type) {
-        if (table.equals(sPhonesTable)) {
-            switch (type) {
-                case Contacts.Phones.TYPE_MOBILE: return 0;
-                case Contacts.Phones.TYPE_WORK: return 1;
-                case Contacts.Phones.TYPE_HOME: return 2;
-                case Contacts.Phones.TYPE_PAGER: return 3;
-                case Contacts.Phones.TYPE_CUSTOM: return 4;
-                case Contacts.Phones.TYPE_OTHER: return 5;
-                case Contacts.Phones.TYPE_FAX_WORK: return 6;
-                case Contacts.Phones.TYPE_FAX_HOME: return 7;
-                default: return 1000;
-            }
-        }
-
-        if (table.equals(sContactMethodsTable)) {
-            switch (type) {
-                case Contacts.ContactMethods.TYPE_HOME: return 0;
-                case Contacts.ContactMethods.TYPE_WORK: return 1;
-                case Contacts.ContactMethods.TYPE_CUSTOM: return 2;
-                case Contacts.ContactMethods.TYPE_OTHER: return 3;
-                default: return 1000;
-            }
-        }
-
-        if (table.equals(sOrganizationsTable)) {
-            switch (type) {
-                case Organizations.TYPE_WORK: return 0;
-                case Organizations.TYPE_CUSTOM: return 1;
-                case Organizations.TYPE_OTHER: return 2;
-                default: return 1000;
-            }
-        }
-
-        throw new IllegalArgumentException("unexpected table, " + table);
-    }
-
-    /**
-     * Determines which of the rows in table for the personId should be picked as the primary
-     * row based on the rank of the row's type.
-     * @param kind the kind of contact
-     * @param personId used to limit the rows to those pertaining to this person
-     * @param itemId optional, a row to ignore
-     * @return the _id of the row that should be the new primary. Is null if there are no
-     *   matching rows.
-     */
-    private Long findNewPrimary(int kind, Long personId, Long itemId) {
-        final String table = kindToTable(kind);
-        if (personId == null) throw new IllegalArgumentException("personId must not be null");
-        StringBuilder sb = new StringBuilder();
-        sb.append("person=");
-        sb.append(personId);
-        if (itemId != null) {
-            sb.append(" and _id!=");
-            sb.append(itemId);
-        }
-        if (sContactMethodsTable.equals(table)) {
-            sb.append(" and ");
-            sb.append(ContactMethods.KIND);
-            sb.append("=");
-            sb.append(kind);
-        }
-
-        Cursor cursor = getDatabase().query(table, ID_TYPE_PROJECTION, sb.toString(),
-                null, null, null, null);
-        try {
-            Long newPrimaryId = null;
-            int bestRank = -1;
-            while (cursor.moveToNext()) {
-                final int rank = getRankOfType(table, cursor.getInt(1));
-                if (bestRank == -1 || rank < bestRank) {
-                    newPrimaryId = cursor.getLong(0);
-                    bestRank = rank;
-                }
-            }
-            return newPrimaryId;
-        } finally {
-            cursor.close();
-        }
-    }
-
-    private void setIsPrimary(int kind, long personId, long itemId) {
-        final String table = kindToTable(kind);
-        StringBuilder sb = new StringBuilder();
-        sb.append("person=");
-        sb.append(personId);
-
-        if (sContactMethodsTable.equals(table)) {
-            sb.append(" and ");
-            sb.append(ContactMethods.KIND);
-            sb.append("=");
-            sb.append(kind);
-        }
-
-        final String where = sb.toString();
-        getDatabase().execSQL(
-                "UPDATE " + table + " SET isprimary=(_id=" + itemId + ") WHERE " + where);
-    }
-
-    /**
-     * Clears the isprimary flag for all rows other than the itemId.
-     * @param kind the kind of item
-     * @param personId used to limit the updates to rows pertaining to this person
-     * @param itemId which row to leave untouched
-     */
-    private void clearOtherIsPrimary(int kind, Long personId, Long itemId) {
-        final String table = kindToTable(kind);
-        if (personId == null) throw new IllegalArgumentException("personId must not be null");
-        StringBuilder sb = new StringBuilder();
-        sb.append("person=");
-        sb.append(personId);
-        if (itemId != null) {
-            sb.append(" and _id!=");
-            sb.append(itemId);
-        }
-        if (sContactMethodsTable.equals(table)) {
-            sb.append(" and ");
-            sb.append(ContactMethods.KIND);
-            sb.append("=");
-            sb.append(kind);
-        }
-
-        mValuesLocal.clear();
-        mValuesLocal.put("isprimary", 0);
-        getDatabase().update(table, mValuesLocal, sb.toString(), null);
-    }
-
-    /**
-     * Set the specified primary column for the person. This is used to make the people
-     * row reflect the isprimary flag in the people or contactmethods tables, which is
-     * authoritative.
-     * @param personId the person to modify
-     * @param column the name of the primary column (phone or email)
-     * @param primaryId the new value to write into the primary column
-     */
-    private void updatePeoplePrimary(Long personId, String column, Long primaryId) {
-        mValuesLocal.clear();
-        mValuesLocal.put(column, primaryId);
-        getDatabase().update(sPeopleTable, mValuesLocal, "_id=" + personId, null);
-    }
-
-    private static String addIdToWhereClause(String id, String where) {
-        if (id != null) {
-            StringBuilder whereSb = new StringBuilder("_id=");
-            whereSb.append(id);
-            if (!TextUtils.isEmpty(where)) {
-                whereSb.append(" AND (");
-                whereSb.append(where);
-                whereSb.append(')');
-            }
-            return whereSb.toString();
-        } else {
-            return where;
-        }
-    }
-
-    private boolean queryGroupMembershipContainsStarred(long personId) {
-        // TODO: Part 1 of 2 part hack to work around a bug in reusing SQLiteStatements
-        SQLiteStatement mGroupsMembershipQuery = null;
-
-        if (mGroupsMembershipQuery == null) {
-            String query =
-                "SELECT COUNT(*) FROM groups, groupmembership WHERE "
-                + sGroupsJoinString + " AND person=? AND groups.name=?";
-            mGroupsMembershipQuery = getDatabase().compileStatement(query);
-        }
-        long result = DatabaseUtils.longForQuery(mGroupsMembershipQuery,
-                new String[]{String.valueOf(personId), Groups.GROUP_ANDROID_STARRED});
-
-        // TODO: Part 2 of 2 part hack to work around a bug in reusing SQLiteStatements
-        mGroupsMembershipQuery.close();
-
-        return result != 0;
-    }
-
-    @Override
-    public boolean changeRequiresLocalSync(Uri uri) {
-        final int match = sURIMatcher.match(uri);
-        switch (match) {
-            // Changes to these URIs cannot cause syncable data to be changed, so don't
-            // bother trying to sync them.
-            case CALLS:
-            case CALLS_FILTER:
-            case CALLS_ID:
-            case PRESENCE:
-            case PRESENCE_ID:
-            case PEOPLE_UPDATE_CONTACT_TIME:
-                return false;
-
-            default:
-                return true;
-        }
-    }
-
-    @Override
-    protected Iterable<? extends AbstractTableMerger> getMergers() {
-        ArrayList<AbstractTableMerger> list = new ArrayList<AbstractTableMerger> ();
-        list.add(new PersonMerger());
-        list.add(new GroupMerger());
-        list.add(new PhotoMerger());
-        return list;
-    }
-
-    protected static String sPeopleTable = "people";
-    protected static Uri sPeopleRawURL = Uri.parse("content://contacts/people/raw/");
-    protected static String sDeletedPeopleTable = "_deleted_people";
-    protected static Uri sDeletedPeopleURL = Uri.parse("content://contacts/deleted_people/");
-    protected static String sGroupsTable = "groups";
-    protected static String sSettingsTable = "settings";
-    protected static Uri sGroupsURL = Uri.parse("content://contacts/groups/");
-    protected static String sDeletedGroupsTable = "_deleted_groups";
-    protected static Uri sDeletedGroupsURL =
-            Uri.parse("content://contacts/deleted_groups/");
-    protected static String sPhonesTable = "phones";
-    protected static String sOrganizationsTable = "organizations";
-    protected static String sContactMethodsTable = "contact_methods";
-    protected static String sGroupmembershipTable = "groupmembership";
-    protected static String sPhotosTable = "photos";
-    protected static Uri sPhotosURL = Uri.parse("content://contacts/photos/");
-    protected static String sExtensionsTable = "extensions";
-    protected static String sCallsTable = "calls";
-
-    protected class PersonMerger extends AbstractTableMerger
-    {
-        private ContentValues mValues = new ContentValues();
-        Map<String, SQLiteCursor> mCursorMap = Maps.newHashMap();
-        public PersonMerger()
-        {
-            super(getDatabase(),
-                    sPeopleTable, sPeopleRawURL, sDeletedPeopleTable, sDeletedPeopleURL);
-        }
-
-        @Override
-        protected void notifyChanges() {
-            // notify that a change has occurred.
-            getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI,
-                    null /* observer */, false /* do not sync to network */);
-        }
-
-        @Override
-        public void insertRow(ContentProvider diffs, Cursor diffsCursor) {
-            final SQLiteDatabase db = getDatabase();
-
-            Long localPrimaryPhoneId = null;
-            Long localPrimaryEmailId = null;
-            Long localPrimaryOrganizationId = null;
-
-            // Copy the person
-            mPeopleInserter.prepareForInsert();
-            DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
-                    People._SYNC_ID, mPeopleInserter, mIndexPeopleSyncId);
-            DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
-                    People._SYNC_TIME, mPeopleInserter, mIndexPeopleSyncTime);
-            DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
-                    People._SYNC_VERSION, mPeopleInserter, mIndexPeopleSyncVersion);
-            DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
-                    People._SYNC_DIRTY, mPeopleInserter, mIndexPeopleSyncDirty);
-            DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
-                    People._SYNC_ACCOUNT, mPeopleInserter, mIndexPeopleSyncAccountName);
-            DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
-                    People._SYNC_ACCOUNT_TYPE, mPeopleInserter, mIndexPeopleSyncAccountType);
-            DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
-                    People.NAME, mPeopleInserter, mIndexPeopleName);
-            DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
-                    People.PHONETIC_NAME, mPeopleInserter, mIndexPeoplePhoneticName);
-            DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
-                    People.NOTES, mPeopleInserter, mIndexPeopleNotes);
-            long localPersonID = mPeopleInserter.execute();
-
-            Cursor c;
-            final SQLiteDatabase diffsDb = ((ContactsProvider) diffs).getDatabase();
-            long diffsPersonID = diffsCursor.getLong(diffsCursor.getColumnIndexOrThrow(People._ID));
-
-            // Copy the Photo info
-            c = doSubQuery(diffsDb, sPhotosTable, null, diffsPersonID, null);
-            try {
-                if (c.moveToNext()) {
-                    mDb.delete(sPhotosTable, "person=" + localPersonID, null);
-                    mPhotosInserter.prepareForInsert();
-                    DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ID,
-                            mPhotosInserter, mIndexPhotosSyncId);
-                    DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_TIME,
-                            mPhotosInserter, mIndexPhotosSyncTime);
-                    DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_VERSION,
-                            mPhotosInserter, mIndexPhotosSyncVersion);
-                    DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ACCOUNT,
-                            mPhotosInserter, mIndexPhotosSyncAccountName);
-                    DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ACCOUNT_TYPE,
-                            mPhotosInserter, mIndexPhotosSyncAccountType);
-                    DatabaseUtils.cursorStringToInsertHelper(c, Photos.EXISTS_ON_SERVER,
-                            mPhotosInserter, mIndexPhotosExistsOnServer);
-                    mPhotosInserter.bind(mIndexPhotosSyncError, (String)null);
-                    mPhotosInserter.bind(mIndexPhotosSyncDirty, 0);
-                    mPhotosInserter.bind(mIndexPhotosPersonId, localPersonID);
-                    mPhotosInserter.execute();
-                }
-            } finally {
-                c.deactivate();
-            }
-
-            // Copy all phones
-            c = doSubQuery(diffsDb, sPhonesTable, null, diffsPersonID, sPhonesTable + "._id");
-            if (c != null) {
-                Long newPrimaryId = null;
-                int bestRank = -1;
-                final int labelIndex = c.getColumnIndexOrThrow(Phones.LABEL);
-                final int typeIndex = c.getColumnIndexOrThrow(Phones.TYPE);
-                final int numberIndex = c.getColumnIndexOrThrow(Phones.NUMBER);
-                final int keyIndex = c.getColumnIndexOrThrow(Phones.NUMBER_KEY);
-                final int primaryIndex = c.getColumnIndexOrThrow(Phones.ISPRIMARY);
-                while(c.moveToNext()) {
-                    final int type = c.getInt(typeIndex);
-                    final int isPrimaryValue = c.getInt(primaryIndex);
-                    mPhonesInserter.prepareForInsert();
-                    mPhonesInserter.bind(mIndexPhonesPersonId, localPersonID);
-                    mPhonesInserter.bind(mIndexPhonesLabel, c.getString(labelIndex));
-                    mPhonesInserter.bind(mIndexPhonesType, type);
-                    mPhonesInserter.bind(mIndexPhonesNumber, c.getString(numberIndex));
-                    mPhonesInserter.bind(mIndexPhonesNumberKey, c.getString(keyIndex));
-                    mPhonesInserter.bind(mIndexPhonesIsPrimary, isPrimaryValue);
-                    long rowId = mPhonesInserter.execute();
-
-                    if (isPrimaryValue != 0) {
-                        if (localPrimaryPhoneId != null) {
-                            throw new IllegalArgumentException(
-                                    "more than one phone was marked as primary, "
-                                            + DatabaseUtils.dumpCursorToString(c));
-                        }
-                        localPrimaryPhoneId = rowId;
-                    }
-
-                    if (localPrimaryPhoneId == null) {
-                        final int rank = getRankOfType(sPhonesTable, type);
-                        if (bestRank == -1 || rank < bestRank) {
-                            newPrimaryId = rowId;
-                            bestRank = rank;
-                        }
-                    }
-                }
-                c.deactivate();
-
-                if (localPrimaryPhoneId == null) {
-                    localPrimaryPhoneId = newPrimaryId;
-                }
-            }
-
-            // Copy all contact_methods
-            c = doSubQuery(diffsDb, sContactMethodsTable, null, diffsPersonID,
-                    sContactMethodsTable + "._id");
-            if (c != null) {
-                Long newPrimaryId = null;
-                int bestRank = -1;
-                final int labelIndex = c.getColumnIndexOrThrow(ContactMethods.LABEL);
-                final int kindIndex = c.getColumnIndexOrThrow(ContactMethods.KIND);
-                final int typeIndex = c.getColumnIndexOrThrow(ContactMethods.TYPE);
-                final int dataIndex = c.getColumnIndexOrThrow(ContactMethods.DATA);
-                final int auxDataIndex = c.getColumnIndexOrThrow(ContactMethods.AUX_DATA);
-                final int primaryIndex = c.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
-                while(c.moveToNext()) {
-                    final int type = c.getInt(typeIndex);
-                    final int kind = c.getInt(kindIndex);
-                    final int isPrimaryValue = c.getInt(primaryIndex);
-                    mContactMethodsInserter.prepareForInsert();
-                    mContactMethodsInserter.bind(mIndexContactMethodsPersonId, localPersonID);
-                    mContactMethodsInserter.bind(mIndexContactMethodsLabel,
-                            c.getString(labelIndex));
-                    mContactMethodsInserter.bind(mIndexContactMethodsKind, kind);
-                    mContactMethodsInserter.bind(mIndexContactMethodsType, type);
-                    mContactMethodsInserter.bind(mIndexContactMethodsData, c.getString(dataIndex));
-                    mContactMethodsInserter.bind(mIndexContactMethodsAuxData,
-                            c.getString(auxDataIndex));
-                    mContactMethodsInserter.bind(mIndexContactMethodsIsPrimary, isPrimaryValue);
-                    long rowId = mContactMethodsInserter.execute();
-                    if ((kind == Contacts.KIND_EMAIL) && (isPrimaryValue != 0)) {
-                        if (localPrimaryEmailId != null) {
-                            throw new IllegalArgumentException(
-                                    "more than one email was marked as primary, "
-                                            + DatabaseUtils.dumpCursorToString(c));
-                        }
-                        localPrimaryEmailId = rowId;
-                    }
-
-                    if (localPrimaryEmailId == null) {
-                        final int rank = getRankOfType(sContactMethodsTable, type);
-                        if (bestRank == -1 || rank < bestRank) {
-                            newPrimaryId = rowId;
-                            bestRank = rank;
-                        }
-                    }
-                }
-                c.deactivate();
-
-                if (localPrimaryEmailId == null) {
-                    localPrimaryEmailId = newPrimaryId;
-                }
-            }
-
-            // Copy all organizations
-            c = doSubQuery(diffsDb, sOrganizationsTable, null, diffsPersonID,
-                    sOrganizationsTable + "._id");
-            try {
-                Long newPrimaryId = null;
-                int bestRank = -1;
-                final int labelIndex = c.getColumnIndexOrThrow(Organizations.LABEL);
-                final int typeIndex = c.getColumnIndexOrThrow(Organizations.TYPE);
-                final int companyIndex = c.getColumnIndexOrThrow(Organizations.COMPANY);
-                final int titleIndex = c.getColumnIndexOrThrow(Organizations.TITLE);
-                final int primaryIndex = c.getColumnIndexOrThrow(Organizations.ISPRIMARY);
-                while(c.moveToNext()) {
-                    final int type = c.getInt(typeIndex);
-                    final int isPrimaryValue = c.getInt(primaryIndex);
-                    mOrganizationsInserter.prepareForInsert();
-                    mOrganizationsInserter.bind(mIndexOrganizationsPersonId, localPersonID);
-                    mOrganizationsInserter.bind(mIndexOrganizationsLabel, c.getString(labelIndex));
-                    mOrganizationsInserter.bind(mIndexOrganizationsType, type);
-                    mOrganizationsInserter.bind(mIndexOrganizationsCompany,
-                            c.getString(companyIndex));
-                    mOrganizationsInserter.bind(mIndexOrganizationsTitle, c.getString(titleIndex));
-                    mOrganizationsInserter.bind(mIndexOrganizationsIsPrimary, isPrimaryValue);
-                    long rowId = mOrganizationsInserter.execute();
-                    if (isPrimaryValue != 0) {
-                        if (localPrimaryOrganizationId != null) {
-                            throw new IllegalArgumentException(
-                                    "more than one organization was marked as primary, "
-                                            + DatabaseUtils.dumpCursorToString(c));
-                        }
-                        localPrimaryOrganizationId = rowId;
-                    }
-
-                    if (localPrimaryOrganizationId == null) {
-                        final int rank = getRankOfType(sOrganizationsTable, type);
-                        if (bestRank == -1 || rank < bestRank) {
-                            newPrimaryId = rowId;
-                            bestRank = rank;
-                        }
-                    }
-                }
-
-                if (localPrimaryOrganizationId == null) {
-                    localPrimaryOrganizationId = newPrimaryId;
-                }
-            } finally {
-                c.deactivate();
-            }
-
-            // Copy all groupmembership rows
-            c = doSubQuery(diffsDb, sGroupmembershipTable, null, diffsPersonID,
-                    sGroupmembershipTable + "._id");
-            try {
-                final int accountNameIndex =
-                    c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ACCOUNT);
-                final int accountTypeIndex =
-                    c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
-                final int idIndex = c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ID);
-                while(c.moveToNext()) {
-                    mGroupMembershipInserter.prepareForInsert();
-                    mGroupMembershipInserter.bind(mIndexGroupMembershipPersonId, localPersonID);
-                    mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccountName,
-                            c.getString(accountNameIndex));
-                    mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccountType,
-                            c.getString(accountTypeIndex));
-                    mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncId,
-                            c.getString(idIndex));
-                    mGroupMembershipInserter.execute();
-                }
-            } finally {
-                c.deactivate();
-            }
-
-            // Copy all extensions rows
-            c = doSubQuery(diffsDb, sExtensionsTable, null, diffsPersonID,
-                    sExtensionsTable + "._id");
-            try {
-                final int nameIndex = c.getColumnIndexOrThrow(Extensions.NAME);
-                final int valueIndex = c.getColumnIndexOrThrow(Extensions.VALUE);
-                while(c.moveToNext()) {
-                    mExtensionsInserter.prepareForInsert();
-                    mExtensionsInserter.bind(mIndexExtensionsPersonId, localPersonID);
-                    mExtensionsInserter.bind(mIndexExtensionsName, c.getString(nameIndex));
-                    mExtensionsInserter.bind(mIndexExtensionsValue, c.getString(valueIndex));
-                    mExtensionsInserter.execute();
-                }
-            } finally {
-                c.deactivate();
-            }
-
-            // Update the _SYNC_DIRTY flag of the person. We have to do this
-            // after inserting since the updated of the phones, contact
-            // methods and organizations will fire a sql trigger that will
-            // cause this flag to be set.
-            mValues.clear();
-            mValues.put(People._SYNC_DIRTY, 0);
-            mValues.put(People.PRIMARY_PHONE_ID, localPrimaryPhoneId);
-            mValues.put(People.PRIMARY_EMAIL_ID, localPrimaryEmailId);
-            mValues.put(People.PRIMARY_ORGANIZATION_ID, localPrimaryOrganizationId);
-            final boolean isStarred = queryGroupMembershipContainsStarred(localPersonID);
-            mValues.put(People.STARRED, isStarred ? 1 : 0);
-            db.update(mTable, mValues, People._ID + '=' + localPersonID, null);
-        }
-
-        @Override
-        public void updateRow(long localPersonID, ContentProvider diffs, Cursor diffsCursor) {
-            updateOrResolveRow(localPersonID, null, diffs, diffsCursor, false);
-        }
-
-        @Override
-        public void resolveRow(long localPersonID, String syncID,
-                ContentProvider diffs, Cursor diffsCursor) {
-            updateOrResolveRow(localPersonID, syncID, diffs, diffsCursor, true);
-        }
-
-        protected void updateOrResolveRow(long localPersonID, String syncID,
-                ContentProvider diffs, Cursor diffsCursor, boolean conflicts) {
-            final SQLiteDatabase db = getDatabase();
-            // The local version of localPersonId's record has changed. This
-            // person also has a changed record in the diffs. Merge the changes
-            // in the following way:
-            //  - if any fields in the people table changed use the server's
-            //    version
-            //  - for phones, emails, addresses, compute the join of all unique
-            //    subrecords. If any of the subrecords has changes in both
-            //    places then choose the server version of the subrecord
-            //
-            // Limitation: deletes of phones, emails, or addresses are ignored
-            // when the record has changed on both the client and the server
-
-            long diffsPersonID = diffsCursor.getLong(diffsCursor.getColumnIndexOrThrow("_id"));
-
-            // Join the server phones, organizations, and contact_methods with the local ones.
-            //  - Add locally any that exist only on the server.
-            //  - If the row conflicts, delete locally any that exist only on the client.
-            //  - If the row doesn't conflict, ignore any that exist only on the client.
-            //  - Update any that exist in both places.
-
-            Map<Integer, Long> primaryLocal = new HashMap<Integer, Long>();
-            Map<Integer, Long> primaryDiffs = new HashMap<Integer, Long>();
-
-            Cursor cRemote;
-            Cursor cLocal;
-
-            // Phones
-            cRemote = null;
-            cLocal = null;
-            final SQLiteDatabase diffsDb = ((ContactsProvider) diffs).getDatabase();
-            try {
-                cLocal = doSubQuery(db, sPhonesTable, null, localPersonID, sPhonesKeyOrderBy);
-                cRemote = doSubQuery(diffsDb, sPhonesTable,
-                        null, diffsPersonID, sPhonesKeyOrderBy);
-
-                final int idColLocal = cLocal.getColumnIndexOrThrow(Phones._ID);
-                final int isPrimaryColLocal = cLocal.getColumnIndexOrThrow(Phones.ISPRIMARY);
-                final int isPrimaryColRemote = cRemote.getColumnIndexOrThrow(Phones.ISPRIMARY);
-
-                CursorJoiner joiner =
-                        new CursorJoiner(cLocal, sPhonesKeyColumns, cRemote, sPhonesKeyColumns);
-                for (CursorJoiner.Result joinResult : joiner) {
-                    switch(joinResult) {
-                        case LEFT:
-                            if (!conflicts) {
-                                db.delete(sPhonesTable,
-                                        Phones._ID + "=" + cLocal.getLong(idColLocal), null);
-                            } else {
-                                if (cLocal.getLong(isPrimaryColLocal) != 0) {
-                                    savePrimaryId(primaryLocal, Contacts.KIND_PHONE,
-                                            cLocal.getLong(idColLocal));
-                                }
-                            }
-                            break;
-
-                        case RIGHT:
-                        case BOTH:
-                            mValues.clear();
-                            DatabaseUtils.cursorIntToContentValues(
-                                    cRemote, Phones.TYPE, mValues);
-                            DatabaseUtils.cursorStringToContentValues(
-                                    cRemote, Phones.LABEL, mValues);
-                            DatabaseUtils.cursorStringToContentValues(
-                                    cRemote, Phones.NUMBER, mValues);
-                            DatabaseUtils.cursorStringToContentValues(
-                                    cRemote, Phones.NUMBER_KEY, mValues);
-                            DatabaseUtils.cursorIntToContentValues(
-                                    cRemote, Phones.ISPRIMARY, mValues);
-
-                            long localId;
-                            if (joinResult == CursorJoiner.Result.RIGHT) {
-                                mValues.put(Phones.PERSON_ID, localPersonID);
-                                localId = mPhonesInserter.insert(mValues);
-                            } else {
-                                localId = cLocal.getLong(idColLocal);
-                                db.update(sPhonesTable, mValues, "_id =" + localId, null);
-                            }
-                            if (cRemote.getLong(isPrimaryColRemote) != 0) {
-                                savePrimaryId(primaryDiffs, Contacts.KIND_PHONE, localId);
-                            }
-                            break;
-                    }
-                }
-            } finally {
-                if (cRemote != null) cRemote.deactivate();
-                if (cLocal != null) cLocal.deactivate();
-            }
-
-            // Contact methods
-            cRemote = null;
-            cLocal = null;
-            try {
-                cLocal = doSubQuery(db,
-                        sContactMethodsTable, null, localPersonID, sContactMethodsKeyOrderBy);
-                cRemote = doSubQuery(diffsDb,
-                        sContactMethodsTable, null, diffsPersonID, sContactMethodsKeyOrderBy);
-
-                final int idColLocal = cLocal.getColumnIndexOrThrow(ContactMethods._ID);
-                final int kindColLocal = cLocal.getColumnIndexOrThrow(ContactMethods.KIND);
-                final int kindColRemote = cRemote.getColumnIndexOrThrow(ContactMethods.KIND);
-                final int isPrimaryColLocal =
-                        cLocal.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
-                final int isPrimaryColRemote =
-                        cRemote.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
-
-                CursorJoiner joiner = new CursorJoiner(
-                        cLocal, sContactMethodsKeyColumns, cRemote, sContactMethodsKeyColumns);
-                for (CursorJoiner.Result joinResult : joiner) {
-                    switch(joinResult) {
-                        case LEFT:
-                            if (!conflicts) {
-                                db.delete(sContactMethodsTable, ContactMethods._ID + "="
-                                        + cLocal.getLong(idColLocal), null);
-                            } else {
-                                if (cLocal.getLong(isPrimaryColLocal) != 0) {
-                                    savePrimaryId(primaryLocal, cLocal.getInt(kindColLocal),
-                                            cLocal.getLong(idColLocal));
-                                }
-                            }
-                            break;
-
-                        case RIGHT:
-                        case BOTH:
-                            mValues.clear();
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    ContactMethods.LABEL, mValues);
-                            DatabaseUtils.cursorIntToContentValues(cRemote,
-                                    ContactMethods.TYPE, mValues);
-                            DatabaseUtils.cursorIntToContentValues(cRemote,
-                                    ContactMethods.KIND, mValues);
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    ContactMethods.DATA, mValues);
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    ContactMethods.AUX_DATA, mValues);
-                            DatabaseUtils.cursorIntToContentValues(cRemote,
-                                    ContactMethods.ISPRIMARY, mValues);
-
-                            long localId;
-                            if (joinResult == CursorJoiner.Result.RIGHT) {
-                                mValues.put(ContactMethods.PERSON_ID, localPersonID);
-                                localId = mContactMethodsInserter.insert(mValues);
-                            } else {
-                                localId = cLocal.getLong(idColLocal);
-                                db.update(sContactMethodsTable, mValues, "_id =" + localId, null);
-                            }
-                            if (cRemote.getLong(isPrimaryColRemote) != 0) {
-                                savePrimaryId(primaryDiffs, cRemote.getInt(kindColRemote), localId);
-                            }
-                            break;
-                    }
-                }
-            } finally {
-                if (cRemote != null) cRemote.deactivate();
-                if (cLocal != null) cLocal.deactivate();
-            }
-
-            // Organizations
-            cRemote = null;
-            cLocal = null;
-            try {
-                cLocal = doSubQuery(db,
-                        sOrganizationsTable, null, localPersonID, sOrganizationsKeyOrderBy);
-                cRemote = doSubQuery(diffsDb,
-                        sOrganizationsTable, null, diffsPersonID, sOrganizationsKeyOrderBy);
-
-                final int idColLocal = cLocal.getColumnIndexOrThrow(Organizations._ID);
-                final int isPrimaryColLocal =
-                        cLocal.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
-                final int isPrimaryColRemote =
-                        cRemote.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
-                CursorJoiner joiner = new CursorJoiner(
-                        cLocal, sOrganizationsKeyColumns, cRemote, sOrganizationsKeyColumns);
-                for (CursorJoiner.Result joinResult : joiner) {
-                    switch(joinResult) {
-                        case LEFT:
-                            if (!conflicts) {
-                                db.delete(sOrganizationsTable,
-                                        Phones._ID + "=" + cLocal.getLong(idColLocal), null);
-                            } else {
-                                if (cLocal.getLong(isPrimaryColLocal) != 0) {
-                                    savePrimaryId(primaryLocal, Contacts.KIND_ORGANIZATION,
-                                            cLocal.getLong(idColLocal));
-                                }
-                            }
-                            break;
-
-                        case RIGHT:
-                        case BOTH:
-                            mValues.clear();
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    Organizations.LABEL, mValues);
-                            DatabaseUtils.cursorIntToContentValues(cRemote,
-                                    Organizations.TYPE, mValues);
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    Organizations.COMPANY, mValues);
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    Organizations.TITLE, mValues);
-                            DatabaseUtils.cursorIntToContentValues(cRemote,
-                                    Organizations.ISPRIMARY, mValues);
-                            long localId;
-                            if (joinResult == CursorJoiner.Result.RIGHT) {
-                                mValues.put(Organizations.PERSON_ID, localPersonID);
-                                localId = mOrganizationsInserter.insert(mValues);
-                            } else {
-                                localId = cLocal.getLong(idColLocal);
-                                db.update(sOrganizationsTable, mValues,
-                                        "_id =" + localId, null /* whereArgs */);
-                            }
-                            if (cRemote.getLong(isPrimaryColRemote) != 0) {
-                                savePrimaryId(primaryDiffs, Contacts.KIND_ORGANIZATION, localId);
-                            }
-                            break;
-                    }
-                }
-            } finally {
-                if (cRemote != null) cRemote.deactivate();
-                if (cLocal != null) cLocal.deactivate();
-            }
-
-            // Groupmembership
-            cRemote = null;
-            cLocal = null;
-            try {
-                cLocal = doSubQuery(db,
-                        sGroupmembershipTable, null, localPersonID, sGroupmembershipKeyOrderBy);
-                cRemote = doSubQuery(diffsDb,
-                        sGroupmembershipTable, null, diffsPersonID, sGroupmembershipKeyOrderBy);
-
-                final int idColLocal = cLocal.getColumnIndexOrThrow(GroupMembership._ID);
-                CursorJoiner joiner = new CursorJoiner(
-                        cLocal, sGroupmembershipKeyColumns, cRemote, sGroupmembershipKeyColumns);
-                for (CursorJoiner.Result joinResult : joiner) {
-                    switch(joinResult) {
-                        case LEFT:
-                            if (!conflicts) {
-                                db.delete(sGroupmembershipTable,
-                                        Phones._ID + "=" + cLocal.getLong(idColLocal), null);
-                            }
-                            break;
-
-                        case RIGHT:
-                        case BOTH:
-                            mValues.clear();
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    GroupMembership.GROUP_SYNC_ACCOUNT, mValues);
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, mValues);
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    GroupMembership.GROUP_SYNC_ID, mValues);
-                            if (joinResult == CursorJoiner.Result.RIGHT) {
-                                mValues.put(GroupMembership.PERSON_ID, localPersonID);
-                                mGroupMembershipInserter.insert(mValues);
-                            } else {
-                                db.update(sGroupmembershipTable, mValues,
-                                        "_id =" + cLocal.getLong(idColLocal), null /* whereArgs */);
-                            }
-                            break;
-                    }
-                }
-            } finally {
-                if (cRemote != null) cRemote.deactivate();
-                if (cLocal != null) cLocal.deactivate();
-            }
-
-            // Extensions
-            cRemote = null;
-            cLocal = null;
-            try {
-                cLocal = doSubQuery(db,
-                        sExtensionsTable, null, localPersonID, Extensions.NAME);
-                cRemote = doSubQuery(diffsDb,
-                        sExtensionsTable, null, diffsPersonID, Extensions.NAME);
-
-                final int idColLocal = cLocal.getColumnIndexOrThrow(Extensions._ID);
-                CursorJoiner joiner = new CursorJoiner(
-                        cLocal, sExtensionsKeyColumns, cRemote, sExtensionsKeyColumns);
-                for (CursorJoiner.Result joinResult : joiner) {
-                    switch(joinResult) {
-                        case LEFT:
-                            if (!conflicts) {
-                                db.delete(sExtensionsTable,
-                                        Phones._ID + "=" + cLocal.getLong(idColLocal), null);
-                            }
-                            break;
-
-                        case RIGHT:
-                        case BOTH:
-                            mValues.clear();
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    Extensions.NAME, mValues);
-                            DatabaseUtils.cursorStringToContentValues(cRemote,
-                                    Extensions.VALUE, mValues);
-                            if (joinResult == CursorJoiner.Result.RIGHT) {
-                                mValues.put(Extensions.PERSON_ID, localPersonID);
-                                mExtensionsInserter.insert(mValues);
-                            } else {
-                                db.update(sExtensionsTable, mValues,
-                                        "_id =" + cLocal.getLong(idColLocal), null /* whereArgs */);
-                            }
-                            break;
-                    }
-                }
-            } finally {
-                if (cRemote != null) cRemote.deactivate();
-                if (cLocal != null) cLocal.deactivate();
-            }
-
-            // Copy the Photo's server id and account so that the merger will find it
-            cRemote = doSubQuery(diffsDb, sPhotosTable, null, diffsPersonID, null);
-            try {
-                if(cRemote.moveToNext()) {
-                    mValues.clear();
-                    DatabaseUtils.cursorStringToContentValues(cRemote, Photos._SYNC_ID, mValues);
-                    DatabaseUtils.cursorStringToContentValues(cRemote,
-                            Photos._SYNC_ACCOUNT, mValues);
-                    DatabaseUtils.cursorStringToContentValues(cRemote,
-                            Photos._SYNC_ACCOUNT_TYPE, mValues);
-                    db.update(sPhotosTable, mValues, Photos.PERSON_ID + '=' + localPersonID, null);
-                }
-            } finally {
-                cRemote.deactivate();
-            }
-
-            // make sure there is exactly one primary set for each of these types
-            Long primaryPhoneId = setSinglePrimary(
-                    primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_PHONE);
-
-            Long primaryEmailId = setSinglePrimary(
-                    primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_EMAIL);
-
-            Long primaryOrganizationId = setSinglePrimary(
-                    primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_ORGANIZATION);
-
-            setSinglePrimary(primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_IM);
-
-            setSinglePrimary(primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_POSTAL);
-
-            // Update the person
-            mValues.clear();
-            DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_ID, mValues);
-            DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_TIME, mValues);
-            DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_VERSION, mValues);
-            DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_ACCOUNT, mValues);
-            DatabaseUtils.cursorStringToContentValues(diffsCursor,
-                    People._SYNC_ACCOUNT_TYPE, mValues);
-            DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NAME, mValues);
-            DatabaseUtils.cursorStringToContentValues(diffsCursor, People.PHONETIC_NAME, mValues);
-            DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NOTES, mValues);
-            mValues.put(People.PRIMARY_PHONE_ID, primaryPhoneId);
-            mValues.put(People.PRIMARY_EMAIL_ID, primaryEmailId);
-            mValues.put(People.PRIMARY_ORGANIZATION_ID, primaryOrganizationId);
-            final boolean isStarred = queryGroupMembershipContainsStarred(localPersonID);
-            mValues.put(People.STARRED, isStarred ? 1 : 0);
-            mValues.put(People._SYNC_DIRTY, conflicts ? 1 : 0);
-            db.update(mTable, mValues, People._ID + '=' + localPersonID, null);
-        }
-
-        private void savePrimaryId(Map<Integer, Long> primaryDiffs, Integer kind, long localId) {
-            if (primaryDiffs.containsKey(kind)) {
-                throw new IllegalArgumentException("more than one of kind "
-                        + kind + " was marked as primary");
-            }
-            primaryDiffs.put(kind, localId);
-        }
-
-        private Long setSinglePrimary(
-                Map<Integer, Long> diffsMap,
-                Map<Integer, Long> localMap,
-                long localPersonID, int kind) {
-            Long primaryId = diffsMap.containsKey(kind) ? diffsMap.get(kind) : null;
-            if (primaryId == null) {
-                primaryId = localMap.containsKey(kind) ? localMap.get(kind) : null;
-            }
-            if (primaryId == null) {
-                primaryId = findNewPrimary(kind, localPersonID, null);
-            }
-            clearOtherIsPrimary(kind, localPersonID, primaryId);
-            return primaryId;
-        }
-
-        /**
-         * Returns a cursor on the specified table that selects rows where
-         * the "person" column is equal to the personId parameter. The cursor
-         * is also saved and may be returned in future calls where db and table
-         * parameter are the same. In that case the projection and orderBy parameters
-         * are ignored, so one must take care to not change those parameters across
-         * multiple calls to the same db/table.
-         * <p>
-         * Since the cursor may be saced by this call, the caller must be sure to not
-         * close the cursor, though they still must deactivate it when they are done
-         * with it.
-         */
-        private Cursor doSubQuery(SQLiteDatabase db, String table, String[] projection,
-                long personId, String orderBy) {
-            final String[] selectArgs = new String[]{Long.toString(personId)};
-            final String key = (db == getDatabase() ? "local_" : "remote_") + table;
-            SQLiteCursor cursor = mCursorMap.get(key);
-
-            // don't use the cached cursor if it is from a different DB
-            if (cursor != null && cursor.getDatabase() != db) {
-                cursor.close();
-                cursor = null;
-            }
-
-            // If we can't find a cached cursor then create a new one and add it to the cache.
-            // Otherwise just change the selection arguments and requery it.
-            if (cursor == null) {
-                cursor = (SQLiteCursor)db.query(table, projection, "person=?", selectArgs,
-                        null, null, orderBy);
-                mCursorMap.put(key, cursor);
-            } else {
-                cursor.setSelectionArguments(selectArgs);
-                cursor.requery();
-            }
-            return cursor;
-        }
-    }
-
-    protected class GroupMerger extends AbstractTableMerger {
-        private ContentValues mValues = new ContentValues();
-
-        private static final String UNSYNCED_GROUP_BY_NAME_WHERE_CLAUSE =
-                Groups._SYNC_ID + " is null AND "
-                        + Groups._SYNC_ACCOUNT + " is null AND "
-                        + Groups.NAME + "=?";
-
-        private static final String UNSYNCED_GROUP_BY_SYSTEM_ID_WHERE_CLAUSE =
-                Groups._SYNC_ID + " is null AND "
-                        + Groups._SYNC_ACCOUNT + " is null AND "
-                        + Groups.SYSTEM_ID + "=?";
-
-        public GroupMerger()
-        {
-            super(getDatabase(), sGroupsTable, sGroupsURL, sDeletedGroupsTable, sDeletedGroupsURL);
-        }
-
-        @Override
-        protected void notifyChanges() {
-            // notify that a change has occurred.
-            getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI,
-                    null /* observer */, false /* do not sync to network */);
-        }
-
-        @Override
-        public void insertRow(ContentProvider diffs, Cursor cursor) {
-            // if an unsynced group with this name already exists then update it, otherwise
-            // insert a new group
-            mValues.clear();
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ID, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT_TYPE, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues);
-            mValues.put(Groups._SYNC_DIRTY, 0);
-
-            final String systemId = mValues.getAsString(Groups.SYSTEM_ID);
-            boolean rowUpdated = false;
-            if (TextUtils.isEmpty(systemId)) {
-                rowUpdated = getDatabase().update(mTable, mValues,
-                    UNSYNCED_GROUP_BY_NAME_WHERE_CLAUSE,
-                    new String[]{mValues.getAsString(Groups.NAME)}) > 0;
-            } else {
-                rowUpdated = getDatabase().update(mTable, mValues,
-                    UNSYNCED_GROUP_BY_SYSTEM_ID_WHERE_CLAUSE,
-                    new String[]{systemId}) > 0;
-            }
-            if (!rowUpdated) {
-                mGroupsInserter.insert(mValues);
-            } else {
-                // We may have just synced the metadata for a groups we previously marked for
-                // syncing.
-                final ContentResolver cr = getContext().getContentResolver();
-                final String accountName = mValues.getAsString(Groups._SYNC_ACCOUNT);
-                final String accountType = mValues.getAsString(Groups._SYNC_ACCOUNT_TYPE);
-                onLocalChangesForAccount(cr, new Account(accountName, accountType), false);
-            }
-
-            String oldName = null;
-            String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME));
-            String accountName = cursor.getString(
-                    cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
-            String accountType = cursor.getString(
-                    cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE));
-            String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
-            // this must come after the insert, otherwise the join won't work
-            fixupPeopleStarredOnGroupRename(oldName, newName, new Account(accountName, accountType),
-                    syncId);
-        }
-
-        @Override
-        public void updateRow(long localId, ContentProvider diffs, Cursor diffsCursor) {
-            updateOrResolveRow(localId, null, diffs, diffsCursor, false);
-        }
-
-        @Override
-        public void resolveRow(long localId, String syncID,
-                ContentProvider diffs, Cursor diffsCursor) {
-            updateOrResolveRow(localId, syncID, diffs, diffsCursor, true);
-        }
-
-        protected void updateOrResolveRow(long localRowId, String syncID,
-                ContentProvider diffs, Cursor cursor, boolean conflicts) {
-            final SQLiteDatabase db = getDatabase();
-
-            String oldName = DatabaseUtils.stringForQuery(db,
-                    "select name from groups where _id=" + localRowId, null);
-            String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME));
-            String accountName = cursor.getString(
-                    cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
-            String accountType = cursor.getString(
-                    cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE));
-            String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
-            // this can come before or after the delete
-            fixupPeopleStarredOnGroupRename(oldName, newName,
-                    new Account(accountName, accountType), syncId);
-
-            mValues.clear();
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ID, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT_TYPE, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues);
-            mValues.put(Groups._SYNC_DIRTY, 0);
-            db.update(mTable, mValues, Groups._ID + '=' + localRowId, null);
-        }
-
-        @Override
-        public void deleteRow(Cursor cursor) {
-            // we have to read this row from the DB since the projection that is used
-            // by cursor doesn't necessarily contain the columns we need
-            Cursor c = getDatabase().query(sGroupsTable, null,
-                    "_id=" + cursor.getLong(cursor.getColumnIndexOrThrow(Groups._ID)),
-                    null, null, null, null);
-            try {
-                c.moveToNext();
-                String oldName = c.getString(c.getColumnIndexOrThrow(Groups.NAME));
-                String newName = null;
-                String accountName = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
-                String accountType = c.getString(
-                        c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE));
-                String syncId = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ID));
-                String systemId = c.getString(c.getColumnIndexOrThrow(Groups.SYSTEM_ID));
-                if (!TextUtils.isEmpty(systemId)) {
-                    // We don't support deleting of system groups, but due to a server bug they
-                    // occasionally get sent. Ignore the delete.
-                    Log.w(TAG, "ignoring a delete for a system group: " +
-                            DatabaseUtils.dumpCurrentRowToString(c));
-                    cursor.moveToNext();
-                    return;
-                }
-
-                // this must come before the delete, since the join won't work once this row is gone
-                fixupPeopleStarredOnGroupRename(oldName, newName,
-                        new Account(accountName, accountType), syncId);
-            } finally {
-                c.close();
-            }
-
-            cursor.deleteRow();
-        }
-    }
-
-    protected class PhotoMerger extends AbstractTableMerger {
-        private ContentValues mValues = new ContentValues();
-
-        public PhotoMerger() {
-            super(getDatabase(), sPhotosTable, sPhotosURL, null, null);
-        }
-
-        @Override
-        protected void notifyChanges() {
-            // notify that a change has occurred.
-            getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI,
-                    null /* observer */, false /* do not sync to network */);
-        }
-
-        @Override
-        public void insertRow(ContentProvider diffs, Cursor cursor) {
-            // This photo may correspond to a contact that is in the delete table. If so then
-            // ignore this insert.
-            String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Photos._SYNC_ID));
-            boolean contactIsDeleted = DatabaseUtils.longForQuery(getDatabase(),
-                    "select count(*) from _deleted_people where _sync_id=?",
-                    new String[]{syncId}) > 0;
-            if (contactIsDeleted) {
-                return;
-            }
-
-            throw new UnsupportedOperationException(
-                    "the photo row is inserted by PersonMerger.insertRow");
-        }
-
-        @Override
-        public void updateRow(long localId, ContentProvider diffs, Cursor diffsCursor) {
-            updateOrResolveRow(localId, null, diffs, diffsCursor, false);
-        }
-
-        @Override
-        public void resolveRow(long localId, String syncID,
-                ContentProvider diffs, Cursor diffsCursor) {
-            updateOrResolveRow(localId, syncID, diffs, diffsCursor, true);
-        }
-
-        protected void updateOrResolveRow(long localRowId, String syncID,
-                ContentProvider diffs, Cursor cursor, boolean conflicts) {
-            if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                Log.v(TAG, "PhotoMerger.updateOrResolveRow: localRowId " + localRowId
-                        + ", syncId " + syncID + ", conflicts " + conflicts
-                        + ", server row " + DatabaseUtils.dumpCurrentRowToString(cursor));
-            }
-            mValues.clear();
-            DatabaseUtils.cursorStringToContentValues(cursor, Photos._SYNC_TIME, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Photos._SYNC_VERSION, mValues);
-            DatabaseUtils.cursorStringToContentValues(cursor, Photos.EXISTS_ON_SERVER, mValues);
-            // reset the error field to allow the phone to attempt to redownload the photo.
-            mValues.put(Photos.SYNC_ERROR, (String)null);
-
-            // If the photo didn't change locally and the server doesn't have a photo for this
-            // contact then delete the local photo.
-            long syncDirty = DatabaseUtils.longForQuery(getDatabase(),
-                    "SELECT _sync_dirty FROM photos WHERE _id=" + localRowId
-                            + " UNION SELECT 0 AS _sync_dirty ORDER BY _sync_dirty DESC LIMIT 1",
-                    null);
-            if (syncDirty == 0) {
-                if (mValues.getAsInteger(Photos.EXISTS_ON_SERVER) == 0) {
-                    mValues.put(Photos.DATA, (String)null);
-                    mValues.put(Photos.LOCAL_VERSION, mValues.getAsString(Photos.LOCAL_VERSION));
-                }
-                // if it does exist on the server then we will attempt to download it later
-            }
-            // if it does conflict then we will send the client version of the photo to
-            // the server later. That will trigger a new sync of the photo data which will
-            // cause this method to be called again, at which time the row will no longer
-            // conflict. We will then download the photo we just sent to the server and
-            // set the LOCAL_VERSION to match the data we just downloaded.
-
-            getDatabase().update(mTable, mValues, Photos._ID + '=' + localRowId, null);
-        }
-
-        @Override
-        public void deleteRow(Cursor cursor) {
-            // this row is never deleted explicitly, instead it is deleted by a trigger on
-            // the people table
-            cursor.moveToNext();
-        }
-    }
-
-    private static final String TAG = "ContactsProvider";
-
-    /* package private */ static final String DATABASE_NAME = "contacts.db";
-    /* package private */ static final int DATABASE_VERSION = 83;
-
-    protected static final String CONTACTS_AUTHORITY = "contacts";
-    protected static final String CALL_LOG_AUTHORITY = "call_log";
-
-    private static final int PEOPLE_BASE = 0;
-    private static final int PEOPLE = PEOPLE_BASE;
-    private static final int PEOPLE_FILTER = PEOPLE_BASE + 1;
-    private static final int PEOPLE_ID = PEOPLE_BASE + 2;
-    private static final int PEOPLE_PHONES = PEOPLE_BASE + 3;
-    private static final int PEOPLE_PHONES_ID = PEOPLE_BASE + 4;
-    private static final int PEOPLE_CONTACTMETHODS = PEOPLE_BASE + 5;
-    private static final int PEOPLE_CONTACTMETHODS_ID = PEOPLE_BASE + 6;
-    private static final int PEOPLE_RAW = PEOPLE_BASE + 7;
-    private static final int PEOPLE_WITH_PHONES_FILTER = PEOPLE_BASE + 8;
-    private static final int PEOPLE_STREQUENT = PEOPLE_BASE + 9;
-    private static final int PEOPLE_STREQUENT_FILTER = PEOPLE_BASE + 10;
-    private static final int PEOPLE_ORGANIZATIONS = PEOPLE_BASE + 11;
-    private static final int PEOPLE_ORGANIZATIONS_ID = PEOPLE_BASE + 12;
-    private static final int PEOPLE_GROUPMEMBERSHIP = PEOPLE_BASE + 13;
-    private static final int PEOPLE_GROUPMEMBERSHIP_ID = PEOPLE_BASE + 14;
-    private static final int PEOPLE_PHOTO = PEOPLE_BASE + 15;
-    private static final int PEOPLE_EXTENSIONS = PEOPLE_BASE + 16;
-    private static final int PEOPLE_EXTENSIONS_ID = PEOPLE_BASE + 17;
-    private static final int PEOPLE_CONTACTMETHODS_WITH_PRESENCE = PEOPLE_BASE + 18;
-    private static final int PEOPLE_OWNER = PEOPLE_BASE + 19;
-    private static final int PEOPLE_UPDATE_CONTACT_TIME = PEOPLE_BASE + 20;
-    private static final int PEOPLE_PHONES_WITH_PRESENCE = PEOPLE_BASE + 21;
-    private static final int PEOPLE_WITH_EMAIL_OR_IM_FILTER = PEOPLE_BASE + 22;
-    private static final int PEOPLE_PHOTO_DATA = PEOPLE_BASE + 23;
-
-    private static final int DELETED_BASE = 1000;
-    private static final int DELETED_PEOPLE = DELETED_BASE;
-    private static final int DELETED_GROUPS = DELETED_BASE + 1;
-
-    private static final int PHONES_BASE = 2000;
-    private static final int PHONES = PHONES_BASE;
-    private static final int PHONES_ID = PHONES_BASE + 1;
-    private static final int PHONES_FILTER = PHONES_BASE + 2;
-    private static final int PHONES_FILTER_NAME = PHONES_BASE + 3;
-    private static final int PHONES_MOBILE_FILTER_NAME = PHONES_BASE + 4;
-    private static final int PHONES_WITH_PRESENCE = PHONES_BASE + 5;
-
-    private static final int CONTACTMETHODS_BASE = 3000;
-    private static final int CONTACTMETHODS = CONTACTMETHODS_BASE;
-    private static final int CONTACTMETHODS_ID = CONTACTMETHODS_BASE + 1;
-    private static final int CONTACTMETHODS_EMAIL = CONTACTMETHODS_BASE + 2;
-    private static final int CONTACTMETHODS_EMAIL_FILTER = CONTACTMETHODS_BASE + 3;
-    private static final int CONTACTMETHODS_WITH_PRESENCE = CONTACTMETHODS_BASE + 4;
-
-    private static final int CALLS_BASE = 4000;
-    private static final int CALLS = CALLS_BASE;
-    private static final int CALLS_ID = CALLS_BASE + 1;
-    private static final int CALLS_FILTER = CALLS_BASE + 2;
-
-    private static final int PRESENCE_BASE = 5000;
-    private static final int PRESENCE = PRESENCE_BASE;
-    private static final int PRESENCE_ID = PRESENCE_BASE + 1;
-
-    private static final int ORGANIZATIONS_BASE = 6000;
-    private static final int ORGANIZATIONS = ORGANIZATIONS_BASE;
-    private static final int ORGANIZATIONS_ID = ORGANIZATIONS_BASE + 1;
-
-    private static final int VOICE_DIALER_TIMESTAMP = 7000;
-    private static final int SEARCH_SUGGESTIONS = 7001;
-    private static final int SEARCH_SHORTCUT = 7002;
-
-    private static final int GROUPS_BASE = 8000;
-    private static final int GROUPS = GROUPS_BASE;
-    private static final int GROUPS_ID = GROUPS_BASE + 2;
-    private static final int GROUP_NAME_MEMBERS = GROUPS_BASE + 3;
-    private static final int GROUP_NAME_MEMBERS_FILTER = GROUPS_BASE + 4;
-    private static final int GROUP_SYSTEM_ID_MEMBERS = GROUPS_BASE + 5;
-    private static final int GROUP_SYSTEM_ID_MEMBERS_FILTER = GROUPS_BASE + 6;
-
-    private static final int GROUPMEMBERSHIP_BASE = 9000;
-    private static final int GROUPMEMBERSHIP = GROUPMEMBERSHIP_BASE;
-    private static final int GROUPMEMBERSHIP_ID = GROUPMEMBERSHIP_BASE + 2;
-    private static final int GROUPMEMBERSHIP_RAW = GROUPMEMBERSHIP_BASE + 3;
-
-    private static final int PHOTOS_BASE = 10000;
-    private static final int PHOTOS = PHOTOS_BASE;
-    private static final int PHOTOS_ID = PHOTOS_BASE + 1;
-
-    private static final int EXTENSIONS_BASE = 11000;
-    private static final int EXTENSIONS = EXTENSIONS_BASE;
-    private static final int EXTENSIONS_ID = EXTENSIONS_BASE + 2;
-
-    private static final int SETTINGS = 12000;
-
-    private static final int LIVE_FOLDERS_BASE = 13000;
-    private static final int LIVE_FOLDERS_PEOPLE = LIVE_FOLDERS_BASE + 1;
-    private static final int LIVE_FOLDERS_PEOPLE_GROUP_NAME = LIVE_FOLDERS_BASE + 2;
-    private static final int LIVE_FOLDERS_PEOPLE_WITH_PHONES = LIVE_FOLDERS_BASE + 3;
-    private static final int LIVE_FOLDERS_PEOPLE_FAVORITES = LIVE_FOLDERS_BASE + 4;
-
-    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-
-    private static final HashMap<String, String> sGroupsProjectionMap;
-    private static final HashMap<String, String> sPeopleProjectionMap;
-    private static final HashMap<String, String> sPeopleWithPhotoProjectionMap;
-    private static final HashMap<String, String> sPeopleWithEmailOrImProjectionMap;
-    /** Used to force items to the top of a times_contacted list */
-    private static final HashMap<String, String> sStrequentStarredProjectionMap;
-    private static final HashMap<String, String> sCallsProjectionMap;
-    private static final HashMap<String, String> sPhonesProjectionMap;
-    private static final HashMap<String, String> sPhonesWithPresenceProjectionMap;
-    private static final HashMap<String, String> sContactMethodsProjectionMap;
-    private static final HashMap<String, String> sContactMethodsWithPresenceProjectionMap;
-    private static final HashMap<String, String> sPresenceProjectionMap;
-    private static final HashMap<String, String> sEmailSearchProjectionMap;
-    private static final HashMap<String, String> sOrganizationsProjectionMap;
-    private static final HashMap<String, String> sSearchSuggestionsProjectionMap;
-    private static final HashMap<String, String> sGroupMembershipProjectionMap;
-    private static final HashMap<String, String> sPhotosProjectionMap;
-    private static final HashMap<String, String> sExtensionsProjectionMap;
-    private static final HashMap<String, String> sLiveFoldersProjectionMap;
-
-    private static final String sPhonesKeyOrderBy;
-    private static final String sContactMethodsKeyOrderBy;
-    private static final String sOrganizationsKeyOrderBy;
-    private static final String sGroupmembershipKeyOrderBy;
-
-    private static final String DISPLAY_NAME_SQL
-            = "(CASE WHEN (name IS NOT NULL AND name != '') "
-                + "THEN name "
-            + "ELSE "
-                + "(CASE WHEN primary_organization is NOT NULL THEN "
-                    + "(SELECT company FROM organizations WHERE "
-                        + "organizations._id = primary_organization) "
-                + "ELSE "
-                    + "(CASE WHEN primary_phone IS NOT NULL THEN "
-                        +"(SELECT number FROM phones WHERE phones._id = primary_phone) "
-                    + "ELSE "
-                        + "(CASE WHEN primary_email IS NOT NULL THEN "
-                            + "(SELECT data FROM contact_methods WHERE "
-                                + "contact_methods._id = primary_email) "
-                        + "ELSE "
-                            + "null "
-                        + "END) "
-                    + "END) "
-                + "END) "
-            + "END) ";
-
-    private static final String PHONETICALLY_SORTABLE_STRING_SQL =
-        "GET_PHONETICALLY_SORTABLE_STRING("
-            + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') "
-                + "THEN phonetic_name "
-            + "ELSE "
-                + "(CASE WHEN (name is NOT NULL AND name != '')"
-                    + "THEN name "
-                + "ELSE "
-                    + "(CASE WHEN primary_email IS NOT NULL THEN "
-                        + "(SELECT data FROM contact_methods WHERE "
-                            + "contact_methods._id = primary_email) "
-                    + "ELSE "
-                        + "(CASE WHEN primary_phone IS NOT NULL THEN "
-                            + "(SELECT number FROM phones WHERE phones._id = primary_phone) "
-                        + "ELSE "
-                            + "null "
-                        + "END) "
-                    + "END) "
-                + "END) "
-            + "END"
-        + ")";
-
-    private static final String PRIMARY_ORGANIZATION_WHEN_SQL
-            = " WHEN primary_organization is NOT NULL THEN "
-            + "(SELECT company FROM organizations WHERE organizations._id = primary_organization)";
-
-    private static final String PRIMARY_PHONE_WHEN_SQL
-            = " WHEN primary_phone IS NOT NULL THEN "
-            + "(SELECT number FROM phones WHERE phones._id = primary_phone)";
-
-    private static final String PRIMARY_EMAIL_WHEN_SQL
-            = " WHEN primary_email IS NOT NULL THEN "
-            + "(SELECT data FROM contact_methods WHERE contact_methods._id = primary_email)";
-
-    // The outer CASE is for figuring out what info DISPLAY_NAME_SQL returned.
-    // We then pick the next piece of info, to avoid the two lines in the search
-    // suggestion being identical.
-    private static final String SUGGEST_DESCRIPTION_SQL
-            = "(CASE"
-                // DISPLAY_NAME_SQL returns name, try org, phone, email
-                + " WHEN (name IS NOT NULL AND name != '') THEN "
-                    + "(CASE"
-                        + PRIMARY_ORGANIZATION_WHEN_SQL
-                        + PRIMARY_PHONE_WHEN_SQL
-                        + PRIMARY_EMAIL_WHEN_SQL
-                        + " ELSE null END)"
-                // DISPLAY_NAME_SQL returns org, try phone, email
-                + " WHEN primary_organization is NOT NULL THEN "
-                    + "(CASE"
-                        + PRIMARY_PHONE_WHEN_SQL
-                        + PRIMARY_EMAIL_WHEN_SQL
-                        + " ELSE null END)"
-                // DISPLAY_NAME_SQL returns phone, try email
-                + " WHEN primary_phone IS NOT NULL THEN "
-                    + "(CASE"
-                        + PRIMARY_EMAIL_WHEN_SQL
-                        + " ELSE null END)"
-                // DISPLAY_NAME_SQL returns email or NULL, return NULL
-                + " ELSE null END)";
-
-    private static final String PRESENCE_ICON_SQL
-            = "(CASE"
-                + buildPresenceStatusWhen(People.OFFLINE)
-                + buildPresenceStatusWhen(People.INVISIBLE)
-                + buildPresenceStatusWhen(People.AWAY)
-                + buildPresenceStatusWhen(People.IDLE)
-                + buildPresenceStatusWhen(People.DO_NOT_DISTURB)
-                + buildPresenceStatusWhen(People.AVAILABLE)
-                + " ELSE null END)";
-
-    private static String buildPresenceStatusWhen(int status) {
-        return " WHEN " + Presence.PRESENCE_STATUS + " = " + status
-            + " THEN " + Presence.getPresenceIconResourceId(status);
-    }
-
-    private static final String[] sPhonesKeyColumns;
-    private static final String[] sContactMethodsKeyColumns;
-    private static final String[] sOrganizationsKeyColumns;
-    private static final String[] sGroupmembershipKeyColumns;
-    private static final String[] sExtensionsKeyColumns;
-
-    static private String buildOrderBy(String table, String... columns) {
-        StringBuilder sb = null;
-        for (String column : columns) {
-            if (sb == null) {
-                sb = new StringBuilder();
-            } else {
-                sb.append(", ");
-            }
-            sb.append(table);
-            sb.append('.');
-            sb.append(column);
-        }
-        return (sb == null) ? "" : sb.toString();
-    }
-
-    static {
-        // Contacts URI matching table
-        UriMatcher matcher = sURIMatcher;
-        matcher.addURI(CONTACTS_AUTHORITY, "extensions", EXTENSIONS);
-        matcher.addURI(CONTACTS_AUTHORITY, "extensions/#", EXTENSIONS_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "groups", GROUPS);
-        matcher.addURI(CONTACTS_AUTHORITY, "groups/#", GROUPS_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "groups/name/*/members", GROUP_NAME_MEMBERS);
-        matcher.addURI(CONTACTS_AUTHORITY, "groups/name/*/members/filter/*",
-                GROUP_NAME_MEMBERS_FILTER);
-        matcher.addURI(CONTACTS_AUTHORITY, "groups/system_id/*/members", GROUP_SYSTEM_ID_MEMBERS);
-        matcher.addURI(CONTACTS_AUTHORITY, "groups/system_id/*/members/filter/*",
-                GROUP_SYSTEM_ID_MEMBERS_FILTER);
-        matcher.addURI(CONTACTS_AUTHORITY, "groupmembership", GROUPMEMBERSHIP);
-        matcher.addURI(CONTACTS_AUTHORITY, "groupmembership/#", GROUPMEMBERSHIP_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "groupmembershipraw", GROUPMEMBERSHIP_RAW);
-        matcher.addURI(CONTACTS_AUTHORITY, "people", PEOPLE);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/strequent", PEOPLE_STREQUENT);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/strequent/filter/*", PEOPLE_STREQUENT_FILTER);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/filter/*", PEOPLE_FILTER);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/with_phones_filter/*",
-                PEOPLE_WITH_PHONES_FILTER);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/with_email_or_im_filter/*",
-                PEOPLE_WITH_EMAIL_OR_IM_FILTER);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#", PEOPLE_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/extensions", PEOPLE_EXTENSIONS);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/extensions/#", PEOPLE_EXTENSIONS_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones", PEOPLE_PHONES);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones_with_presence",
-                PEOPLE_PHONES_WITH_PRESENCE);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/photo", PEOPLE_PHOTO);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/photo/data", PEOPLE_PHOTO_DATA);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones/#", PEOPLE_PHONES_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods", PEOPLE_CONTACTMETHODS);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods_with_presence",
-                PEOPLE_CONTACTMETHODS_WITH_PRESENCE);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods/#", PEOPLE_CONTACTMETHODS_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/organizations", PEOPLE_ORGANIZATIONS);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/organizations/#", PEOPLE_ORGANIZATIONS_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/groupmembership", PEOPLE_GROUPMEMBERSHIP);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/groupmembership/#", PEOPLE_GROUPMEMBERSHIP_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/raw", PEOPLE_RAW);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/owner", PEOPLE_OWNER);
-        matcher.addURI(CONTACTS_AUTHORITY, "people/#/update_contact_time",
-                PEOPLE_UPDATE_CONTACT_TIME);
-        matcher.addURI(CONTACTS_AUTHORITY, "deleted_people", DELETED_PEOPLE);
-        matcher.addURI(CONTACTS_AUTHORITY, "deleted_groups", DELETED_GROUPS);
-        matcher.addURI(CONTACTS_AUTHORITY, "phones", PHONES);
-        matcher.addURI(CONTACTS_AUTHORITY, "phones_with_presence", PHONES_WITH_PRESENCE);
-        matcher.addURI(CONTACTS_AUTHORITY, "phones/filter/*", PHONES_FILTER);
-        matcher.addURI(CONTACTS_AUTHORITY, "phones/filter_name/*", PHONES_FILTER_NAME);
-        matcher.addURI(CONTACTS_AUTHORITY, "phones/mobile_filter_name/*",
-                PHONES_MOBILE_FILTER_NAME);
-        matcher.addURI(CONTACTS_AUTHORITY, "phones/#", PHONES_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "photos", PHOTOS);
-        matcher.addURI(CONTACTS_AUTHORITY, "photos/#", PHOTOS_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods", CONTACTMETHODS);
-        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/email", CONTACTMETHODS_EMAIL);
-        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/email/*", CONTACTMETHODS_EMAIL_FILTER);
-        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/#", CONTACTMETHODS_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/with_presence",
-                CONTACTMETHODS_WITH_PRESENCE);
-        matcher.addURI(CONTACTS_AUTHORITY, "presence", PRESENCE);
-        matcher.addURI(CONTACTS_AUTHORITY, "presence/#", PRESENCE_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "organizations", ORGANIZATIONS);
-        matcher.addURI(CONTACTS_AUTHORITY, "organizations/#", ORGANIZATIONS_ID);
-        matcher.addURI(CONTACTS_AUTHORITY, "voice_dialer_timestamp", VOICE_DIALER_TIMESTAMP);
-        matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
-                SEARCH_SUGGESTIONS);
-        matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
-                SEARCH_SUGGESTIONS);
-        matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
-                SEARCH_SHORTCUT);
-        matcher.addURI(CONTACTS_AUTHORITY, "settings", SETTINGS);
-
-        matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people", LIVE_FOLDERS_PEOPLE);
-        matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people/*",
-                LIVE_FOLDERS_PEOPLE_GROUP_NAME);
-        matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people_with_phones",
-                LIVE_FOLDERS_PEOPLE_WITH_PHONES);
-        matcher.addURI(CONTACTS_AUTHORITY, "live_folders/favorites",
-                LIVE_FOLDERS_PEOPLE_FAVORITES);
-
-        // Call log URI matching table
-        matcher.addURI(CALL_LOG_AUTHORITY, "calls", CALLS);
-        matcher.addURI(CALL_LOG_AUTHORITY, "calls/filter/*", CALLS_FILTER);
-        matcher.addURI(CALL_LOG_AUTHORITY, "calls/#", CALLS_ID);
-
-        HashMap<String, String> map;
-
-        // Create the common people columns
-        HashMap<String, String> peopleColumns = new HashMap<String, String>();
-        peopleColumns.put(PeopleColumns.NAME, People.NAME);
-        peopleColumns.put(PeopleColumns.NOTES, People.NOTES);
-        peopleColumns.put(PeopleColumns.TIMES_CONTACTED, People.TIMES_CONTACTED);
-        peopleColumns.put(PeopleColumns.LAST_TIME_CONTACTED, People.LAST_TIME_CONTACTED);
-        peopleColumns.put(PeopleColumns.STARRED, People.STARRED);
-        peopleColumns.put(PeopleColumns.CUSTOM_RINGTONE, People.CUSTOM_RINGTONE);
-        peopleColumns.put(PeopleColumns.SEND_TO_VOICEMAIL, People.SEND_TO_VOICEMAIL);
-        peopleColumns.put(PeopleColumns.PHONETIC_NAME, People.PHONETIC_NAME);
-        peopleColumns.put(PeopleColumns.DISPLAY_NAME,
-                DISPLAY_NAME_SQL + " AS " + People.DISPLAY_NAME);
-        peopleColumns.put(PeopleColumns.SORT_STRING,
-                PHONETICALLY_SORTABLE_STRING_SQL + " AS " + People.SORT_STRING);
-
-        // Create the common groups columns
-        HashMap<String, String> groupsColumns = new HashMap<String, String>();
-        groupsColumns.put(GroupsColumns.NAME, Groups.NAME);
-        groupsColumns.put(GroupsColumns.NOTES, Groups.NOTES);
-        groupsColumns.put(GroupsColumns.SYSTEM_ID, Groups.SYSTEM_ID);
-        groupsColumns.put(GroupsColumns.SHOULD_SYNC, Groups.SHOULD_SYNC);
-
-        // Create the common presence columns
-        HashMap<String, String> presenceColumns = new HashMap<String, String>();
-        presenceColumns.put(PresenceColumns.IM_PROTOCOL, PresenceColumns.IM_PROTOCOL);
-        presenceColumns.put(PresenceColumns.IM_HANDLE, PresenceColumns.IM_HANDLE);
-        presenceColumns.put(PresenceColumns.IM_ACCOUNT, PresenceColumns.IM_ACCOUNT);
-        presenceColumns.put(PresenceColumns.PRESENCE_STATUS, PresenceColumns.PRESENCE_STATUS);
-        presenceColumns.put(PresenceColumns.PRESENCE_CUSTOM_STATUS,
-                PresenceColumns.PRESENCE_CUSTOM_STATUS);
-
-        // Create the common sync columns
-        HashMap<String, String> syncColumns = new HashMap<String, String>();
-        syncColumns.put(SyncConstValue._SYNC_ID, SyncConstValue._SYNC_ID);
-        syncColumns.put(SyncConstValue._SYNC_TIME, SyncConstValue._SYNC_TIME);
-        syncColumns.put(SyncConstValue._SYNC_VERSION, SyncConstValue._SYNC_VERSION);
-        syncColumns.put(SyncConstValue._SYNC_LOCAL_ID, SyncConstValue._SYNC_LOCAL_ID);
-        syncColumns.put(SyncConstValue._SYNC_DIRTY, SyncConstValue._SYNC_DIRTY);
-        syncColumns.put(SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT);
-        syncColumns.put(SyncConstValue._SYNC_ACCOUNT_TYPE, SyncConstValue._SYNC_ACCOUNT_TYPE);
-
-        // Phones columns
-        HashMap<String, String> phonesColumns = new HashMap<String, String>();
-        phonesColumns.put(Phones.NUMBER, Phones.NUMBER);
-        phonesColumns.put(Phones.NUMBER_KEY, Phones.NUMBER_KEY);
-        phonesColumns.put(Phones.TYPE, Phones.TYPE);
-        phonesColumns.put(Phones.LABEL, Phones.LABEL);
-
-        // People projection map
-        map = new HashMap<String, String>();
-        map.put(People._ID, "people._id AS " + People._ID);
-        peopleColumns.put(People.PRIMARY_PHONE_ID, People.PRIMARY_PHONE_ID);
-        peopleColumns.put(People.PRIMARY_EMAIL_ID, People.PRIMARY_EMAIL_ID);
-        peopleColumns.put(People.PRIMARY_ORGANIZATION_ID, People.PRIMARY_ORGANIZATION_ID);
-        map.putAll(peopleColumns);
-        map.putAll(phonesColumns);
-        map.putAll(syncColumns);
-        map.putAll(presenceColumns);
-        sPeopleProjectionMap = map;
-
-        // People with photo projection map
-        map = new HashMap<String, String>(sPeopleProjectionMap);
-        map.put("photo_data", "photos.data AS photo_data");
-        sPeopleWithPhotoProjectionMap = map;
-
-        // People with E-mail or IM projection map
-        map = new HashMap<String, String>();
-        map.put(People._ID, "people._id AS " + People._ID);
-        map.put(ContactMethods.DATA,
-                "contact_methods." + ContactMethods.DATA + " AS " + ContactMethods.DATA);
-        map.put(ContactMethods.KIND,
-                "contact_methods." + ContactMethods.KIND + " AS " + ContactMethods.KIND);
-        map.putAll(peopleColumns);
-        sPeopleWithEmailOrImProjectionMap = map;
-
-        // Groups projection map
-        map = new HashMap<String, String>();
-        map.put(Groups._ID, Groups._ID);
-        map.putAll(groupsColumns);
-        map.putAll(syncColumns);
-        sGroupsProjectionMap = map;
-
-        // Group Membership projection map
-        map = new HashMap<String, String>();
-        map.put(GroupMembership._ID, "groupmembership._id AS " + GroupMembership._ID);
-        map.put(GroupMembership.PERSON_ID, GroupMembership.PERSON_ID);
-        map.put(GroupMembership.GROUP_ID, "groups._id AS " + GroupMembership.GROUP_ID);
-        map.put(GroupMembership.GROUP_SYNC_ACCOUNT, GroupMembership.GROUP_SYNC_ACCOUNT);
-        map.put(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
-        map.put(GroupMembership.GROUP_SYNC_ID, GroupMembership.GROUP_SYNC_ID);
-        map.putAll(groupsColumns);
-        sGroupMembershipProjectionMap = map;
-
-        // Use this when you need to force items to the top of a times_contacted list
-        map = new HashMap<String, String>(sPeopleProjectionMap);
-        map.put(People.TIMES_CONTACTED, Long.MAX_VALUE + " AS " + People.TIMES_CONTACTED);
-        map.put("photo_data", "photos.data AS photo_data");
-        sStrequentStarredProjectionMap = map;
-
-        // Calls projection map
-        map = new HashMap<String, String>();
-        map.put(Calls._ID, Calls._ID);
-        map.put(Calls.NUMBER, Calls.NUMBER);
-        map.put(Calls.DATE, Calls.DATE);
-        map.put(Calls.DURATION, Calls.DURATION);
-        map.put(Calls.TYPE, Calls.TYPE);
-        map.put(Calls.NEW, Calls.NEW);
-        map.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
-        map.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
-        map.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
-        sCallsProjectionMap = map;
-
-        // Phones projection map
-        map = new HashMap<String, String>();
-        map.put(Phones._ID, "phones._id AS " + Phones._ID);
-        map.putAll(phonesColumns);
-        map.put(Phones.PERSON_ID, "phones.person AS " + Phones.PERSON_ID);
-        map.put(Phones.ISPRIMARY, Phones.ISPRIMARY);
-        map.putAll(peopleColumns);
-        sPhonesProjectionMap = map;
-
-        // Phones with presence projection map
-        map = new HashMap<String, String>(sPhonesProjectionMap);
-        map.putAll(presenceColumns);
-        sPhonesWithPresenceProjectionMap = map;
-
-        // Organizations projection map
-        map = new HashMap<String, String>();
-        map.put(Organizations._ID, "organizations._id AS " + Organizations._ID);
-        map.put(Organizations.LABEL, Organizations.LABEL);
-        map.put(Organizations.TYPE, Organizations.TYPE);
-        map.put(Organizations.PERSON_ID, Organizations.PERSON_ID);
-        map.put(Organizations.COMPANY, Organizations.COMPANY);
-        map.put(Organizations.TITLE, Organizations.TITLE);
-        map.put(Organizations.ISPRIMARY, Organizations.ISPRIMARY);
-        sOrganizationsProjectionMap = map;
-
-        // Extensions projection map
-        map = new HashMap<String, String>();
-        map.put(Extensions._ID, Extensions._ID);
-        map.put(Extensions.NAME, Extensions.NAME);
-        map.put(Extensions.VALUE, Extensions.VALUE);
-        map.put(Extensions.PERSON_ID, Extensions.PERSON_ID);
-        sExtensionsProjectionMap = map;
-
-        // Contact methods projection map
-        map = new HashMap<String, String>();
-        map.put(ContactMethods._ID, "contact_methods._id AS " + ContactMethods._ID);
-        map.put(ContactMethods.KIND, ContactMethods.KIND);
-        map.put(ContactMethods.TYPE, ContactMethods.TYPE);
-        map.put(ContactMethods.LABEL, ContactMethods.LABEL);
-        map.put(ContactMethods.DATA, ContactMethods.DATA);
-        map.put(ContactMethods.AUX_DATA, ContactMethods.AUX_DATA);
-        map.put(ContactMethods.PERSON_ID, "contact_methods.person AS " + ContactMethods.PERSON_ID);
-        map.put(ContactMethods.ISPRIMARY, ContactMethods.ISPRIMARY);
-        map.putAll(peopleColumns);
-        sContactMethodsProjectionMap = map;
-
-        // Contact methods with presence projection map
-        map = new HashMap<String, String>(sContactMethodsProjectionMap);
-        map.putAll(presenceColumns);
-        sContactMethodsWithPresenceProjectionMap = map;
-
-        // Email search projection map
-        map = new HashMap<String, String>();
-        map.put(ContactMethods.NAME, ContactMethods.NAME);
-        map.put(ContactMethods.DATA, ContactMethods.DATA);
-        map.put(ContactMethods._ID, "contact_methods._id AS " + ContactMethods._ID);
-        sEmailSearchProjectionMap = map;
-
-        // Presence projection map
-        map = new HashMap<String, String>();
-        map.put(Presence._ID, "presence._id AS " + Presence._ID);
-        map.putAll(presenceColumns);
-        map.putAll(peopleColumns);
-        sPresenceProjectionMap = map;
-
-        // Search suggestions projection map
-        map = new HashMap<String, String>();
-        map.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
-                DISPLAY_NAME_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
-        map.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
-                SUGGEST_DESCRIPTION_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2);
-        map.put(SearchManager.SUGGEST_COLUMN_ICON_1,
-                "(CASE WHEN " + Photos.DATA + " IS NOT NULL"
-                + " THEN '" + People.CONTENT_URI + "/' || people._id ||"
-                        + " '/" + Photos.CONTENT_DIRECTORY + "/data'"
-                + " ELSE " + com.android.internal.R.drawable.ic_contact_picture
-                + " END) AS " + SearchManager.SUGGEST_COLUMN_ICON_1);
-        map.put(SearchManager.SUGGEST_COLUMN_ICON_2,
-                PRESENCE_ICON_SQL + " AS " + SearchManager.SUGGEST_COLUMN_ICON_2);
-        map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
-                "people._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
-        map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
-                "people._id AS " + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
-        map.put(People._ID, "people._id AS " + People._ID);
-        sSearchSuggestionsProjectionMap = map;
-
-        // Photos projection map
-        map = new HashMap<String, String>();
-        map.put(Photos._ID, Photos._ID);
-        map.put(Photos.LOCAL_VERSION, Photos.LOCAL_VERSION);
-        map.put(Photos.EXISTS_ON_SERVER, Photos.EXISTS_ON_SERVER);
-        map.put(Photos.SYNC_ERROR, Photos.SYNC_ERROR);
-        map.put(Photos.PERSON_ID, Photos.PERSON_ID);
-        map.put(Photos.DATA, Photos.DATA);
-        map.put(Photos.DOWNLOAD_REQUIRED, ""
-                + "(exists_on_server!=0 "
-                + " AND sync_error IS NULL "
-                + " AND (local_version IS NULL OR _sync_version != local_version)) "
-                + "AS " + Photos.DOWNLOAD_REQUIRED);
-        map.putAll(syncColumns);
-        sPhotosProjectionMap = map;
-
-        // Live folder projection
-        map = new HashMap<String, String>();
-        map.put(LiveFolders._ID, "people._id AS " + LiveFolders._ID);
-        map.put(LiveFolders.NAME, DISPLAY_NAME_SQL + " AS " + LiveFolders.NAME);
-        // TODO: Put contact photo back when we have a way to display a default icon
-        // for contacts without a photo
-        // map.put(LiveFolders.ICON_BITMAP, Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
-        sLiveFoldersProjectionMap = map;
-
-        // Order by statements
-        sPhonesKeyOrderBy = buildOrderBy(sPhonesTable, Phones.NUMBER);
-        sContactMethodsKeyOrderBy = buildOrderBy(sContactMethodsTable,
-                ContactMethods.DATA, ContactMethods.KIND);
-        sOrganizationsKeyOrderBy = buildOrderBy(sOrganizationsTable, Organizations.COMPANY);
-        sGroupmembershipKeyOrderBy =
-                buildOrderBy(sGroupmembershipTable, GroupMembership.GROUP_SYNC_ACCOUNT_TYPE,
-                        GroupMembership.GROUP_SYNC_ACCOUNT);
-
-        sPhonesKeyColumns = new String[]{Phones.NUMBER};
-        sContactMethodsKeyColumns = new String[]{ContactMethods.DATA, ContactMethods.KIND};
-        sOrganizationsKeyColumns = new String[]{Organizations.COMPANY};
-        sGroupmembershipKeyColumns = new String[]{GroupMembership.GROUP_SYNC_ACCOUNT,
-                GroupMembership.GROUP_SYNC_ACCOUNT_TYPE};
-        sExtensionsKeyColumns = new String[]{Extensions.NAME};
-
-        String groupJoinByLocalId = "groups._id=groupmembership.group_id";
-        String groupJoinByServerId = "("
-                + "groups._sync_account=groupmembership.group_sync_account"
-                + " AND "
-                + "groups._sync_account_type=groupmembership.group_sync_account_type"
-                + " AND "
-                + "groups._sync_id=groupmembership.group_sync_id"
-                + ")";
-        sGroupsJoinString = "(" + groupJoinByLocalId + " OR " + groupJoinByServerId + ")";
-    }
-}
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 57e1e5d..c95d452 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -24,7 +24,6 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.DisplayNameSources;
 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
@@ -51,8 +50,6 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.Entity;
-import android.content.EntityIterator;
 import android.content.IContentService;
 import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
@@ -60,11 +57,12 @@
 import android.content.UriMatcher;
 import android.content.SharedPreferences.Editor;
 import android.content.res.AssetFileDescriptor;
+import android.content.res.Configuration;
+import android.database.CharArrayBuffer;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteConstraintException;
 import android.database.sqlite.SQLiteContentHelper;
-import android.database.sqlite.SQLiteCursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.database.sqlite.SQLiteStatement;
@@ -83,8 +81,11 @@
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.FullNameStyle;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.PhoneticNameStyle;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
@@ -110,9 +111,11 @@
 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;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -156,6 +159,16 @@
             "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
             + Contacts.STARRED + "=1) + 25";
 
+    /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
+            "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
+            " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
+            " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?";
+
+    /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
+            "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
+            " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
+            " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?";
+
     private static final int CONTACTS = 1000;
     private static final int CONTACTS_ID = 1001;
     private static final int CONTACTS_LOOKUP = 1002;
@@ -214,20 +227,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 +243,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 +278,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 +309,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,16 +353,18 @@
     /** 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. */
     private SQLiteStatement mSetSuperPrimaryStatement;
-    /** Precompiled sql statement for incrementing times contacted for a contact */
-    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;
@@ -398,10 +374,19 @@
     private SQLiteStatement mStatusUpdateReplace;
     private SQLiteStatement mStatusAttributionUpdate;
     private SQLiteStatement mStatusUpdateDelete;
+    private SQLiteStatement mResetNameVerifiedForOtherRawContacts;
 
     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
@@ -467,7 +452,7 @@
                 SEARCH_SUGGESTIONS);
         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
                 SEARCH_SUGGESTIONS);
-        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
+        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
                 SEARCH_SHORTCUT);
 
         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts",
@@ -486,7 +471,14 @@
 
         sContactsProjectionMap = new HashMap<String, String>();
         sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
-        sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
+        sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY);
+        sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.DISPLAY_NAME_ALTERNATIVE);
+        sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
+        sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
+        sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
+        sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
+        sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
         sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
         sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
         sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
@@ -522,7 +514,7 @@
         sContactsVCardProjectionMap = Maps.newHashMap();
         sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME
                 + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME);
-        sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "0 AS " + OpenableColumns.SIZE);
+        sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "NULL AS " + OpenableColumns.SIZE);
 
         sRawContactsProjectionMap = new HashMap<String, String>();
         sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
@@ -533,6 +525,22 @@
         sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
         sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
         sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
+        sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY,
+                RawContacts.DISPLAY_NAME_PRIMARY);
+        sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE,
+                RawContacts.DISPLAY_NAME_ALTERNATIVE);
+        sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE,
+                RawContacts.DISPLAY_NAME_SOURCE);
+        sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME,
+                RawContacts.PHONETIC_NAME);
+        sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE,
+                RawContacts.PHONETIC_NAME_STYLE);
+        sRawContactsProjectionMap.put(RawContacts.NAME_VERIFIED,
+                RawContacts.NAME_VERIFIED);
+        sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY,
+                RawContacts.SORT_KEY_PRIMARY);
+        sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE,
+                RawContacts.SORT_KEY_ALTERNATIVE);
         sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
         sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
                 RawContacts.LAST_TIME_CONTACTED);
@@ -578,8 +586,16 @@
         sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
         sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
         sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
+        sDataProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
         sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
         sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
+        sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.DISPLAY_NAME_ALTERNATIVE);
+        sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
+        sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
+        sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
+        sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
+        sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
         sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
         sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
         sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
@@ -604,6 +620,7 @@
         columns.put(RawContacts.SYNC2, RawContacts.SYNC2);
         columns.put(RawContacts.SYNC3, RawContacts.SYNC3);
         columns.put(RawContacts.SYNC4, RawContacts.SYNC4);
+        columns.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
         columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
         columns.put(Data.MIMETYPE, Data.MIMETYPE);
         columns.put(Data.DATA1, Data.DATA1);
@@ -692,6 +709,14 @@
         sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
         sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
         sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
+        sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.DISPLAY_NAME_ALTERNATIVE);
+        sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
+        sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
+        sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
+        sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
+        sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE,
+                Contacts.SORT_KEY_ALTERNATIVE);
         sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
         sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
         sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
@@ -732,28 +757,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 +909,7 @@
         protected final String mMimetype;
         protected long mMimetypeId;
 
+        @SuppressWarnings("all")
         public DataRowHandler(String mimetype) {
             mMimetype = mimetype;
 
@@ -933,7 +968,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 +981,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 +992,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 +1013,9 @@
             } finally {
                 c.close();
             }
-            return primaryId;
+            if (primaryId != -1) {
+                setIsPrimary(rawContactId, primaryId, mimeTypeId);
+            }
         }
 
         /**
@@ -987,50 +1026,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 +1044,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++) {
@@ -1152,8 +1152,22 @@
                 // if there are non-null values, we know for a fact that some values are present.
                 NameSplitter.Name name = new NameSplitter.Name();
                 name.fromValues(augmented);
-                final String joined = mSplitter.join(name);
+                // As the name could be changed, let's guess the name style again.
+                name.fullNameStyle = FullNameStyle.UNDEFINED;
+                mSplitter.guessNameStyle(name);
+
+                final String joined = mSplitter.join(name, true);
                 update.put(StructuredName.DISPLAY_NAME, joined);
+
+                update.put(StructuredName.FULL_NAME_STYLE, name.fullNameStyle);
+                update.put(StructuredName.PHONETIC_NAME_STYLE, name.phoneticNameStyle);
+            } else if (touchedUnstruct && touchedStruct){
+                if (TextUtils.isEmpty(update.getAsString(StructuredName.FULL_NAME_STYLE))) {
+                    update.put(StructuredName.FULL_NAME_STYLE, mSplitter.guessFullNameStyle(unstruct));
+                }
+                if (TextUtils.isEmpty(update.getAsString(StructuredName.PHONETIC_NAME_STYLE))) {
+                    update.put(StructuredName.PHONETIC_NAME_STYLE, mSplitter.guessPhoneticNameStyle(unstruct));
+                }
             }
         }
     }
@@ -1491,9 +1505,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 +1591,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 +1641,56 @@
         }
     }
 
+    /**
+     * 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 NameSplitter.Name mName = new NameSplitter.Name();
 
     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;
-    }
-
+    private Locale mCurrentLocale;
     @Override
     public boolean onCreate() {
         super.onCreate();
@@ -1677,7 +1707,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();
@@ -1700,23 +1730,21 @@
                                 " FROM " + Tables.RAW_CONTACTS +
                                 " WHERE " + RawContacts._ID + "=?))");
 
-        mContactsLastTimeContactedUpdate = db.compileStatement(
-                "UPDATE " + Tables.CONTACTS +
-                " SET " + Contacts.LAST_TIME_CONTACTED + "=? " +
-                "WHERE " + Contacts._ID + "=?");
-
         mRawContactDisplayNameUpdate = db.compileStatement(
                 "UPDATE " + Tables.RAW_CONTACTS +
-                " SET " + RawContactsColumns.DISPLAY_NAME + "=?,"
-                        + RawContactsColumns.DISPLAY_NAME_SOURCE + "=?" +
+                " SET " +
+                        RawContacts.DISPLAY_NAME_SOURCE + "=?," +
+                        RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
+                        RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
+                        RawContacts.PHONETIC_NAME + "=?," +
+                        RawContacts.PHONETIC_NAME_STYLE + "=?," +
+                        RawContacts.SORT_KEY_PRIMARY + "=?," +
+                        RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
                 " 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,18 +1756,10 @@
                         " 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(
-                context.getString(com.android.internal.R.string.common_name_prefixes),
-                context.getString(com.android.internal.R.string.common_last_name_prefixes),
-                context.getString(com.android.internal.R.string.common_name_suffixes),
-                context.getString(com.android.internal.R.string.common_name_conjunctions),
-                locale);
-        mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
-        mPostalSplitter = new PostalSplitter(locale);
+        initByLocale(context);
 
         mNameLookupInsert = db.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
                 + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + ","
@@ -1785,6 +1805,17 @@
                 "DELETE FROM " + Tables.STATUS_UPDATES +
                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
 
+        // When setting NAME_VERIFIED to 1 on a raw contact, reset it to 0
+        // on all other raw contacts in the same aggregate
+        mResetNameVerifiedForOtherRawContacts = db.compileStatement(
+                "UPDATE " + Tables.RAW_CONTACTS +
+                " SET " + RawContacts.NAME_VERIFIED + "=0" +
+                " WHERE " + RawContacts.CONTACT_ID + "=(" +
+                        "SELECT " + RawContacts.CONTACT_ID +
+                        " FROM " + Tables.RAW_CONTACTS +
+                        " WHERE " + RawContacts._ID + "=?)" +
+                " AND " + RawContacts._ID + "!=?");
+
         mDataRowHandlers = new HashMap<String, DataRowHandler>();
 
         mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
@@ -1810,9 +1841,36 @@
 
         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);
     }
 
+    private void initByLocale(Context context) {
+        mCurrentLocale = getLocale();
+        if (mCurrentLocale == null) {
+            return;
+        }
+        mNameSplitter = new NameSplitter(
+                context.getString(com.android.internal.R.string.common_name_prefixes),
+                context.getString(com.android.internal.R.string.common_last_name_prefixes),
+                context.getString(com.android.internal.R.string.common_name_suffixes),
+                context.getString(com.android.internal.R.string.common_name_conjunctions),
+                mCurrentLocale);
+        mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
+        mPostalSplitter = new PostalSplitter(mCurrentLocale);
+    }
+
+    @Override
+    public void onConfigurationChanged (Configuration newConfig) {
+        if (newConfig != null && mCurrentLocale != null
+                && mCurrentLocale.equals(newConfig.locale)) {
+            initByLocale(getContext());
+        }
+    }
     protected void verifyAccounts() {
         AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
         onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
@@ -1824,14 +1882,15 @@
         return ContactsDatabaseHelper.getInstance(context);
     }
 
-    /* package */ ContactAggregationScheduler getContactAggregationScheduler() {
-        return mAggregationScheduler;
-    }
-
     /* package */ NameSplitter getNameSplitter() {
         return mNameSplitter;
     }
 
+    /* Visible for testing */
+    protected Locale getLocale() {
+        return Locale.getDefault();
+    }
+
     protected boolean isLegacyContactImportNeeded() {
         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
         return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION;
@@ -1852,6 +1911,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 +1919,6 @@
                      */
                     mAccessLatch.countDown();
                     mAccessLatch = null;
-                    scheduleContactAggregation();
                 }
             }
         };
@@ -1884,7 +1943,6 @@
     /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
         boolean aggregatorEnabled = mContactAggregator.isEnabled();
         mContactAggregator.setEnabled(false);
-        mImportMode = true;
         try {
             importer.importContacts();
             mContactAggregator.setEnabled(aggregatorEnabled);
@@ -1892,20 +1950,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 +2021,12 @@
         mInsertedRawContacts.clear();
         mUpdatedRawContacts.clear();
         mUpdatedSyncStates.clear();
+        mDirtyRawContacts.clear();
     }
 
     @Override
     protected void beforeTransactionCommit() {
+
         if (VERBOSE_LOGGING) {
             Log.v(TAG, "beforeTransactionCommit");
         }
@@ -1994,15 +2043,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 +2073,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 +2096,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 +2112,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 +2132,7 @@
             }
 
             case RAW_CONTACTS: {
-                final Account account = readAccountFromQueryParams(uri);
-                id = insertRawContact(values, account);
+                id = insertRawContact(uri, values);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 break;
             }
@@ -2099,8 +2151,7 @@
             }
 
             case GROUPS: {
-                final Account account = readAccountFromQueryParams(uri);
-                id = insertGroup(uri, values, account, callerIsSyncAdapter);
+                id = insertGroup(uri, values, callerIsSyncAdapter);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 break;
             }
@@ -2129,28 +2180,68 @@
     }
 
     /**
-     * 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
+     * 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 uri Current {@link Uri} being operated on.
+     * @param values {@link ContentValues} to read and possibly update.
+     * @throws IllegalArgumentException when only one of
+     *             {@link RawContacts#ACCOUNT_NAME} or
+     *             {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
+     *             other undefined.
+     * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
+     *             and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
+     *             the given {@link Uri} and {@link ContentValues}.
      */
-    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)) {
-                return false;
+    private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
+        String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+        String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
+        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
+
+        String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+        String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+        final boolean partialValues = TextUtils.isEmpty(valueAccountName)
+                ^ TextUtils.isEmpty(valueAccountType);
+
+        if (partialUri || partialValues) {
+            // Throw when either account is incomplete
+            throw new IllegalArgumentException("Must specify both or neither of"
+                    + " ACCOUNT_NAME and ACCOUNT_TYPE");
+        }
+
+        // Accounts are valid by only checking one parameter, since we've
+        // already ruled out partial accounts.
+        final boolean validUri = !TextUtils.isEmpty(accountName);
+        final boolean validValues = !TextUtils.isEmpty(valueAccountName);
+
+        if (validValues && validUri) {
+            // Check that accounts match when both present
+            final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
+                    && TextUtils.equals(accountType, valueAccountType);
+            if (!accountMatch) {
+                throw new IllegalArgumentException("When both specified, "
+                        + " ACCOUNT_NAME and ACCOUNT_TYPE must match");
             }
-            account = valuesAccount;
+        } else if (validUri) {
+            // Fill values from Uri when not present
+            values.put(RawContacts.ACCOUNT_NAME, accountName);
+            values.put(RawContacts.ACCOUNT_TYPE, accountType);
+        } else if (validValues) {
+            accountName = valueAccountName;
+            accountType = valueAccountType;
+        } else {
+            return null;
         }
-        if (account != null) {
-            values.put(RawContacts.ACCOUNT_NAME, account.name);
-            values.put(RawContacts.ACCOUNT_TYPE, account.type);
+
+        // Use cached Account object when matches, otherwise create
+        if (mAccount == null
+                || !mAccount.name.equals(accountName)
+                || !mAccount.type.equals(accountType)) {
+            mAccount = new Account(accountName, accountType);
         }
-        return true;
+
+        return mAccount;
     }
 
     /**
@@ -2166,29 +2257,28 @@
     /**
      * 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)) {
-            return -1;
-        }
+    private long insertRawContact(Uri uri, ContentValues values) {
+        mValues.clear();
+        mValues.putAll(values);
+        mValues.putNull(RawContacts.CONTACT_ID);
+
+        final Account account = resolveAccount(uri, mValues);
 
         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, account);
+
         return rawContactId;
     }
 
@@ -2276,35 +2366,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 +2430,215 @@
                     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 + ","
+                        + Data.DATA2 + ","
+                        + Data.DATA3 + ","
+                        + Data.DATA4 + ","
+                        + Data.DATA5 + ","
+                        + Data.DATA6 + ","
+                        + Data.DATA7 + ","
+                        + Data.DATA8 + ","
+                        + Data.DATA9 + ","
+                        + Data.DATA10 + ","
+                        + Data.DATA11 +
+                " 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 DATA1 = 2;
+        public static final int GIVEN_NAME = 3;                         // data2
+        public static final int FAMILY_NAME = 4;                        // data3
+        public static final int PREFIX = 5;                             // data4
+        public static final int TITLE = 5;                              // data4
+        public static final int MIDDLE_NAME = 6;                        // data5
+        public static final int SUFFIX = 7;                             // data6
+        public static final int PHONETIC_GIVEN_NAME = 8;                // data7
+        public static final int PHONETIC_MIDDLE_NAME = 9;               // data8
+        public static final int ORGANIZATION_PHONETIC_NAME = 9;         // data8
+        public static final int PHONETIC_FAMILY_NAME = 10;              // data9
+        public static final int FULL_NAME_STYLE = 11;                   // data10
+        public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11;  // data10
+        public static final int PHONETIC_NAME_STYLE = 12;               // data11
+    }
+
+    /**
+     * Updates a raw contact display name based on data rows, e.g. structured name,
+     * organization, email etc.
+     */
+    private void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
+        int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
+        NameSplitter.Name bestName = null;
+        String bestDisplayName = null;
+        String bestPhoneticName = null;
+        int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
+
+        mSelectionArgs1[0] = String.valueOf(rawContactId);
+        Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1);
+        try {
+            while (c.moveToNext()) {
+                int mimeType = c.getInt(DisplayNameQuery.MIMETYPE);
+                int source = getDisplayNameSource(mimeType);
+                if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
+                    continue;
+                }
+
+                if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) {
+                    continue;
+                }
+
+                if (mimeType == mMimeTypeIdStructuredName) {
+                    NameSplitter.Name name;
+                    if (bestName != null) {
+                        name = new NameSplitter.Name();
+                    } else {
+                        name = mName;
+                        name.clear();
+                    }
+                    name.prefix = c.getString(DisplayNameQuery.PREFIX);
+                    name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME);
+                    name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME);
+                    name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME);
+                    name.suffix = c.getString(DisplayNameQuery.SUFFIX);
+                    name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE)
+                            ? FullNameStyle.UNDEFINED
+                            : c.getInt(DisplayNameQuery.FULL_NAME_STYLE);
+                    name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME);
+                    name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME);
+                    name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME);
+                    name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE)
+                            ? PhoneticNameStyle.UNDEFINED
+                            : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE);
+                    if (!name.isEmpty()) {
+                        bestDisplayNameSource = source;
+                        bestName = name;
+                    }
+                } else if (mimeType == mMimeTypeIdOrganization) {
+                    mCharArrayBuffer.sizeCopied = 0;
+                    c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
+                    if (mCharArrayBuffer.sizeCopied != 0) {
+                        bestDisplayNameSource = source;
+                        bestDisplayName = new String(mCharArrayBuffer.data, 0,
+                                mCharArrayBuffer.sizeCopied);
+                        bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME);
+                        bestPhoneticNameStyle =
+                                c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE)
+                                    ? PhoneticNameStyle.UNDEFINED
+                                    : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE);
+                    } else {
+                        c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer);
+                        if (mCharArrayBuffer.sizeCopied != 0) {
+                            bestDisplayNameSource = source;
+                            bestDisplayName = new String(mCharArrayBuffer.data, 0,
+                                    mCharArrayBuffer.sizeCopied);
+                            bestPhoneticName = null;
+                            bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
+                        }
+                    }
+                } else {
+                    // Display name is at DATA1 in all other types.
+                    // This is ensured in the constructor.
+
+                    mCharArrayBuffer.sizeCopied = 0;
+                    c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
+                    if (mCharArrayBuffer.sizeCopied != 0) {
+                        bestDisplayNameSource = source;
+                        bestDisplayName = new String(mCharArrayBuffer.data, 0,
+                                mCharArrayBuffer.sizeCopied);
+                        bestPhoneticName = null;
+                        bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
+                    }
+                }
+            }
+
+        } finally {
+            c.close();
+        }
+
+        String displayNamePrimary;
+        String displayNameAlternative;
+        String sortKeyPrimary = null;
+        String sortKeyAlternative = null;
+        int displayNameStyle = FullNameStyle.UNDEFINED;
+
+        if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) {
+            displayNameStyle = bestName.fullNameStyle;
+            if (displayNameStyle == FullNameStyle.CJK
+                    || displayNameStyle == FullNameStyle.UNDEFINED) {
+                displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
+                bestName.fullNameStyle = displayNameStyle;
+            }
+
+            displayNamePrimary = mNameSplitter.join(bestName, true);
+            displayNameAlternative = mNameSplitter.join(bestName, false);
+
+            bestPhoneticName = mNameSplitter.joinPhoneticName(bestName);
+            bestPhoneticNameStyle = bestName.phoneticNameStyle;
+        } else {
+            displayNamePrimary = displayNameAlternative = bestDisplayName;
+        }
+
+        if (bestPhoneticName != null) {
+            sortKeyPrimary = sortKeyAlternative = bestPhoneticName;
+            if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
+                bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName);
+            }
+        } else {
+            if (displayNameStyle == FullNameStyle.UNDEFINED) {
+                displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName);
+                if (displayNameStyle == FullNameStyle.UNDEFINED
+                        || displayNameStyle == FullNameStyle.CJK) {
+                    displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle(
+                            displayNameStyle, bestPhoneticNameStyle);
+                }
+                displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
+            }
+            if (displayNameStyle == FullNameStyle.CHINESE) {
+                sortKeyPrimary = sortKeyAlternative =
+                        ContactLocaleUtils.getSortKey(displayNamePrimary, FullNameStyle.CHINESE);
+            }
+        }
+
+        if (sortKeyPrimary == null) {
+            sortKeyPrimary = displayNamePrimary;
+            sortKeyAlternative = displayNameAlternative;
+        }
+
+        setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary,
+                displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle,
+                sortKeyPrimary, sortKeyAlternative);
+    }
+
+    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 +2677,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 +2715,26 @@
     /**
      * 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)) {
-            return -1;
-        }
+    private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
+        mValues.clear();
+        mValues.putAll(values);
+
+        final Account account = resolveAccount(uri, mValues);
 
         // 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;
         }
 
@@ -2577,32 +2896,32 @@
                 long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
                 mStatusUpdateReplace.bindLong(1, dataId);
                 mStatusUpdateReplace.bindLong(2, timestamp);
-                DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 3, status);
-                DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 4, resPackage);
-                DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 5, iconResource);
-                DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 6, labelResource);
+                bindString(mStatusUpdateReplace, 3, status);
+                bindString(mStatusUpdateReplace, 4, resPackage);
+                bindLong(mStatusUpdateReplace, 5, iconResource);
+                bindLong(mStatusUpdateReplace, 6, labelResource);
                 mStatusUpdateReplace.execute();
             } else {
 
                 try {
                     mStatusUpdateInsert.bindLong(1, dataId);
-                    DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 2, status);
-                    DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 3, resPackage);
-                    DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 4, iconResource);
-                    DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 5, labelResource);
+                    bindString(mStatusUpdateInsert, 2, status);
+                    bindString(mStatusUpdateInsert, 3, resPackage);
+                    bindLong(mStatusUpdateInsert, 4, iconResource);
+                    bindLong(mStatusUpdateInsert, 5, labelResource);
                     mStatusUpdateInsert.executeInsert();
                 } catch (SQLiteConstraintException e) {
                     // The row already exists - update it
                     long timestamp = System.currentTimeMillis();
                     mStatusUpdateAutoTimestamp.bindLong(1, timestamp);
-                    DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 2, status);
+                    bindString(mStatusUpdateAutoTimestamp, 2, status);
                     mStatusUpdateAutoTimestamp.bindLong(3, dataId);
-                    DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 4, status);
+                    bindString(mStatusUpdateAutoTimestamp, 4, status);
                     mStatusUpdateAutoTimestamp.execute();
 
-                    DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 1, resPackage);
-                    DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 2, iconResource);
-                    DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 3, labelResource);
+                    bindString(mStatusAttributionUpdate, 1, resPackage);
+                    bindLong(mStatusAttributionUpdate, 2, iconResource);
+                    bindLong(mStatusAttributionUpdate, 3, labelResource);
                     mStatusAttributionUpdate.bindLong(4, dataId);
                     mStatusAttributionUpdate.execute();
                 }
@@ -2661,12 +2980,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 +2998,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 +3014,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: {
@@ -2718,7 +3042,7 @@
 
             case SETTINGS: {
                 mSyncToNetwork |= !callerIsSyncAdapter;
-                return deleteSettings(uri, selection, selectionArgs);
+                return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs);
             }
 
             case STATUS_UPDATES: {
@@ -2732,14 +3056,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 +3099,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 +3113,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 +3147,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 +3230,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 +3251,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);
@@ -2938,11 +3268,17 @@
             }
 
             case SETTINGS: {
-                count = updateSettings(uri, values, selection, selectionArgs);
+                count = updateSettings(uri, values, appendAccountToSelection(uri, selection),
+                        selectionArgs);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 break;
             }
 
+            case STATUS_UPDATES: {
+                count = updateStatusUpdate(uri, values, selection, selectionArgs);
+                break;
+            }
+
             default: {
                 mSyncToNetwork = true;
                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
@@ -2952,9 +3288,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;
@@ -2971,9 +3366,8 @@
         }
         if (updatedValues.containsKey(Groups.SHOULD_SYNC)
                 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
-            final long groupId = ContentUris.parseId(uri);
             Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
-                    Groups.ACCOUNT_TYPE}, Groups._ID + "=" + groupId, null, null,
+                    Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null,
                     null, null);
             String accountName;
             String accountType;
@@ -2983,7 +3377,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 +3426,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();
@@ -3045,6 +3443,7 @@
             values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
                     ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
         }
+
         int count = mDb.update(Tables.RAW_CONTACTS, values, selection, null);
         if (count != 0) {
             if (values.containsKey(RawContacts.STARRED)) {
@@ -3053,9 +3452,20 @@
             if (values.containsKey(RawContacts.SOURCE_ID)) {
                 mContactAggregator.updateLookupKey(mDb, rawContactId);
             }
+            if (values.containsKey(RawContacts.NAME_VERIFIED)) {
+
+                // If setting NAME_VERIFIED for this raw contact, reset it for all
+                // other raw contacts in the same aggregate
+                if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) {
+                    mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId);
+                    mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId);
+                    mResetNameVerifiedForOtherRawContacts.execute();
+                }
+                mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId);
+            }
             if (requestUndoDelete && previousDeleted == 1) {
                 // undo delete, needs aggregation again.
-                mInsertedRawContacts.add(rawContactId);
+                mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType));
             }
         }
         return count;
@@ -3164,7 +3574,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,13 +3591,14 @@
         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
                 values, Contacts.STARRED);
 
-        return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=" + contactId, null);
-    }
+        int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1);
 
-    public void updateContactLastContactedTime(long contactId, long lastTimeContacted) {
-        mContactsLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
-        mContactsLastTimeContactedUpdate.bindLong(2, contactId);
-        mContactsLastTimeContactedUpdate.execute();
+        if (values.containsKey(Contacts.LAST_TIME_CONTACTED) &&
+                !values.containsKey(Contacts.TIMES_CONTACTED)) {
+            mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
+            mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
+        }
+        return rslt;
     }
 
     private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
@@ -3204,9 +3616,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 +3630,7 @@
                     exceptionValues);
         }
 
+        mContactAggregator.invalidateAggregationExceptionCache();
         mContactAggregator.markForAggregation(rawContactId1);
         mContactAggregator.markForAggregation(rawContactId2);
 
@@ -3387,7 +3802,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;
             }
 
@@ -3400,13 +3816,21 @@
                 }
                 String lookupKey = pathSegments.get(2);
                 if (segmentCount == 4) {
+                    // TODO: pull this out into a method and generalize to not require contactId
                     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 +3840,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 +3851,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 +3933,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 +3955,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,13 +3967,13 @@
                 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);
                     if (normalizedName.length() > 0) {
                         sb.append(Data.RAW_CONTACT_ID + " IN ");
-                        appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false);
+                        appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
                         orNeeded = true;
                     }
 
@@ -3560,7 +3991,7 @@
                         sb.append("')");
                     }
                     sb.append(")");
-                    qb.appendWhere(" AND " + sb);
+                    qb.appendWhere(sb);
                 }
                 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
                 if (sortOrder == null) {
@@ -3577,8 +4008,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 +4018,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;
             }
@@ -3631,7 +4063,7 @@
                                     " FROM " + Tables.DATA +
                                     " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
                                     " AND " + Data.RAW_CONTACT_ID + " IN ");
-                            appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false);
+                            appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
                         }
                     }
                     sb.append(")");
@@ -3653,9 +4085,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 +4100,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 +4120,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 +4151,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 +4219,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;
             }
 
@@ -3792,8 +4229,8 @@
             }
 
             case SEARCH_SHORTCUT: {
-                long contactId = ContentUris.parseId(uri);
-                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection);
+                String lookupKey = uri.getLastPathSegment();
+                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection);
             }
 
             case LIVE_FOLDERS_CONTACTS:
@@ -3828,7 +4265,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;
             }
 
@@ -3855,7 +4293,12 @@
         return c;
     }
 
-    private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
+    /**
+     * Returns the contact Id for the contact identified by the lookupKey.  Robust against changes
+     * in the lookup key:  if the key has changed, will look up the contact by the name encoded in
+     * the lookup key.
+     */
+    public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
         ContactLookupKey key = new ContactLookupKey();
         ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
 
@@ -4043,7 +4486,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 +4515,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 +4531,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 +4549,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,9 +4630,20 @@
     }
 
     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
-        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
-        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
-        if (!TextUtils.isEmpty(accountName)) {
+        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
+
+        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
+        if (partialUri) {
+            // Throw when either account is incomplete
+            throw new IllegalArgumentException("Must specify both or neither of"
+                    + " ACCOUNT_NAME and ACCOUNT_TYPE");
+        }
+
+        // Accounts are valid by only checking one parameter, since we've
+        // already ruled out partial accounts.
+        final boolean validAccount = !TextUtils.isEmpty(accountName);
+        if (validAccount) {
             qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
                     + RawContacts.ACCOUNT_TYPE + "="
@@ -4200,9 +4654,20 @@
     }
 
     private String appendAccountToSelection(Uri uri, String selection) {
-        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
-        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
-        if (!TextUtils.isEmpty(accountName)) {
+        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
+
+        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
+        if (partialUri) {
+            // Throw when either account is incomplete
+            throw new IllegalArgumentException("Must specify both or neither of"
+                    + " ACCOUNT_NAME and ACCOUNT_TYPE");
+        }
+
+        // Accounts are valid by only checking one parameter, since we've
+        // already ruled out partial accounts.
+        final boolean validAccount = !TextUtils.isEmpty(accountName);
+        if (validAccount) {
             StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
                     + RawContacts.ACCOUNT_TYPE + "="
@@ -4224,8 +4689,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 +4742,7 @@
         if (mDbHelper.hasAccessToRestrictedData()) {
             return "1";
         } else {
-            return RawContacts.IS_RESTRICTED + "=0";
+            return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0";
         }
     }
 
@@ -4299,14 +4764,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,401 +4840,13 @@
         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.
-     */
-    private static class RawContactsEntityIterator implements EntityIterator {
-        private final Cursor mEntityCursor;
-        private volatile boolean mIsClosed;
-
-        private static final String[] DATA_KEYS = new String[]{
-                Data.DATA1,
-                Data.DATA2,
-                Data.DATA3,
-                Data.DATA4,
-                Data.DATA5,
-                Data.DATA6,
-                Data.DATA7,
-                Data.DATA8,
-                Data.DATA9,
-                Data.DATA10,
-                Data.DATA11,
-                Data.DATA12,
-                Data.DATA13,
-                Data.DATA14,
-                Data.DATA15,
-                Data.SYNC1,
-                Data.SYNC2,
-                Data.SYNC3,
-                Data.SYNC4};
-
-        public static final String[] PROJECTION = new String[]{
-                RawContacts.ACCOUNT_NAME,
-                RawContacts.ACCOUNT_TYPE,
-                RawContacts.SOURCE_ID,
-                RawContacts.VERSION,
-                RawContacts.DIRTY,
-                RawContacts.Entity.DATA_ID,
-                Data.RES_PACKAGE,
-                Data.MIMETYPE,
-                Data.DATA1,
-                Data.DATA2,
-                Data.DATA3,
-                Data.DATA4,
-                Data.DATA5,
-                Data.DATA6,
-                Data.DATA7,
-                Data.DATA8,
-                Data.DATA9,
-                Data.DATA10,
-                Data.DATA11,
-                Data.DATA12,
-                Data.DATA13,
-                Data.DATA14,
-                Data.DATA15,
-                Data.SYNC1,
-                Data.SYNC2,
-                Data.SYNC3,
-                Data.SYNC4,
-                RawContacts._ID,
-                Data.IS_PRIMARY,
-                Data.IS_SUPER_PRIMARY,
-                Data.DATA_VERSION,
-                GroupMembership.GROUP_SOURCE_ID,
-                RawContacts.SYNC1,
-                RawContacts.SYNC2,
-                RawContacts.SYNC3,
-                RawContacts.SYNC4,
-                RawContacts.DELETED,
-                RawContacts.CONTACT_ID,
-                RawContacts.STARRED,
-                RawContacts.IS_RESTRICTED};
-
-        private static final int COLUMN_ACCOUNT_NAME = 0;
-        private static final int COLUMN_ACCOUNT_TYPE = 1;
-        private static final int COLUMN_SOURCE_ID = 2;
-        private static final int COLUMN_VERSION = 3;
-        private static final int COLUMN_DIRTY = 4;
-        private static final int COLUMN_DATA_ID = 5;
-        private static final int COLUMN_RES_PACKAGE = 6;
-        private static final int COLUMN_MIMETYPE = 7;
-        private static final int COLUMN_DATA1 = 8;
-        private static final int COLUMN_RAW_CONTACT_ID = 27;
-        private static final int COLUMN_IS_PRIMARY = 28;
-        private static final int COLUMN_IS_SUPER_PRIMARY = 29;
-        private static final int COLUMN_DATA_VERSION = 30;
-        private static final int COLUMN_GROUP_SOURCE_ID = 31;
-        private static final int COLUMN_SYNC1 = 32;
-        private static final int COLUMN_SYNC2 = 33;
-        private static final int COLUMN_SYNC3 = 34;
-        private static final int COLUMN_SYNC4 = 35;
-        private static final int COLUMN_DELETED = 36;
-        private static final int COLUMN_CONTACT_ID = 37;
-        private static final int COLUMN_STARRED = 38;
-        private static final int COLUMN_IS_RESTRICTED = 39;
-
-        public RawContactsEntityIterator(ContactsProvider2 provider, Uri entityUri,
-                String contactsIdString,
-                String selection, String[] selectionArgs, String sortOrder) {
-            mIsClosed = false;
-            Uri uri;
-            if (contactsIdString != null) {
-                uri = Uri.withAppendedPath(RawContacts.CONTENT_URI, contactsIdString);
-                uri = Uri.withAppendedPath(uri, RawContacts.Entity.CONTENT_DIRECTORY);
-            } else {
-                uri = ContactsContract.RawContactsEntity.CONTENT_URI;
-            }
-            final Uri.Builder builder = uri.buildUpon();
-            String query = entityUri.getQuery();
-            builder.encodedQuery(query);
-            mEntityCursor = provider.query(builder.build(),
-                    PROJECTION, selection, selectionArgs, sortOrder);
-            mEntityCursor.moveToFirst();
-        }
-
-        public void reset() throws RemoteException {
-            if (mIsClosed) {
-                throw new IllegalStateException("calling reset() when the iterator is closed");
-            }
-            mEntityCursor.moveToFirst();
-        }
-
-        public void close() {
-            if (mIsClosed) {
-                throw new IllegalStateException("closing when already closed");
-            }
-            mIsClosed = true;
-            mEntityCursor.close();
-        }
-
-        public boolean hasNext() throws RemoteException {
-            if (mIsClosed) {
-                throw new IllegalStateException("calling hasNext() when the iterator is closed");
-            }
-
-            return !mEntityCursor.isAfterLast();
-        }
-
-        public Entity next() throws RemoteException {
-            if (mIsClosed) {
-                throw new IllegalStateException("calling next() when the iterator is closed");
-            }
-            if (!hasNext()) {
-                throw new IllegalStateException("you may only call next() if hasNext() is true");
-            }
-
-            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
-
-            final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID);
-
-            // we expect the cursor is already at the row we need to read from
-            ContentValues contactValues = new ContentValues();
-            contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
-            contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
-            contactValues.put(RawContacts._ID, rawContactId);
-            contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY));
-            contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION));
-            contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
-            contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1));
-            contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2));
-            contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3));
-            contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4));
-            contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED));
-            contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID));
-            contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED));
-            contactValues.put(RawContacts.IS_RESTRICTED, c.getInt(COLUMN_IS_RESTRICTED));
-            Entity contact = new Entity(contactValues);
-
-            // read data rows until the contact id changes
-            do {
-                if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) {
-                    break;
-                }
-//                if (c.isNull(COLUMN_CONTACT_ID)) {
-//                    continue;
-//                }
-                // add the data to to the contact
-                ContentValues dataValues = new ContentValues();
-                dataValues.put(Data._ID, c.getLong(COLUMN_DATA_ID));
-                dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
-                dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
-                dataValues.put(Data.IS_PRIMARY, c.getLong(COLUMN_IS_PRIMARY));
-                dataValues.put(Data.IS_SUPER_PRIMARY, c.getLong(COLUMN_IS_SUPER_PRIMARY));
-                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
-                if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
-                    dataValues.put(GroupMembership.GROUP_SOURCE_ID,
-                            c.getString(COLUMN_GROUP_SOURCE_ID));
-                }
-                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
-                for (int i = 0; i < DATA_KEYS.length; i++) {
-                    final int columnIndex = i + COLUMN_DATA1;
-                    String key = DATA_KEYS[i];
-                    if (c.isNull(columnIndex)) {
-                        // don't put anything
-                    } else if (c.isLong(columnIndex)) {
-                        dataValues.put(key, c.getLong(columnIndex));
-                    } else if (c.isFloat(columnIndex)) {
-                        dataValues.put(key, c.getFloat(columnIndex));
-                    } else if (c.isString(columnIndex)) {
-                        dataValues.put(key, c.getString(columnIndex));
-                    } else if (c.isBlob(columnIndex)) {
-                        dataValues.put(key, c.getBlob(columnIndex));
-                    }
-                }
-                contact.addSubValue(Data.CONTENT_URI, dataValues);
-            } while (mEntityCursor.moveToNext());
-
-            return contact;
-        }
-    }
-
-    /**
-     * 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.
-     */
-    private static class GroupsEntityIterator implements EntityIterator {
-        private final Cursor mEntityCursor;
-        private volatile boolean mIsClosed;
-
-        private static final String[] PROJECTION = new String[]{
-                Groups._ID,
-                Groups.ACCOUNT_NAME,
-                Groups.ACCOUNT_TYPE,
-                Groups.SOURCE_ID,
-                Groups.DIRTY,
-                Groups.VERSION,
-                Groups.RES_PACKAGE,
-                Groups.TITLE,
-                Groups.TITLE_RES,
-                Groups.GROUP_VISIBLE,
-                Groups.SYNC1,
-                Groups.SYNC2,
-                Groups.SYNC3,
-                Groups.SYNC4,
-                Groups.SYSTEM_ID,
-                Groups.NOTES,
-                Groups.DELETED,
-                Groups.SHOULD_SYNC};
-
-        private static final int COLUMN_ID = 0;
-        private static final int COLUMN_ACCOUNT_NAME = 1;
-        private static final int COLUMN_ACCOUNT_TYPE = 2;
-        private static final int COLUMN_SOURCE_ID = 3;
-        private static final int COLUMN_DIRTY = 4;
-        private static final int COLUMN_VERSION = 5;
-        private static final int COLUMN_RES_PACKAGE = 6;
-        private static final int COLUMN_TITLE = 7;
-        private static final int COLUMN_TITLE_RES = 8;
-        private static final int COLUMN_GROUP_VISIBLE = 9;
-        private static final int COLUMN_SYNC1 = 10;
-        private static final int COLUMN_SYNC2 = 11;
-        private static final int COLUMN_SYNC3 = 12;
-        private static final int COLUMN_SYNC4 = 13;
-        private static final int COLUMN_SYSTEM_ID = 14;
-        private static final int COLUMN_NOTES = 15;
-        private static final int COLUMN_DELETED = 16;
-        private static final int COLUMN_SHOULD_SYNC = 17;
-
-        public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri,
-                String selection, String[] selectionArgs, String sortOrder) {
-            mIsClosed = false;
-
-            final String updatedSortOrder = (sortOrder == null)
-                    ? Groups._ID
-                    : (Groups._ID + "," + sortOrder);
-
-            final SQLiteDatabase db = provider.mDbHelper.getReadableDatabase();
-            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-            qb.setTables(provider.mDbHelper.getGroupView());
-            qb.setProjectionMap(sGroupsProjectionMap);
-            if (groupIdString != null) {
-                qb.appendWhere(Groups._ID + "=" + groupIdString);
-            }
-            final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
-            final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
-            if (!TextUtils.isEmpty(accountName)) {
-                qb.appendWhere(Groups.ACCOUNT_NAME + "="
-                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
-                        + Groups.ACCOUNT_TYPE + "="
-                        + DatabaseUtils.sqlEscapeString(accountType));
-            }
-            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
-                    null, null, updatedSortOrder);
-            mEntityCursor.moveToFirst();
-        }
-
-        public void close() {
-            if (mIsClosed) {
-                throw new IllegalStateException("closing when already closed");
-            }
-            mIsClosed = true;
-            mEntityCursor.close();
-        }
-
-        public boolean hasNext() throws RemoteException {
-            if (mIsClosed) {
-                throw new IllegalStateException("calling hasNext() when the iterator is closed");
-            }
-
-            return !mEntityCursor.isAfterLast();
-        }
-
-        public void reset() throws RemoteException {
-            if (mIsClosed) {
-                throw new IllegalStateException("calling reset() when the iterator is closed");
-            }
-            mEntityCursor.moveToFirst();
-        }
-
-        public Entity next() throws RemoteException {
-            if (mIsClosed) {
-                throw new IllegalStateException("calling next() when the iterator is closed");
-            }
-            if (!hasNext()) {
-                throw new IllegalStateException("you may only call next() if hasNext() is true");
-            }
-
-            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
-
-            final long groupId = c.getLong(COLUMN_ID);
-
-            // we expect the cursor is already at the row we need to read from
-            ContentValues groupValues = new ContentValues();
-            groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
-            groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
-            groupValues.put(Groups._ID, groupId);
-            groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY));
-            groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION));
-            groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
-            groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
-            groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE));
-            groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES));
-            groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE));
-            groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1));
-            groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2));
-            groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3));
-            groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4));
-            groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID));
-            groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED));
-            groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES));
-            groupValues.put(Groups.SHOULD_SYNC, c.getString(COLUMN_SHOULD_SYNC));
-            Entity group = new Entity(groupValues);
-
-            mEntityCursor.moveToNext();
-
-            return group;
-        }
-    }
-
-    @Override
-    public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
-            String sortOrder) {
-        waitForAccess();
-
-        final int match = sUriMatcher.match(uri);
-        switch (match) {
-            case RAW_CONTACTS:
-            case RAW_CONTACTS_ID:
-                String contactsIdString = null;
-                if (match == RAW_CONTACTS_ID) {
-                    contactsIdString = uri.getPathSegments().get(1);
-                }
-
-                return new RawContactsEntityIterator(this, uri, contactsIdString,
-                        selection, selectionArgs, sortOrder);
-            case GROUPS:
-            case GROUPS_ID:
-                String idString = null;
-                if (match == GROUPS_ID) {
-                    idString = uri.getPathSegments().get(1);
-                }
-
-                return new GroupsEntityIterator(this, idString,
-                        uri, selection, selectionArgs, sortOrder);
-            default:
-                throw new UnsupportedOperationException("Unknown uri: " + uri);
-        }
-    }
-
     @Override
     public String getType(Uri uri) {
         final int match = sUriMatcher.match(uri);
         switch (match) {
             case CONTACTS:
-            case CONTACTS_LOOKUP:
                 return Contacts.CONTENT_TYPE;
+            case CONTACTS_LOOKUP:
             case CONTACTS_ID:
             case CONTACTS_LOOKUP_ID:
                 return Contacts.CONTENT_ITEM_TYPE;
@@ -4811,14 +4887,17 @@
         }
     }
 
-    private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) {
-        if (displayName != null) {
-            mRawContactDisplayNameUpdate.bindString(1, displayName);
-        } else {
-            mRawContactDisplayNameUpdate.bindNull(1);
-        }
-        mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource);
-        mRawContactDisplayNameUpdate.bindLong(3, rawContactId);
+    private void setDisplayName(long rawContactId, int displayNameSource,
+            String displayNamePrimary, String displayNameAlternative, String phoneticName,
+            int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) {
+        mRawContactDisplayNameUpdate.bindLong(1, displayNameSource);
+        bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary);
+        bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative);
+        bindString(mRawContactDisplayNameUpdate, 4, phoneticName);
+        mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle);
+        bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary);
+        bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative);
+        mRawContactDisplayNameUpdate.bindLong(8, rawContactId);
         mRawContactDisplayNameUpdate.execute();
     }
 
@@ -4826,8 +4905,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);
     }
 
     /*
@@ -4902,12 +4980,70 @@
 
     public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name) {
         mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name);
+        if (!TextUtils.isEmpty(name)) {
+            Iterator<String> it = ContactLocaleUtils.getNameLookupKeys(name, FullNameStyle.CHINESE);
+            if (it != null) {
+                while(it.hasNext()) {
+                    String key = it.next();
+                    mNameLookupBuilder.insertNameLookup(rawContactId, dataId,
+                            NameLookupType.NAME_SHORTHAND,
+                            mNameLookupBuilder.normalizeName(key));
+                }
+            }
+        }
     }
 
+    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 +5066,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;
@@ -4973,10 +5119,10 @@
      * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
      */
     public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
-        DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 1, rawContactId);
-        DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 2, dataId);
-        DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 3, lookupType);
-        DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 4, name);
+        mNameLookupInsert.bindLong(1, rawContactId);
+        mNameLookupInsert.bindLong(2, dataId);
+        mNameLookupInsert.bindLong(3, lookupType);
+        bindString(mNameLookupInsert, 4, name);
         mNameLookupInsert.executeInsert();
     }
 
@@ -4984,7 +5130,7 @@
      * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
      */
     public void deleteNameLookup(long dataId) {
-        DatabaseUtils.bindObjectToProgram(mNameLookupDelete, 1, dataId);
+        mNameLookupDelete.bindLong(1, dataId);
         mNameLookupDelete.execute();
     }
 
@@ -5001,25 +5147,24 @@
                 + NameLookupType.NAME_COLLATION_KEY + ","
                 + NameLookupType.EMAIL_BASED_NICKNAME + ","
                 + NameLookupType.NICKNAME + ","
+                + NameLookupType.NAME_SHORTHAND + ","
                 + NameLookupType.ORGANIZATION + "))");
     }
 
     public String getRawContactsByFilterAsNestedQuery(String filterParam) {
         StringBuilder sb = new StringBuilder();
-        appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
+        appendRawContactsByFilterAsNestedQuery(sb, filterParam);
         return sb.toString();
     }
 
-    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
-            String limit) {
-        appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit,
-                true);
+    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) {
+        appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true);
     }
 
     private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
-            String limit, boolean allowEmailMatch) {
+            boolean allowEmailMatch) {
         sb.append("(" +
-                "SELECT DISTINCT " + NameLookupColumns.RAW_CONTACT_ID +
+                "SELECT " + NameLookupColumns.RAW_CONTACT_ID +
                 " FROM " + Tables.NAME_LOOKUP +
                 " WHERE " + NameLookupColumns.NORMALIZED_NAME +
                 " GLOB '");
@@ -5027,16 +5172,12 @@
         sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
                 + NameLookupType.NAME_COLLATION_KEY + ","
                 + NameLookupType.NICKNAME + ","
+                + NameLookupType.NAME_SHORTHAND + ","
                 + NameLookupType.ORGANIZATION);
         if (allowEmailMatch) {
             sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME);
         }
-        sb.append(")");
-
-        if (limit != null) {
-            sb.append(" LIMIT ").append(limit);
-        }
-        sb.append(")");
+        sb.append("))");
     }
 
     /**
@@ -5091,7 +5232,91 @@
         } 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);
+    }
+
+    private void bindString(SQLiteStatement stmt, int index, String value) {
+        if (value == null) {
+            stmt.bindNull(index);
+        } else {
+            stmt.bindString(index, value);
+        }
+    }
+
+    private void bindLong(SQLiteStatement stmt, int index, Number value) {
+        if (value == null) {
+            stmt.bindNull(index);
+        } else {
+            stmt.bindLong(index, value.longValue());
+        }
+    }
 }
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index e939ef7..d890310 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -16,11 +16,11 @@
 
 package com.android.providers.contacts;
 
-import com.android.internal.database.ArrayListCursor;
+import com.android.common.ArrayListCursor;
 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,36 +86,38 @@
 
         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,
+            Contacts.LOOKUP_KEY,
         };
 
         public static final int CONTACT_ID = 0;
         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;
+        public static final int LOOKUP_KEY = 8;
     }
 
     private static class SearchSuggestion {
-        String contactId;
+        long contactId;
         boolean titleIsName;
         String organization;
         String email;
         String phoneNumber;
         Uri photoUri;
+        String lookupKey;
         String normalizedName;
         int presence = -1;
         boolean processed;
@@ -136,7 +127,7 @@
         String icon2;
 
         public SearchSuggestion(long contactId) {
-            this.contactId = String.valueOf(contactId);
+            this.contactId = contactId;
         }
 
         private void process() {
@@ -174,6 +165,11 @@
             processed = true;
         }
 
+        /**
+         * Returns key for sorting search suggestions.
+         *
+         * <p>TODO: switch to new sort key
+         */
         public String getSortKey() {
             if (normalizedName == null) {
                 process();
@@ -193,8 +189,8 @@
                 list.add(text2);
                 list.add(icon1);
                 list.add(icon2);
-                list.add(contactId);
-                list.add(contactId);
+                list.add(lookupKey);
+                list.add(lookupKey);
             } else {
                 for (int i = 0; i < projection.length; i++) {
                     addColumnValue(list, projection[i]);
@@ -215,9 +211,9 @@
             } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) {
                 list.add(icon2);
             } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) {
-                list.add(contactId);
+                list.add(lookupKey);
             } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) {
-                list.add(contactId);
+                list.add(lookupKey);
             } else {
                 throw new IllegalArgumentException("Invalid column name: " + column);
             }
@@ -225,9 +221,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 +264,30 @@
         }
     }
 
-    public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, long contactId, String[] projection) {
+    /**
+     * Returns a search suggestions cursor for the contact bearing the provided lookup key.  If the
+     * lookup key cannot be found in the database, the contact name is decoded from the lookup key
+     * and used to re-identify the contact.  If the contact still cannot be found, an empty cursor
+     * is returned.
+     *
+     * <p>Note that if {@code lookupKey} is not a valid lookup key, an empty cursor is returned
+     * silently.  This would occur with old-style shortcuts that were created using the contact id
+     * instead of the lookup key.
+     */
+    public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, String lookupKey,
+            String[] projection) {
+        ensureMimetypeIdsLoaded();
+        long contactId;
+        try {
+            contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey);
+        } catch (IllegalArgumentException e) {
+            contactId = -1L;
+        }
         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 " + ContactsColumns.CONCRETE_ID + "=" + contactId);
+        return buildCursorForSearchSuggestions(db, sb.toString(), projection, null);
     }
 
     private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) {
@@ -296,22 +336,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");
+        mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause);
 
-        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 +394,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);
                     }
@@ -352,6 +416,8 @@
                             ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
                             Photo.CONTENT_DIRECTORY);
                 }
+
+                suggestion.lookupKey = c.getString(SearchSuggestionQuery.LOOKUP_KEY);
             }
         } finally {
             c.close();
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index 65120f9..898eb45 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -45,6 +45,7 @@
 import android.provider.Contacts.ContactMethods;
 import android.provider.Contacts.Extensions;
 import android.provider.Contacts.People;
+import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
@@ -188,6 +189,18 @@
     private static final Uri LIVE_FOLDERS_CONTACTS_FAVORITES_URI = Uri.withAppendedPath(
             ContactsContract.AUTHORITY_URI, "live_folders/favorites");
 
+    private static final String CONTACTS_UPDATE_LASTTIMECONTACTED =
+            "UPDATE " + Tables.CONTACTS +
+            " SET " + Contacts.LAST_TIME_CONTACTED + "=? " +
+            "WHERE " + Contacts._ID + "=?";
+    private static final String RAWCONTACTS_UPDATE_LASTTIMECONTACTED =
+            "UPDATE " + Tables.RAW_CONTACTS + " SET "
+            + RawContacts.LAST_TIME_CONTACTED + "=? WHERE "
+            + RawContacts._ID + "=?";
+
+    private String[] mSelectionArgs1 = new String[1];
+    private String[] mSelectionArgs2 = new String[2];
+
     public interface LegacyTables {
         public static final String PEOPLE = "view_v1_people";
         public static final String PEOPLE_JOIN_PRESENCE = "view_v1_people people " + PRESENCE_JOINS;
@@ -335,7 +348,7 @@
                 SEARCH_SUGGESTIONS);
         matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
                 SEARCH_SUGGESTIONS);
-        matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
+        matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
                 SEARCH_SHORTCUT);
         matcher.addURI(authority, "settings", SETTINGS);
 
@@ -484,8 +497,6 @@
     private final NameSplitter mPhoneticNameSplitter;
     private final GlobalSearchSupport mGlobalSearchSupport;
 
-    /** Precompiled sql statement for incrementing times contacted for a contact */
-    private final SQLiteStatement mLastTimeContactedUpdate;
     private final SQLiteStatement mDataMimetypeQuery;
     private final SQLiteStatement mDataRawContactIdQuery;
 
@@ -512,10 +523,6 @@
                 .getDefault());
 
         SQLiteDatabase db = mDbHelper.getReadableDatabase();
-        mLastTimeContactedUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
-                + RawContacts.LAST_TIME_CONTACTED + "=? WHERE "
-                + RawContacts._ID + "=?");
-
         mDataMimetypeQuery = db.compileStatement(
                 "SELECT " + DataColumns.MIMETYPE_ID +
                 " FROM " + Tables.DATA +
@@ -1144,14 +1151,20 @@
 
         long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
         long contactId = mDbHelper.getContactId(rawContactId);
+        SQLiteDatabase mDb = mDbHelper.getWritableDatabase();
+        mSelectionArgs2[0] = String.valueOf(lastTimeContacted);
         if (contactId != 0) {
-            mContactsProvider.updateContactLastContactedTime(contactId, lastTimeContacted);
+            mSelectionArgs2[1] = String.valueOf(contactId);
+            mDb.execSQL(CONTACTS_UPDATE_LASTTIMECONTACTED, mSelectionArgs2);
+            // increment times_contacted column
+            mSelectionArgs1[0] = String.valueOf(contactId);
+            mDb.execSQL(ContactsProvider2.UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
         }
-
-        mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
-        mLastTimeContactedUpdate.bindLong(2, rawContactId);
-        mLastTimeContactedUpdate.execute();
-
+        mSelectionArgs2[1] = String.valueOf(rawContactId);
+        mDb.execSQL(RAWCONTACTS_UPDATE_LASTTIMECONTACTED, mSelectionArgs2);
+        // increment times_contacted column
+        mSelectionArgs1[0] = String.valueOf(contactId);
+        mDb.execSQL(ContactsProvider2.UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
         return 1;
     }
 
@@ -1531,7 +1544,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:
@@ -1857,8 +1870,8 @@
                 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
 
             case SEARCH_SHORTCUT: {
-                long contactId = ContentUris.parseId(uri);
-                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection);
+                String lookupKey = uri.getLastPathSegment();
+                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection);
             }
 
             case LIVE_FOLDERS_PEOPLE:
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/NameSplitter.java b/src/com/android/providers/contacts/NameSplitter.java
index 873f83b..0b98343 100644
--- a/src/com/android/providers/contacts/NameSplitter.java
+++ b/src/com/android/providers/contacts/NameSplitter.java
@@ -15,10 +15,17 @@
  */
 package com.android.providers.contacts;
 
+import com.android.internal.util.HanziToPinyin;
+import com.android.internal.util.HanziToPinyin.Token;
+
 import android.content.ContentValues;
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhoneticNameStyle;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.text.TextUtils;
 
+import java.lang.Character.UnicodeBlock;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.StringTokenizer;
@@ -42,19 +49,34 @@
 
     public static final int MAX_TOKENS = 10;
 
+    private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase();
+    private static final String KOREAN_LANGUAGE = Locale.KOREAN.getLanguage().toLowerCase();
+
+    // This includes simplified and traditional Chinese
+    private static final String CHINESE_LANGUAGE = Locale.CHINESE.getLanguage().toLowerCase();
+
     private final HashSet<String> mPrefixesSet;
     private final HashSet<String> mSuffixesSet;
     private final int mMaxSuffixLength;
     private final HashSet<String> mLastNamePrefixesSet;
     private final HashSet<String> mConjuctions;
     private final Locale mLocale;
+    private final String mLanguage;
 
     public static class Name {
-        private String prefix;
-        private String givenNames;
-        private String middleName;
-        private String familyName;
-        private String suffix;
+        public String prefix;
+        public String givenNames;
+        public String middleName;
+        public String familyName;
+        public String suffix;
+
+        public int fullNameStyle;
+
+        public String phoneticFamilyName;
+        public String phoneticMiddleName;
+        public String phoneticGivenName;
+
+        public int phoneticNameStyle;
 
         public Name() {
         }
@@ -94,20 +116,73 @@
             middleName = values.getAsString(StructuredName.MIDDLE_NAME);
             familyName = values.getAsString(StructuredName.FAMILY_NAME);
             suffix = values.getAsString(StructuredName.SUFFIX);
+
+            Integer integer = values.getAsInteger(StructuredName.FULL_NAME_STYLE);
+            fullNameStyle = integer == null ? FullNameStyle.UNDEFINED : integer;
+
+            phoneticFamilyName = values.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+            phoneticMiddleName = values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+            phoneticGivenName = values.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+
+            integer = values.getAsInteger(StructuredName.PHONETIC_NAME_STYLE);
+            phoneticNameStyle = integer == null ? PhoneticNameStyle.UNDEFINED : integer;
         }
 
         public void toValues(ContentValues values) {
-            values.put(StructuredName.PREFIX, prefix);
-            values.put(StructuredName.GIVEN_NAME, givenNames);
-            values.put(StructuredName.MIDDLE_NAME, middleName);
-            values.put(StructuredName.FAMILY_NAME, familyName);
-            values.put(StructuredName.SUFFIX, suffix);
+            putValueIfPresent(values, StructuredName.PREFIX, prefix);
+            putValueIfPresent(values, StructuredName.GIVEN_NAME, givenNames);
+            putValueIfPresent(values, StructuredName.MIDDLE_NAME, middleName);
+            putValueIfPresent(values, StructuredName.FAMILY_NAME, familyName);
+            putValueIfPresent(values, StructuredName.SUFFIX, suffix);
+            values.put(StructuredName.FULL_NAME_STYLE, fullNameStyle);
+            putValueIfPresent(values, StructuredName.PHONETIC_FAMILY_NAME, phoneticFamilyName);
+            putValueIfPresent(values, StructuredName.PHONETIC_MIDDLE_NAME, phoneticMiddleName);
+            putValueIfPresent(values, StructuredName.PHONETIC_GIVEN_NAME, phoneticGivenName);
+            values.put(StructuredName.PHONETIC_NAME_STYLE, phoneticNameStyle);
         }
+
+        private void putValueIfPresent(ContentValues values, String name, String value) {
+            if (value != null) {
+                values.put(name, value);
+            }
+        }
+
+        public void clear() {
+            prefix = null;
+            givenNames = null;
+            middleName = null;
+            familyName = null;
+            suffix = null;
+            fullNameStyle = FullNameStyle.UNDEFINED;
+            phoneticFamilyName = null;
+            phoneticMiddleName = null;
+            phoneticGivenName = null;
+            phoneticNameStyle = PhoneticNameStyle.UNDEFINED;
+        }
+
+        public boolean isEmpty() {
+            return TextUtils.isEmpty(givenNames)
+                    && TextUtils.isEmpty(middleName)
+                    && TextUtils.isEmpty(familyName)
+                    && TextUtils.isEmpty(suffix)
+                    && TextUtils.isEmpty(phoneticFamilyName)
+                    && TextUtils.isEmpty(phoneticMiddleName)
+                    && TextUtils.isEmpty(phoneticGivenName);
+        }
+
+        @Override
+        public String toString() {
+            return "[given: " + givenNames + " middle: " + middleName + " family: " + familyName
+                    + " ph/given: " + phoneticGivenName + " ph/middle: " + phoneticMiddleName
+                    + " ph/family: " + phoneticFamilyName + "]";
+        }
+
     }
 
     private static class NameTokenizer extends StringTokenizer {
         private final String[] mTokens;
         private int mDotBitmask;
+        private int mCommaBitmask;
         private int mStartPointer;
         private int mEndPointer;
 
@@ -122,13 +197,15 @@
                 final String token = nextToken();
                 if (token.length() > 0) {
                     final char c = token.charAt(0);
-                    if (c == ' ' || c == ',') {
+                    if (c == ' ') {
                         continue;
                     }
                 }
 
                 if (mEndPointer > 0 && token.charAt(0) == '.') {
                     mDotBitmask |= (1 << (mEndPointer - 1));
+                } else if (mEndPointer > 0 && token.charAt(0) == ',') {
+                    mCommaBitmask |= (1 << (mEndPointer - 1));
                 } else {
                     mTokens[mEndPointer] = token;
                     mEndPointer++;
@@ -142,6 +219,13 @@
         public boolean hasDot(int index) {
             return (mDotBitmask & (1 << index)) != 0;
         }
+
+        /**
+         * Returns true if the token is followed by a comma in the original full name.
+         */
+        public boolean hasComma(int index) {
+            return (mCommaBitmask & (1 << index)) != 0;
+        }
     }
 
     /**
@@ -150,7 +234,7 @@
      * @param commonPrefixes comma-separated list of common prefixes,
      *            e.g. "Mr, Ms, Mrs"
      * @param commonLastNamePrefixes comma-separated list of common last name prefixes,
-     *           e.g. "d', st, st., von"
+     *            e.g. "d', st, st., von"
      * @param commonSuffixes comma-separated list of common suffixes,
      *            e.g. "Jr, M.D., MD, D.D.S."
      * @param commonConjunctions comma-separated list of common conjuctions,
@@ -163,7 +247,8 @@
         mLastNamePrefixesSet = convertToSet(commonLastNamePrefixes);
         mSuffixesSet = convertToSet(commonSuffixes);
         mConjuctions = convertToSet(commonConjunctions);
-        mLocale = locale;
+        mLocale = locale != null ? locale : Locale.getDefault();
+        mLanguage = mLocale.getLanguage().toLowerCase();
 
         int maxLength = 0;
         for (String suffix : mSuffixesSet) {
@@ -225,6 +310,36 @@
             return;
         }
 
+        int fullNameStyle = guessFullNameStyle(fullName);
+        if (fullNameStyle == FullNameStyle.CJK) {
+            fullNameStyle = getAdjustedFullNameStyle(fullNameStyle);
+        }
+
+        name.fullNameStyle = fullNameStyle;
+
+        switch (fullNameStyle) {
+            case FullNameStyle.CHINESE:
+                splitChineseName(name, fullName);
+                break;
+
+            case FullNameStyle.JAPANESE:
+            case FullNameStyle.KOREAN:
+                splitJapaneseOrKoreanName(name, fullName);
+                break;
+
+            default:
+                splitWesternName(name, fullName);
+        }
+    }
+
+    /**
+     * Splits a full name composed according to the Western tradition:
+     * <pre>
+     *   [prefix] given name(s) [[middle name] family name] [, suffix]
+     *   [prefix] family name, given name [middle name] [,suffix]
+     * </pre>
+     */
+    private void splitWesternName(Name name, String fullName) {
         NameTokenizer tokens = new NameTokenizer(fullName);
         parsePrefix(name, tokens);
 
@@ -244,6 +359,70 @@
     }
 
     /**
+     * Splits a full name composed according to the Chinese tradition:
+     * <pre>
+     *   [family name [middle name]] given name
+     * </pre>
+     */
+    private void splitChineseName(Name name, String fullName) {
+        StringTokenizer tokenizer = new StringTokenizer(fullName);
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            if (name.givenNames == null) {
+                name.givenNames = token;
+            } else if (name.familyName == null) {
+                name.familyName = name.givenNames;
+                name.givenNames = token;
+            } else if (name.middleName == null) {
+                name.middleName = name.givenNames;
+                name.givenNames = token;
+            } else {
+                name.middleName = name.middleName + name.givenNames;
+                name.givenNames = token;
+            }
+        }
+
+        // If a single word parse that word up.
+        if (name.givenNames != null && name.familyName == null && name.middleName == null) {
+            int length = fullName.length();
+            if (length == 2) {
+                name.familyName = fullName.substring(0, 1);
+                name.givenNames = fullName.substring(1);
+            } else if (length == 3) {
+                name.familyName = fullName.substring(0, 1);
+                name.middleName = fullName.substring(1, 2);
+                name.givenNames = fullName.substring(2);
+            } else if (length == 4) {
+                name.familyName = fullName.substring(0, 2);
+                name.middleName = fullName.substring(2, 3);
+                name.givenNames = fullName.substring(3);
+            }
+
+        }
+    }
+
+    /**
+     * Splits a full name composed according to the Japanese tradition:
+     * <pre>
+     *   [family name] given name(s)
+     * </pre>
+     */
+    private void splitJapaneseOrKoreanName(Name name, String fullName) {
+        StringTokenizer tokenizer = new StringTokenizer(fullName);
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            if (name.givenNames == null) {
+                name.givenNames = token;
+            } else if (name.familyName == null) {
+                name.familyName = name.givenNames;
+                name.givenNames = token;
+            } else {
+                name.givenNames += " " + token;
+            }
+        }
+    }
+
+    /**
      * Flattens the given {@link Name} into a single field, usually for storage
      * in {@link StructuredName#DISPLAY_NAME}.
      */
@@ -264,6 +443,200 @@
     }
 
     /**
+     * Concatenates components of a name according to the rules dictated by the name style.
+     *
+     * @param givenNameFirst is ignored for CJK display name styles
+     */
+    public String join(Name name, boolean givenNameFirst) {
+        switch (name.fullNameStyle) {
+            case FullNameStyle.CHINESE:
+            case FullNameStyle.KOREAN:
+                return join(name.familyName, name.middleName, name.givenNames, name.suffix,
+                        false, false, false);
+
+            case FullNameStyle.JAPANESE:
+                return join(name.familyName, name.middleName, name.givenNames, name.suffix,
+                        true, false, false);
+
+            default:
+                if (givenNameFirst) {
+                    return join(name.givenNames, name.middleName, name.familyName, name.suffix,
+                            true, false, true);
+                } else {
+                    return join(name.familyName, name.givenNames, name.middleName, name.suffix,
+                            true, true, true);
+                }
+        }
+    }
+
+    /**
+     * Concatenates components of the phonetic name following the CJK tradition:
+     * family name + middle name + given name(s).
+     */
+    public String joinPhoneticName(Name name) {
+        return join(name.phoneticFamilyName, name.phoneticMiddleName,
+                name.phoneticGivenName, null, true, false, false);
+    }
+
+    /**
+     * Given a name in Chinese, returns a Pinyin representation.
+     */
+    public String convertHanziToPinyin(String name) {
+
+        // TODO: move this code to HanziToPinyin and optimize
+        ArrayList<Token> tokens = HanziToPinyin.getInstance().get(name);
+        if (tokens != null) {
+            int size = tokens.size();
+            if (size != 0) {
+                StringBuilder sb = new StringBuilder();
+                for (int i = 0; i < size; i++) {
+                    String pinyin = tokens.get(i).target;
+                    if (!TextUtils.isEmpty(pinyin)) {
+                        if (sb.length() != 0) {
+                            sb.append(' ');
+                        }
+                        sb.append(pinyin);
+                    }
+                }
+                return sb.toString();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Concatenates parts of a full name inserting spaces and commas as specified.
+     */
+    private String join(String part1, String part2, String part3, String suffix,
+            boolean useSpace, boolean useCommaAfterPart1, boolean useCommaAfterPart3) {
+        boolean hasPart1 = !TextUtils.isEmpty(part1);
+        boolean hasPart2 = !TextUtils.isEmpty(part2);
+        boolean hasPart3 = !TextUtils.isEmpty(part3);
+        boolean hasSuffix = !TextUtils.isEmpty(suffix);
+
+        boolean isSingleWord = true;
+        String singleWord = null;
+        if (hasPart1) {
+            singleWord = part1;
+        }
+
+        if (hasPart2) {
+            if (singleWord != null) {
+                isSingleWord = false;
+            } else {
+                singleWord = part2;
+            }
+        }
+
+        if (hasPart3) {
+            if (singleWord != null) {
+                isSingleWord = false;
+            } else {
+                singleWord = part3;
+            }
+        }
+
+        if (hasSuffix) {
+            if (singleWord != null) {
+                isSingleWord = false;
+            } else {
+                singleWord = normalizedSuffix(suffix);
+            }
+        }
+
+        if (isSingleWord) {
+            return singleWord;
+        }
+
+        StringBuilder sb = new StringBuilder();
+        if (hasPart1) {
+            sb.append(part1);
+        }
+
+        if (hasPart2) {
+            if (hasPart1) {
+                if (useCommaAfterPart1) {
+                    sb.append(',');
+                }
+                if (useSpace) {
+                    sb.append(' ');
+                }
+            }
+            sb.append(part2);
+        }
+
+        if (hasPart3) {
+            if (hasPart1 || hasPart2) {
+                if (useSpace) {
+                    sb.append(' ');
+                }
+            }
+            sb.append(part3);
+        }
+
+        if (hasSuffix) {
+            if (hasPart1 || hasPart2 || hasPart3) {
+                if (useCommaAfterPart3) {
+                    sb.append(',');
+                }
+                if (useSpace) {
+                    sb.append(' ');
+                }
+            }
+            sb.append(normalizedSuffix(suffix));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Puts a dot after the supplied suffix if that is the accepted form of the suffix,
+     * e.g. "Jr." and "Sr.", but not "I", "II" and "III".
+     */
+    private String normalizedSuffix(String suffix) {
+        int length = suffix.length();
+        if (length == 0 || suffix.charAt(length - 1) == '.') {
+            return suffix;
+        }
+
+        String withDot = suffix + '.';
+        if (mSuffixesSet.contains(withDot.toUpperCase())) {
+            return withDot;
+        } else {
+            return suffix;
+        }
+    }
+
+    /**
+     * If the supplied name style is undefined, returns a default based on the language,
+     * otherwise returns the supplied name style itself.
+     *
+     * @param nameStyle See {@link FullNameStyle}.
+     */
+    public int getAdjustedFullNameStyle(int nameStyle) {
+        if (nameStyle == FullNameStyle.UNDEFINED) {
+            if (JAPANESE_LANGUAGE.equals(mLanguage)) {
+                return FullNameStyle.JAPANESE;
+            } else if (KOREAN_LANGUAGE.equals(mLanguage)) {
+                return FullNameStyle.KOREAN;
+            } else if (CHINESE_LANGUAGE.equals(mLanguage)) {
+                return FullNameStyle.CHINESE;
+            } else {
+                return FullNameStyle.WESTERN;
+            }
+        } else if (nameStyle == FullNameStyle.CJK) {
+            if (JAPANESE_LANGUAGE.equals(mLanguage)) {
+                return FullNameStyle.JAPANESE;
+            } else if (KOREAN_LANGUAGE.equals(mLanguage)) {
+                return FullNameStyle.KOREAN;
+            } else {
+                return FullNameStyle.CHINESE;
+            }
+        }
+        return nameStyle;
+    }
+
+    /**
      * Parses the first word from the name if it is a prefix.
      */
     private void parsePrefix(Name name, NameTokenizer tokens) {
@@ -333,15 +706,36 @@
             return;
         }
 
+        // If the first word is followed by a comma, assume that it's the family name
+        if (tokens.hasComma(tokens.mStartPointer)) {
+           name.familyName = tokens.mTokens[tokens.mStartPointer];
+           tokens.mStartPointer++;
+           return;
+        }
+
+        // If the second word is followed by a comma and the first word
+        // is a last name prefix as in "de Sade" and "von Cliburn", treat
+        // the first two words as the family name.
+        if (tokens.mStartPointer + 1 < tokens.mEndPointer
+                && tokens.hasComma(tokens.mStartPointer + 1)
+                && isFamilyNamePrefix(tokens.mTokens[tokens.mStartPointer])) {
+            String familyNamePrefix = tokens.mTokens[tokens.mStartPointer];
+            if (tokens.hasDot(tokens.mStartPointer)) {
+                familyNamePrefix += '.';
+            }
+            name.familyName = familyNamePrefix + " " + tokens.mTokens[tokens.mStartPointer + 1];
+            tokens.mStartPointer += 2;
+            return;
+        }
+
+        // Finally, assume that the last word is the last name
         name.familyName = tokens.mTokens[tokens.mEndPointer - 1];
         tokens.mEndPointer--;
 
-        // Take care of last names like "D'Onofrio" and "von Cliburn"
+        // Take care of last names like "de Sade" and "von Cliburn"
         if ((tokens.mEndPointer - tokens.mStartPointer) > 0) {
             String lastNamePrefix = tokens.mTokens[tokens.mEndPointer - 1];
-            final String normalized = lastNamePrefix.toUpperCase();
-            if (mLastNamePrefixesSet.contains(normalized)
-                    || mLastNamePrefixesSet.contains(normalized + ".")) {
+            if (isFamilyNamePrefix(lastNamePrefix)) {
                 if (tokens.hasDot(tokens.mEndPointer - 1)) {
                     lastNamePrefix += '.';
                 }
@@ -351,6 +745,16 @@
         }
     }
 
+    /**
+     * Returns true if the supplied word is an accepted last name prefix, e.g. "von", "de"
+     */
+    private boolean isFamilyNamePrefix(String word) {
+        final String normalized = word.toUpperCase();
+
+        return mLastNamePrefixesSet.contains(normalized)
+                || mLastNamePrefixesSet.contains(normalized + ".");
+    }
+
 
     private void parseMiddleName(Name name, NameTokenizer tokens) {
         if (tokens.mStartPointer == tokens.mEndPointer) {
@@ -362,6 +766,9 @@
                     || !mConjuctions.contains(tokens.mTokens[tokens.mEndPointer - 2].
                             toUpperCase())) {
                 name.middleName = tokens.mTokens[tokens.mEndPointer - 1];
+                if (tokens.hasDot(tokens.mEndPointer - 1)) {
+                    name.middleName += '.';
+                }
                 tokens.mEndPointer--;
             }
         }
@@ -388,4 +795,226 @@
             name.givenNames = sb.toString();
         }
     }
+
+    /**
+     * Makes the best guess at the expected full name style based on the character set
+     * used in the supplied name.  If the phonetic name is also supplied, tries to
+     * differentiate between Chinese, Japanese and Korean based on the alphabet used
+     * for the phonetic name.
+     */
+    public void guessNameStyle(Name name) {
+        guessFullNameStyle(name);
+        if (FullNameStyle.CJK == name.fullNameStyle) {
+            name.fullNameStyle = getAdjustedFullNameStyle(name.fullNameStyle);
+        }
+        guessPhoneticNameStyle(name);
+        name.fullNameStyle = getAdjustedNameStyleBasedOnPhoneticNameStyle(name.fullNameStyle,
+                name.phoneticNameStyle);
+    }
+
+    /**
+     * Updates the display name style according to the phonetic name style if we
+     * were unsure about display name style based on the name components, but
+     * phonetic name makes it more definitive.
+     */
+    public int getAdjustedNameStyleBasedOnPhoneticNameStyle(int nameStyle, int phoneticNameStyle) {
+        if (phoneticNameStyle != PhoneticNameStyle.UNDEFINED) {
+            if (nameStyle == FullNameStyle.UNDEFINED || nameStyle == FullNameStyle.CJK) {
+                if (phoneticNameStyle == PhoneticNameStyle.JAPANESE) {
+                    return FullNameStyle.JAPANESE;
+                } else if (phoneticNameStyle == PhoneticNameStyle.KOREAN) {
+                    return FullNameStyle.KOREAN;
+                }
+                if (nameStyle == FullNameStyle.CJK && phoneticNameStyle == PhoneticNameStyle.PINYIN) {
+                    return FullNameStyle.CHINESE;
+                }
+            }
+        }
+        return nameStyle;
+    }
+
+    /**
+     * Makes the best guess at the expected full name style based on the character set
+     * used in the supplied name.
+     */
+    private void guessFullNameStyle(NameSplitter.Name name) {
+        if (name.fullNameStyle != FullNameStyle.UNDEFINED) {
+            return;
+        }
+
+        int bestGuess = guessFullNameStyle(name.givenNames);
+        // A mix of Hanzi and latin chars are common in China, so we have to go through all names
+        // if the name is not JANPANESE or KOREAN.
+        if (bestGuess != FullNameStyle.UNDEFINED && bestGuess != FullNameStyle.CJK
+                && bestGuess != FullNameStyle.WESTERN) {
+            name.fullNameStyle = bestGuess;
+            return;
+        }
+
+        int guess = guessFullNameStyle(name.familyName);
+        if (guess != FullNameStyle.UNDEFINED) {
+            if (guess != FullNameStyle.CJK && guess != FullNameStyle.WESTERN) {
+                name.fullNameStyle = guess;
+                return;
+            }
+            bestGuess = guess;
+        }
+
+        guess = guessFullNameStyle(name.middleName);
+        if (guess != FullNameStyle.UNDEFINED) {
+            if (guess != FullNameStyle.CJK && guess != FullNameStyle.WESTERN) {
+                name.fullNameStyle = guess;
+                return;
+            }
+            bestGuess = guess;
+        }
+
+        name.fullNameStyle = bestGuess;
+    }
+
+    public int guessFullNameStyle(String name) {
+        if (name == null) {
+            return FullNameStyle.UNDEFINED;
+        }
+
+        int nameStyle = FullNameStyle.UNDEFINED;
+        int length = name.length();
+        int offset = 0;
+        while (offset < length) {
+            int codePoint = Character.codePointAt(name, offset);
+            if (Character.isLetter(codePoint)) {
+                UnicodeBlock unicodeBlock = UnicodeBlock.of(codePoint);
+
+                if (!isLatinUnicodeBlock(unicodeBlock)) {
+
+                    if (isCJKUnicodeBlock(unicodeBlock)) {
+                        // We don't know if this is Chinese, Japanese or Korean -
+                        // trying to figure out by looking at other characters in the name
+                        return guessCJKNameStyle(name, offset + Character.charCount(codePoint));
+                    }
+
+                    if (isJapanesePhoneticUnicodeBlock(unicodeBlock)) {
+                        return FullNameStyle.JAPANESE;
+                    }
+
+                    if (isKoreanUnicodeBlock(unicodeBlock)) {
+                        return FullNameStyle.KOREAN;
+                    }
+                }
+                nameStyle = FullNameStyle.WESTERN;
+            }
+            offset += Character.charCount(codePoint);
+        }
+        return nameStyle;
+    }
+
+    private int guessCJKNameStyle(String name, int offset) {
+        int length = name.length();
+        while (offset < length) {
+            int codePoint = Character.codePointAt(name, offset);
+            if (Character.isLetter(codePoint)) {
+                UnicodeBlock unicodeBlock = UnicodeBlock.of(codePoint);
+                if (isJapanesePhoneticUnicodeBlock(unicodeBlock)) {
+                    return FullNameStyle.JAPANESE;
+                }
+                if (isKoreanUnicodeBlock(unicodeBlock)) {
+                    return FullNameStyle.KOREAN;
+                }
+            }
+            offset += Character.charCount(codePoint);
+        }
+
+        return FullNameStyle.CJK;
+    }
+
+    private void guessPhoneticNameStyle(NameSplitter.Name name) {
+        if (name.phoneticNameStyle != PhoneticNameStyle.UNDEFINED) {
+            return;
+        }
+
+        int bestGuess = guessPhoneticNameStyle(name.phoneticFamilyName);
+        if (bestGuess != FullNameStyle.UNDEFINED && bestGuess != FullNameStyle.CJK) {
+            name.phoneticNameStyle = bestGuess;
+            return;
+        }
+
+        int guess = guessPhoneticNameStyle(name.phoneticGivenName);
+        if (guess != FullNameStyle.UNDEFINED) {
+            if (guess != FullNameStyle.CJK) {
+                name.phoneticNameStyle = guess;
+                return;
+            }
+            bestGuess = guess;
+        }
+
+        guess = guessPhoneticNameStyle(name.phoneticMiddleName);
+        if (guess != FullNameStyle.UNDEFINED) {
+            if (guess != FullNameStyle.CJK) {
+                name.phoneticNameStyle = guess;
+                return;
+            }
+            bestGuess = guess;
+        }
+    }
+
+    public int guessPhoneticNameStyle(String name) {
+        if (name == null) {
+            return PhoneticNameStyle.UNDEFINED;
+        }
+
+        int nameStyle = PhoneticNameStyle.UNDEFINED;
+        int length = name.length();
+        int offset = 0;
+        while (offset < length) {
+            int codePoint = Character.codePointAt(name, offset);
+            if (Character.isLetter(codePoint)) {
+                UnicodeBlock unicodeBlock = UnicodeBlock.of(codePoint);
+                if (isJapanesePhoneticUnicodeBlock(unicodeBlock)) {
+                    return PhoneticNameStyle.JAPANESE;
+                }
+                if (isKoreanUnicodeBlock(unicodeBlock)) {
+                    return PhoneticNameStyle.KOREAN;
+                }
+                if (isLatinUnicodeBlock(unicodeBlock)) {
+                    return PhoneticNameStyle.PINYIN;
+                }
+            }
+            offset += Character.charCount(codePoint);
+        }
+
+        return nameStyle;
+    }
+
+    private static boolean isLatinUnicodeBlock(UnicodeBlock unicodeBlock) {
+        return unicodeBlock == UnicodeBlock.BASIC_LATIN ||
+                unicodeBlock == UnicodeBlock.LATIN_1_SUPPLEMENT ||
+                unicodeBlock == UnicodeBlock.LATIN_EXTENDED_A ||
+                unicodeBlock == UnicodeBlock.LATIN_EXTENDED_B ||
+                unicodeBlock == UnicodeBlock.LATIN_EXTENDED_ADDITIONAL;
+    }
+
+    private static boolean isCJKUnicodeBlock(UnicodeBlock block) {
+        return block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
+                || block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
+                || block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
+                || block == UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
+                || block == UnicodeBlock.CJK_RADICALS_SUPPLEMENT
+                || block == UnicodeBlock.CJK_COMPATIBILITY
+                || block == UnicodeBlock.CJK_COMPATIBILITY_FORMS
+                || block == UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
+                || block == UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT;
+    }
+
+    private static boolean isKoreanUnicodeBlock(UnicodeBlock unicodeBlock) {
+        return unicodeBlock == UnicodeBlock.HANGUL_SYLLABLES ||
+                unicodeBlock == UnicodeBlock.HANGUL_JAMO ||
+                unicodeBlock == UnicodeBlock.HANGUL_COMPATIBILITY_JAMO;
+    }
+
+    private static boolean isJapanesePhoneticUnicodeBlock(UnicodeBlock unicodeBlock) {
+        return unicodeBlock == UnicodeBlock.KATAKANA ||
+                unicodeBlock == UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS ||
+                unicodeBlock == UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS ||
+                unicodeBlock == UnicodeBlock.HIRAGANA;
+    }
 }
diff --git a/src/com/android/providers/contacts/PostalSplitter.java b/src/com/android/providers/contacts/PostalSplitter.java
index 7729d71..812c34d 100644
--- a/src/com/android/providers/contacts/PostalSplitter.java
+++ b/src/com/android/providers/contacts/PostalSplitter.java
@@ -25,6 +25,8 @@
  * Split and join {@link StructuredPostal} fields.
  */
 public class PostalSplitter {
+    private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase();
+
     private final Locale mLocale;
 
     public static class Postal {
@@ -80,11 +82,87 @@
      * storage in {@link StructuredPostal#FORMATTED_ADDRESS}.
      */
     public String join(Postal postal) {
+        final String[] values = new String[] {
+                postal.street, postal.pobox, postal.neighborhood, postal.city,
+                postal.region, postal.postcode, postal.country
+        };
         // TODO: split off to handle various locales
-        return joinEnUs(postal);
+        if (mLocale != null &&
+                JAPANESE_LANGUAGE.equals(mLocale.getLanguage()) &&
+                !arePrintableAsciiOnly(values)) {
+            return joinJaJp(postal);
+        } else {
+            return joinEnUs(postal);
+        }
     }
 
-    private String joinEnUs(Postal postal) {
+    private String joinJaJp(final Postal postal) {
+        final boolean hasStreet = !TextUtils.isEmpty(postal.street);
+        final boolean hasPobox = !TextUtils.isEmpty(postal.pobox);
+        final boolean hasNeighborhood = !TextUtils.isEmpty(postal.neighborhood);
+        final boolean hasCity = !TextUtils.isEmpty(postal.city);
+        final boolean hasRegion = !TextUtils.isEmpty(postal.region);
+        final boolean hasPostcode = !TextUtils.isEmpty(postal.postcode);
+        final boolean hasCountry = !TextUtils.isEmpty(postal.country);
+
+        // First block: [country][ ][postcode]
+        // Second block: [region][ ][city][ ][neighborhood]
+        // Third block: [street][ ][pobox]
+
+        final StringBuilder builder = new StringBuilder();
+
+        final boolean hasFirstBlock = hasCountry || hasPostcode;
+        final boolean hasSecondBlock = hasRegion || hasCity || hasNeighborhood;
+        final boolean hasThirdBlock = hasStreet || hasPobox;
+
+        if (hasFirstBlock) {
+            if (hasCountry) {
+                builder.append(postal.country);
+            }
+            if (hasPostcode) {
+                if (hasCountry) builder.append(SPACE);
+                builder.append(postal.postcode);
+            }
+        }
+
+        if (hasSecondBlock) {
+            if (hasFirstBlock) {
+                builder.append(NEWLINE);
+            }
+            if (hasRegion) {
+                builder.append(postal.region);
+            }
+            if (hasCity) {
+                if (hasRegion) builder.append(SPACE);
+                builder.append(postal.city);
+            }
+            if (hasNeighborhood) {
+                if (hasRegion || hasCity) builder.append(SPACE);
+                builder.append(postal.neighborhood);
+            }
+        }
+
+        if (hasThirdBlock) {
+            if (hasFirstBlock || hasSecondBlock) {
+                builder.append(NEWLINE);
+            }
+            if (hasStreet) {
+                builder.append(postal.street);
+            }
+            if (hasPobox) {
+                if (hasStreet) builder.append(SPACE);
+                builder.append(postal.pobox);
+            }
+        }
+
+        if (builder.length() > 0) {
+            return builder.toString();
+        } else {
+            return null;
+        }
+    }
+
+    private String joinEnUs(final Postal postal) {
         final boolean hasStreet = !TextUtils.isEmpty(postal.street);
         final boolean hasPobox = !TextUtils.isEmpty(postal.pobox);
         final boolean hasNeighborhood = !TextUtils.isEmpty(postal.neighborhood);
@@ -149,4 +227,19 @@
             return null;
         }
     }
+
+    private static boolean arePrintableAsciiOnly(final String[] values) {
+        if (values == null) {
+            return true;
+        }
+        for (final String value : values) {
+            if (TextUtils.isEmpty(value)) {
+                continue;
+            }
+            if (!TextUtils.isPrintableAsciiOnly(value)) {
+                return false;
+            }
+        }
+        return true;
+    }
 }
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..622ace2 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -130,15 +130,34 @@
     }
 
     protected long createRawContactWithName() {
-        return createRawContactWithName("John", "Doe");
+        return createRawContactWithName(null);
+    }
+
+    protected long createRawContactWithName(Account account) {
+        return createRawContactWithName("John", "Doe", account);
     }
 
     protected long createRawContactWithName(String firstName, String lastName) {
-        long rawContactId = createRawContact(null);
+        return createRawContactWithName(firstName, lastName, null);
+    }
+
+    protected long createRawContactWithName(String firstName, String lastName, Account account) {
+        long rawContactId = createRawContact(account);
         insertStructuredName(rawContactId, firstName, lastName);
         return rawContactId;
     }
 
+    protected Uri setCallerIsSyncAdapter(Uri uri, Account account) {
+        if (account == null) {
+            return uri;
+        }
+        final Uri.Builder builder = uri.buildUpon();
+        builder.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name);
+        builder.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type);
+        builder.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true");
+        return builder.build();
+    }
+
     protected long createRawContact(Account account, String... extras) {
         ContentValues values = new ContentValues();
         for (int i = 0; i < extras.length; ) {
@@ -390,8 +409,15 @@
         return displayName;
     }
 
+    private String queryLookupKey(long contactId) {
+        Cursor c = queryContact(contactId);
+        assertTrue(c.moveToFirst());
+        String lookupKey = c.getString(c.getColumnIndex(Contacts.LOOKUP_KEY));
+        c.close();
+        return lookupKey;
+    }
+
     protected void assertAggregated(long rawContactId1, long rawContactId2) {
-        forceAggregation();
         long contactId1 = queryContactId(rawContactId1);
         long contactId2 = queryContactId(rawContactId2);
         assertTrue(contactId1 == contactId2);
@@ -399,8 +425,6 @@
 
     protected void assertAggregated(long rawContactId1, long rawContactId2,
             String expectedDisplayName) {
-        forceAggregation();
-
         long contactId1 = queryContactId(rawContactId1);
         long contactId2 = queryContactId(rawContactId2);
         assertTrue(contactId1 == contactId2);
@@ -410,8 +434,6 @@
     }
 
     protected void assertNotAggregated(long rawContactId1, long rawContactId2) {
-        forceAggregation();
-
         long contactId1 = queryContactId(rawContactId1);
         long contactId2 = queryContactId(rawContactId2);
         assertTrue(contactId1 != contactId2);
@@ -588,6 +610,10 @@
         mResolver.update(ContentUris.withAppendedId(contentUri, id), values, null, null);
     }
 
+    protected void assertStoredValue(Uri contentUri, long id, String column, Object expectedValue) {
+        assertStoredValue(ContentUris.withAppendedId(contentUri, id), column, expectedValue);
+    }
+
     protected void assertStoredValue(Uri rowUri, String column, Object expectedValue) {
         String value = getStoredValue(rowUri, column);
         if (expectedValue == null) {
@@ -816,11 +842,314 @@
         c.close();
     }
 
-    protected void forceAggregation() {
-        ((SynchronousContactsProvider2) mActor.provider).aggregate();
-    }
-
     protected void assertNetworkNotified(boolean expected) {
         assertEquals(expected, ((SynchronousContactsProvider2)mActor.provider).isNetworkNotified());
     }
+
+    /**
+     * A contact in the database, and the attributes used to create it.  Construct using
+     * {@link GoldenContactBuilder#build()}.
+     */
+    public final class GoldenContact {
+
+        private final long rawContactId;
+
+        private final long contactId;
+
+        private final String givenName;
+
+        private final String familyName;
+
+        private final String nickname;
+
+        private final byte[] photo;
+
+        private final String company;
+
+        private final String title;
+
+        private final String phone;
+
+        private final String email;
+
+        private GoldenContact(GoldenContactBuilder builder, long rawContactId, long contactId) {
+
+            this.rawContactId = rawContactId;
+            this.contactId = contactId;
+            givenName = builder.givenName;
+            familyName = builder.familyName;
+            nickname = builder.nickname;
+            photo = builder.photo;
+            company = builder.company;
+            title = builder.title;
+            phone = builder.phone;
+            email = builder.email;
+        }
+
+        public void delete() {
+            Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+            mResolver.delete(rawContactUri, null, null);
+        }
+
+        /**
+         * Returns the index of the contact in table "raw_contacts"
+         */
+        public long getRawContactId() {
+            return rawContactId;
+        }
+
+        /**
+         * Returns the index of the contact in table "contacts"
+         */
+        public long getContactId() {
+            return contactId;
+        }
+
+        /**
+         * Returns the lookup key for the contact.
+         */
+        public String getLookupKey() {
+            return queryLookupKey(contactId);
+        }
+
+        /**
+         * Returns the contact's given name.
+         */
+        public String getGivenName() {
+            return givenName;
+        }
+
+        /**
+         * Returns the contact's family name.
+         */
+        public String getFamilyName() {
+            return familyName;
+        }
+
+        /**
+         * Returns the contact's nickname.
+         */
+        public String getNickname() {
+            return nickname;
+        }
+
+        /**
+         * Return's the contact's photo
+         */
+        public byte[] getPhoto() {
+            return photo;
+        }
+
+        /**
+         * Return's the company at which the contact works.
+         */
+        public String getCompany() {
+            return company;
+        }
+
+        /**
+         * Returns the contact's job title.
+         */
+        public String getTitle() {
+            return title;
+        }
+
+        /**
+         * Returns the contact's phone number
+         */
+        public String getPhone() {
+            return phone;
+        }
+
+        /**
+         * Returns the contact's email address
+         */
+        public String getEmail() {
+            return email;
+        }
+     }
+
+    /**
+     * Builds {@link GoldenContact} objects.  Unspecified boolean objects default to false.
+     * Unspecified String objects default to null.
+     */
+    public final class GoldenContactBuilder {
+
+        private String givenName;
+
+        private String familyName;
+
+        private String nickname;
+
+        private byte[] photo;
+
+        private String company;
+
+        private String title;
+
+        private String phone;
+
+        private String email;
+
+        /**
+         * The contact's given and family names.
+         *
+         * TODO(dplotnikov): inline, or should we require them to set both names if they set either?
+         */
+        public GoldenContactBuilder name(String givenName, String familyName) {
+            return givenName(givenName).familyName(familyName);
+        }
+
+        /**
+         * The contact's given name.
+         */
+        public GoldenContactBuilder givenName(String value) {
+            givenName = value;
+            return this;
+        }
+
+        /**
+         * The contact's family name.
+         */
+        public GoldenContactBuilder familyName(String value) {
+            familyName = value;
+            return this;
+        }
+
+        /**
+         * The contact's nickname.
+         */
+        public GoldenContactBuilder nickname(String value) {
+            nickname = value;
+            return this;
+        }
+
+        /**
+         * The contact's photo.
+         */
+        public GoldenContactBuilder photo(byte[] value) {
+            photo = value;
+            return this;
+        }
+
+        /**
+         * The company at which the contact works.
+         */
+        public GoldenContactBuilder company(String value) {
+            company = value;
+            return this;
+        }
+
+        /**
+         * The contact's job title.
+         */
+        public GoldenContactBuilder title(String value) {
+            title = value;
+            return this;
+        }
+
+        /**
+         * The contact's phone number.
+         */
+        public GoldenContactBuilder phone(String value) {
+            phone = value;
+            return this;
+        }
+
+        /**
+         * The contact's email address; also sets their IM status to {@link StatusUpdates#OFFLINE}
+         * with a presence of "Coding for Android".
+         */
+        public GoldenContactBuilder email(String value) {
+            email = value;
+            return this;
+        }
+
+        /**
+         * Builds the {@link GoldenContact} specified by this builder.
+         */
+        public GoldenContact build() {
+
+            final long groupId = createGroup(mAccount, "gsid1", "title1");
+
+            long rawContactId = createRawContact();
+            insertGroupMembership(rawContactId, groupId);
+
+            if (givenName != null || familyName != null) {
+                insertStructuredName(rawContactId, givenName, familyName);
+            }
+            if (nickname != null) {
+                insertNickname(rawContactId, nickname);
+            }
+            if (photo != null) {
+                insertPhoto(rawContactId);
+            }
+            if (company != null || title != null) {
+                insertOrganization(rawContactId);
+            }
+            if (email != null) {
+                insertEmail(rawContactId);
+            }
+            if (phone != null) {
+                insertPhone(rawContactId);
+            }
+
+            long contactId = queryContactId(rawContactId);
+
+            return new GoldenContact(this, rawContactId, contactId);
+        }
+
+        private void insertPhoto(long rawContactId) {
+            ContentValues values = new ContentValues();
+            values.put(Data.RAW_CONTACT_ID, rawContactId);
+            values.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+            values.put(Photo.PHOTO, photo);
+            mResolver.insert(Data.CONTENT_URI, values);
+        }
+
+        private void insertOrganization(long rawContactId) {
+
+            ContentValues values = new ContentValues();
+            values.put(Data.RAW_CONTACT_ID, rawContactId);
+            values.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+            values.put(Organization.TYPE, Organization.TYPE_WORK);
+            if (company != null) {
+                values.put(Organization.COMPANY, company);
+            }
+            if (title != null) {
+                values.put(Organization.TITLE, title);
+            }
+            mResolver.insert(Data.CONTENT_URI, values);
+        }
+
+        private void insertEmail(long rawContactId) {
+
+            ContentValues values = new ContentValues();
+            values.put(Data.RAW_CONTACT_ID, rawContactId);
+            values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+            values.put(Email.TYPE, Email.TYPE_WORK);
+            values.put(Email.DATA, "foo@acme.com");
+            mResolver.insert(Data.CONTENT_URI, values);
+
+            int protocol = Im.PROTOCOL_GOOGLE_TALK;
+
+            values.clear();
+            values.put(StatusUpdates.PROTOCOL, protocol);
+            values.put(StatusUpdates.IM_HANDLE, email);
+            values.put(StatusUpdates.IM_ACCOUNT, "foo");
+            values.put(StatusUpdates.PRESENCE_STATUS, StatusUpdates.OFFLINE);
+            values.put(StatusUpdates.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+            mResolver.insert(StatusUpdates.CONTENT_URI, values);
+        }
+
+        private void insertPhone(long rawContactId) {
+            ContentValues values = new ContentValues();
+            values.put(Data.RAW_CONTACT_ID, rawContactId);
+            values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+            values.put(Data.IS_PRIMARY, 1);
+            values.put(Phone.TYPE, Phone.TYPE_HOME);
+            values.put(Phone.NUMBER, phone);
+            mResolver.insert(Data.CONTENT_URI, values);
+        }
+    }
 }
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index 6e7a95a..af5a1fe 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -43,11 +43,9 @@
 @MediumTest
 public class CallLogProviderTest extends BaseContactsProvider2Test {
 
-    private static final boolean USE_LEGACY_PROVIDER = false;
-
     @Override
     protected Class<? extends ContentProvider> getProviderClass() {
-       return USE_LEGACY_PROVIDER ? ContactsProvider.class : SynchronousContactsProvider2.class;
+       return SynchronousContactsProvider2.class;
     }
 
     @Override
@@ -58,11 +56,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        if (USE_LEGACY_PROVIDER) {
-            addAuthority(CallLog.AUTHORITY);
-        } else {
-            addProvider(TestCallLogProvider.class, CallLog.AUTHORITY);
-        }
+        addProvider(TestCallLogProvider.class, CallLog.AUTHORITY);
     }
 
     public void testInsert() {
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..4322c5c 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
@@ -22,6 +22,8 @@
 import android.net.Uri;
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.test.suitebuilder.annotation.LargeTest;
 
@@ -368,6 +370,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");
@@ -626,9 +642,61 @@
         assertEquals(cupcakeId, queryPhotoId(queryContactId(rawContactId2)));
     }
 
-    private void assertSuggestions(long contactId, long... suggestions) {
-        forceAggregation();
+    public void testDisplayNameSources() {
+        long rawContactId = createRawContact();
+        long contactId = queryContactId(rawContactId);
 
+        assertNull(queryDisplayName(contactId));
+
+        insertEmail(rawContactId, "eclair@android.com");
+        assertEquals("eclair@android.com", queryDisplayName(contactId));
+
+        insertPhoneNumber(rawContactId, "800-555-5555");
+        assertEquals("800-555-5555", queryDisplayName(contactId));
+
+        ContentValues values = new ContentValues();
+        values.put(Organization.COMPANY, "Android");
+        insertOrganization(rawContactId, values);
+        assertEquals("Android", queryDisplayName(contactId));
+
+        insertNickname(rawContactId, "Dro");
+        assertEquals("Dro", queryDisplayName(contactId));
+
+        values.clear();
+        values.put(StructuredName.GIVEN_NAME, "Eclair");
+        values.put(StructuredName.FAMILY_NAME, "Android");
+        insertStructuredName(rawContactId, values);
+        assertEquals("Eclair Android", queryDisplayName(contactId));
+    }
+
+    public void testVerifiedName() {
+        long rawContactId1 = createRawContactWithName("test1", "TEST1");
+        storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.NAME_VERIFIED, "1");
+        long rawContactId2 = createRawContactWithName("test2", "TEST2");
+        long rawContactId3 = createRawContactWithName("test3", "TEST3 LONG");
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId3);
+
+        long contactId = queryContactId(rawContactId1);
+
+        // Should be the verified name
+        assertEquals("test1 TEST1", queryDisplayName(contactId));
+
+        // Mark a different name as verified - this should reset the NAME_VERIFIED field
+        // for the other rawContacts
+        storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.NAME_VERIFIED, "1");
+        assertStoredValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.NAME_VERIFIED, 0);
+        assertEquals("test2 TEST2", queryDisplayName(contactId));
+
+        // Reset the NAME_VERIFIED flag - now the most complex of the three names should win
+        storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.NAME_VERIFIED, "0");
+        assertEquals("test3 TEST3 LONG", queryDisplayName(contactId));
+    }
+
+    private void assertSuggestions(long contactId, long... suggestions) {
         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/ContactLocaleUtilsTest.java b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
new file mode 100644
index 0000000..a859581
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2010 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 java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import com.android.providers.contacts.ContactLocaleUtils;
+
+import android.provider.ContactsContract.FullNameStyle;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+public class ContactLocaleUtilsTest extends AndroidTestCase {
+    private static final String LATIN_NAME = "John Smith";
+    private static final String CHINESE_NAME = "\u675C\u9D51";
+    private static final String CHINESE_LATIN_MIX_NAME_1 = "D\u675C\u9D51";
+    private static final String CHINESE_LATIN_MIX_NAME_2 = "MARY \u675C\u9D51";
+    private static final String[] CHINESE_NAME_KEY = {"\u9D51", "\u675C\u9D51", "JUAN", "DUJUAN",
+            "J", "DJ"};
+    private static final String[] CHINESE_LATIN_MIX_NAME_1_KEY = {"\u9D51", "\u675C\u9D51",
+        "D \u675C\u9D51", "JUAN", "DUJUAN", "J", "DJ", "D DUJUAN", "DDJ"};
+    private static final String[] CHINESE_LATIN_MIX_NAME_2_KEY = {"\u9D51", "\u675C\u9D51",
+        "MARY \u675C\u9D51", "JUAN", "DUJUAN", "MARY DUJUAN", "J", "DJ", "MDJ"};
+    
+
+    public void testContactLocaleUtilsBase() throws Exception {
+        assertEquals(ContactLocaleUtils.getSortKey(LATIN_NAME, FullNameStyle.UNDEFINED),
+                LATIN_NAME);
+        assertNull(ContactLocaleUtils.getNameLookupKeys(LATIN_NAME,
+                FullNameStyle.UNDEFINED));
+    }
+
+    public void testChineseContactLocaleUtils() throws Exception {
+        assertTrue(ContactLocaleUtils.getSortKey(CHINESE_NAME,
+                FullNameStyle.CHINESE).equalsIgnoreCase("DU \u675C JUAN \u9D51"));
+
+        assertTrue(ContactLocaleUtils.getSortKey(CHINESE_LATIN_MIX_NAME_1,
+                FullNameStyle.CHINESE).equalsIgnoreCase("d DU \u675C JUAN \u9D51"));
+
+        assertTrue(ContactLocaleUtils.getSortKey(CHINESE_LATIN_MIX_NAME_2,
+                FullNameStyle.CHINESE).equalsIgnoreCase("mary DU \u675C JUAN \u9D51"));
+
+        Iterator<String> keys = ContactLocaleUtils.getNameLookupKeys(CHINESE_NAME, FullNameStyle.CHINESE);
+        verifyKeys(keys, CHINESE_NAME_KEY);
+
+        keys = ContactLocaleUtils.getNameLookupKeys(CHINESE_LATIN_MIX_NAME_1, FullNameStyle.CHINESE);
+        verifyKeys(keys, CHINESE_LATIN_MIX_NAME_1_KEY);
+
+        keys = ContactLocaleUtils.getNameLookupKeys(CHINESE_LATIN_MIX_NAME_2, FullNameStyle.CHINESE);
+        verifyKeys(keys, CHINESE_LATIN_MIX_NAME_2_KEY);
+    }
+
+    private void verifyKeys(final Iterator<String> resultKeys, final String[] expectedKeys) throws Exception {
+        HashSet<String> allKeys = new HashSet<String>();
+        while (resultKeys.hasNext()) {
+            allKeys.add(resultKeys.next());
+        }
+        Iterator<String> ks = allKeys.iterator();
+        Log.d("ContactLocaleUtilsTest", "Result keys");
+        while(ks.hasNext()) {
+            Log.d("ContactLocaleUtilsTest", ks.next());
+        }
+        Log.d("ContactLocaleUtilsTest", "Expected keys");
+        for(String ekey : expectedKeys) {
+            Log.d("ContactLocaleUtilsTest", ekey);
+        }
+        assertEquals(allKeys.size(), expectedKeys.length);
+        assertTrue(allKeys.containsAll(Arrays.asList(expectedKeys)));
+    }
+}
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/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 4568e5f..7dd980c 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -23,8 +23,8 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Binder;
@@ -43,10 +43,10 @@
 import android.test.mock.MockContext;
 import android.test.mock.MockPackageManager;
 import android.test.mock.MockResources;
-import android.util.Log;
 import android.util.TypedValue;
 
 import java.util.HashMap;
+import java.util.Locale;
 
 /**
  * Helper class that encapsulates an "actor" which is owned by a specific
@@ -131,7 +131,11 @@
             mPackageManager.addPackage(3000, PACKAGE_GREEN);
             mPackageManager.addPackage(4000, PACKAGE_BLUE);
 
-            mRes = new RestrictionMockResources(overallContext.getResources());
+            Resources resources = overallContext.getResources();
+            Configuration configuration = new Configuration(resources.getConfiguration());
+            configuration.locale = Locale.US;
+            resources.updateConfiguration(configuration, resources.getDisplayMetrics());
+            mRes = new RestrictionMockResources(resources);
         }
 
         @Override
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index afeb377..1e6bc49 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;
@@ -32,9 +35,12 @@
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.PhoneticNameStyle;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.Email;
@@ -48,6 +54,10 @@
 import android.test.MoreAsserts;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import java.text.Collator;
+import java.util.Arrays;
+import java.util.Locale;
+
 
 /**
  * Unit tests for {@link ContactsProvider2}.
@@ -174,8 +184,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 +330,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();
@@ -570,7 +577,7 @@
         values.put(StructuredName.DISPLAY_NAME, "Mr.John Kevin von Smith, Jr.");
         insertStructuredName(rawContactId, values);
 
-        assertStructuredName(rawContactId, "Mr", "John", "Kevin", "von Smith", "Jr");
+        assertStructuredName(rawContactId, "Mr", "John", "Kevin", "von Smith", "Jr.");
     }
 
     public void testDisplayNameParsingWhenPartsAreNull() {
@@ -580,7 +587,7 @@
         values.putNull(StructuredName.GIVEN_NAME);
         values.putNull(StructuredName.FAMILY_NAME);
         insertStructuredName(rawContactId, values);
-        assertStructuredName(rawContactId, "Mr", "John", "Kevin", "von Smith", "Jr");
+        assertStructuredName(rawContactId, "Mr", "John", "Kevin", "von Smith", "Jr.");
     }
 
     public void testDisplayNameParsingWhenPartsSpecified() {
@@ -593,6 +600,125 @@
         assertStructuredName(rawContactId, null, null, null, "Johnson", null);
     }
 
+    public void testContactWithoutPhoneticName() {
+        final long rawContactId = createRawContact(null);
+
+        ContentValues values = new ContentValues();
+        values.put(StructuredName.PREFIX, "Mr");
+        values.put(StructuredName.GIVEN_NAME, "John");
+        values.put(StructuredName.MIDDLE_NAME, "K.");
+        values.put(StructuredName.FAMILY_NAME, "Doe");
+        values.put(StructuredName.SUFFIX, "Jr.");
+        Uri dataUri = insertStructuredName(rawContactId, values);
+
+        values.clear();
+        values.put(RawContacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+        values.put(RawContacts.DISPLAY_NAME_PRIMARY, "John K. Doe, Jr.");
+        values.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, "Doe, John K., Jr.");
+        values.putNull(RawContacts.PHONETIC_NAME);
+        values.put(RawContacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+        values.put(RawContacts.SORT_KEY_PRIMARY, "John K. Doe, Jr.");
+        values.put(RawContacts.SORT_KEY_ALTERNATIVE, "Doe, John K., Jr.");
+
+        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+        assertStoredValues(rawContactUri, values);
+
+        values.clear();
+        values.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+        values.put(Contacts.DISPLAY_NAME_PRIMARY, "John K. Doe, Jr.");
+        values.put(Contacts.DISPLAY_NAME_ALTERNATIVE, "Doe, John K., Jr.");
+        values.putNull(Contacts.PHONETIC_NAME);
+        values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+        values.put(Contacts.SORT_KEY_PRIMARY, "John K. Doe, Jr.");
+        values.put(Contacts.SORT_KEY_ALTERNATIVE, "Doe, John K., Jr.");
+
+        Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
+                queryContactId(rawContactId));
+        assertStoredValues(contactUri, values);
+
+        // The same values should be available through a join with Data
+        assertStoredValues(dataUri, values);
+    }
+
+    public void testContactWithChineseName() {
+
+        // Only run this test when Chinese collation is supported
+        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+            return;
+        }
+
+        long rawContactId = createRawContact(null);
+
+        ContentValues values = new ContentValues();
+        values.put(StructuredName.DISPLAY_NAME, "\u6BB5\u5C0F\u6D9B");
+        Uri dataUri = insertStructuredName(rawContactId, values);
+
+        values.clear();
+        values.put(RawContacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+        values.put(RawContacts.DISPLAY_NAME_PRIMARY, "\u6BB5\u5C0F\u6D9B");
+        values.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, "\u6BB5\u5C0F\u6D9B");
+        values.putNull(RawContacts.PHONETIC_NAME);
+        values.put(RawContacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+        values.put(RawContacts.SORT_KEY_PRIMARY, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+        values.put(RawContacts.SORT_KEY_ALTERNATIVE, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+
+        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+        assertStoredValues(rawContactUri, values);
+
+        values.clear();
+        values.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+        values.put(Contacts.DISPLAY_NAME_PRIMARY, "\u6BB5\u5C0F\u6D9B");
+        values.put(Contacts.DISPLAY_NAME_ALTERNATIVE, "\u6BB5\u5C0F\u6D9B");
+        values.putNull(Contacts.PHONETIC_NAME);
+        values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+        values.put(Contacts.SORT_KEY_PRIMARY, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+        values.put(Contacts.SORT_KEY_ALTERNATIVE, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+
+        Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
+                queryContactId(rawContactId));
+        assertStoredValues(contactUri, values);
+
+        // The same values should be available through a join with Data
+        assertStoredValues(dataUri, values);
+    }
+
+    public void testContactWithJapaneseName() {
+        long rawContactId = createRawContact(null);
+
+        ContentValues values = new ContentValues();
+        values.put(StructuredName.GIVEN_NAME, "\u7A7A\u6D77");
+        values.put(StructuredName.PHONETIC_GIVEN_NAME, "\u304B\u3044\u304F\u3046");
+        Uri dataUri = insertStructuredName(rawContactId, values);
+
+        values.clear();
+        values.put(RawContacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+        values.put(RawContacts.DISPLAY_NAME_PRIMARY, "\u7A7A\u6D77");
+        values.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, "\u7A7A\u6D77");
+        values.put(RawContacts.PHONETIC_NAME, "\u304B\u3044\u304F\u3046");
+        values.put(RawContacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.JAPANESE);
+        values.put(RawContacts.SORT_KEY_PRIMARY, "\u304B\u3044\u304F\u3046");
+        values.put(RawContacts.SORT_KEY_ALTERNATIVE, "\u304B\u3044\u304F\u3046");
+
+        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+        assertStoredValues(rawContactUri, values);
+
+        values.clear();
+        values.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+        values.put(Contacts.DISPLAY_NAME_PRIMARY, "\u7A7A\u6D77");
+        values.put(Contacts.DISPLAY_NAME_ALTERNATIVE, "\u7A7A\u6D77");
+        values.put(Contacts.PHONETIC_NAME, "\u304B\u3044\u304F\u3046");
+        values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.JAPANESE);
+        values.put(Contacts.SORT_KEY_PRIMARY, "\u304B\u3044\u304F\u3046");
+        values.put(Contacts.SORT_KEY_ALTERNATIVE, "\u304B\u3044\u304F\u3046");
+
+        Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
+                queryContactId(rawContactId));
+        assertStoredValues(contactUri, values);
+
+        // The same values should be available through a join with Data
+        assertStoredValues(dataUri, values);
+    }
+
     public void testDisplayNameUpdate() {
         long rawContactId1 = createRawContact();
         insertEmail(rawContactId1, "potato@acme.com", true);
@@ -628,16 +754,10 @@
         insertPhoneNumber(rawContactId, "1-800-466-4411");
         assertStoredValue(uri, Contacts.DISPLAY_NAME, "1-800-466-4411");
 
-        // If there is title without company, the title is display name.
-        values.clear();
-        values.put(Organization.TITLE, "Protagonist");
-        Uri organizationUri = insertOrganization(rawContactId, values);
-        assertStoredValue(uri, Contacts.DISPLAY_NAME, "Protagonist");
-
         // If there are title and company, the company is display name.
         values.clear();
         values.put(Organization.COMPANY, "Monsters Inc");
-        mResolver.update(organizationUri, values, null, null);
+        Uri organizationUri = insertOrganization(rawContactId, values);
         assertStoredValue(uri, Contacts.DISPLAY_NAME, "Monsters Inc");
 
         // If there is nickname, that is display name.
@@ -650,7 +770,77 @@
         values.put(StructuredName.MIDDLE_NAME, "P.");
         values.put(StructuredName.FAMILY_NAME, "Sullivan");
         insertStructuredName(rawContactId, values);
-        assertStoredValue(uri, Contacts.DISPLAY_NAME, "James Sullivan");
+        assertStoredValue(uri, Contacts.DISPLAY_NAME, "James P. Sullivan");
+    }
+
+    public void testDisplayNameFromOrganizationWithoutPhoneticName() {
+        long rawContactId = createRawContact();
+        long contactId = queryContactId(rawContactId);
+        ContentValues values = new ContentValues();
+
+        Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+        // If there is title without company, the title is display name.
+        values.clear();
+        values.put(Organization.TITLE, "Protagonist");
+        Uri organizationUri = insertOrganization(rawContactId, values);
+        assertStoredValue(uri, Contacts.DISPLAY_NAME, "Protagonist");
+
+        // If there are title and company, the company is display name.
+        values.clear();
+        values.put(Organization.COMPANY, "Monsters Inc");
+        mResolver.update(organizationUri, values, null, null);
+
+        values.clear();
+        values.put(Contacts.DISPLAY_NAME, "Monsters Inc");
+        values.putNull(Contacts.PHONETIC_NAME);
+        values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+        values.put(Contacts.SORT_KEY_PRIMARY, "Monsters Inc");
+        values.put(Contacts.SORT_KEY_ALTERNATIVE, "Monsters Inc");
+        assertStoredValues(uri, values);
+    }
+
+    public void testDisplayNameFromOrganizationWithJapanesePhoneticName() {
+        long rawContactId = createRawContact();
+        long contactId = queryContactId(rawContactId);
+        ContentValues values = new ContentValues();
+
+        Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+        // If there is title without company, the title is display name.
+        values.clear();
+        values.put(Organization.COMPANY, "DoCoMo");
+        values.put(Organization.PHONETIC_NAME, "\u30C9\u30B3\u30E2");
+        Uri organizationUri = insertOrganization(rawContactId, values);
+
+        values.clear();
+        values.put(Contacts.DISPLAY_NAME, "DoCoMo");
+        values.put(Contacts.PHONETIC_NAME, "\u30C9\u30B3\u30E2");
+        values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.JAPANESE);
+        values.put(Contacts.SORT_KEY_PRIMARY, "\u30C9\u30B3\u30E2");
+        values.put(Contacts.SORT_KEY_ALTERNATIVE, "\u30C9\u30B3\u30E2");
+        assertStoredValues(uri, values);
+    }
+
+    public void testDisplayNameFromOrganizationWithChineseName() {
+        long rawContactId = createRawContact();
+        long contactId = queryContactId(rawContactId);
+        ContentValues values = new ContentValues();
+
+        Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+        // If there is title without company, the title is display name.
+        values.clear();
+        values.put(Organization.COMPANY, "\u4E2D\u56FD\u7535\u4FE1");
+        Uri organizationUri = insertOrganization(rawContactId, values);
+
+        values.clear();
+        values.put(Contacts.DISPLAY_NAME, "\u4E2D\u56FD\u7535\u4FE1");
+        values.putNull(Contacts.PHONETIC_NAME);
+        values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+        values.put(Contacts.SORT_KEY_PRIMARY, "ZHONG \u4E2D GUO \u56FD DIAN \u7535 XIN \u4FE1");
+        values.put(Contacts.SORT_KEY_ALTERNATIVE, "ZHONG \u4E2D GUO \u56FD DIAN \u7535 XIN \u4FE1");
+        assertStoredValues(uri, values);
     }
 
     public void testDisplayNameUpdateFromStructuredNameUpdate() {
@@ -679,6 +869,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 +1101,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);
     }
 
@@ -1083,9 +1332,9 @@
         Uri id_3_1 = insertEmail(id, "c3@email.com");
         Uri id_3_2 = insertPhoneNumber(id, "5551212c3");
 
-        EntityIterator iterator = mResolver.queryEntities(
-                maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, mAccount),
-                RawContacts.SOURCE_ID + " in ('c1', 'c2', 'c3')", null, null);
+        EntityIterator iterator = RawContacts.newEntityIterator(mResolver.query(
+                maybeAddAccountQueryParameters(RawContactsEntity.CONTENT_URI, mAccount), null,
+                RawContacts.SOURCE_ID + " in ('c1', 'c2', 'c3')", null, null));
         Entity entity;
         ContentValues[] subValues;
         entity = iterator.next();
@@ -1225,7 +1474,7 @@
     }
 
     public void testRawContactDeletion() {
-        long rawContactId = createRawContact();
+        long rawContactId = createRawContact(mAccount);
         Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
 
         insertImHandle(rawContactId, Im.PROTOCOL_GOOGLE_TALK, null, "deleteme@android.com");
@@ -1243,8 +1492,7 @@
         assertStoredValue(uri, RawContacts.DELETED, "1");
         assertNetworkNotified(true);
 
-        Uri permanentDeletionUri = uri.buildUpon().appendQueryParameter(
-                ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+        Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
         mResolver.delete(permanentDeletionUri, null, null);
         assertEquals(0, getCount(uri, null, null));
         assertEquals(0, getCount(Uri.withAppendedPath(uri, RawContacts.Data.CONTENT_DIRECTORY),
@@ -1256,8 +1504,8 @@
     }
 
     public void testRawContactDeletionKeepingAggregateContact() {
-        long rawContactId1 = createRawContactWithName();
-        long rawContactId2 = createRawContactWithName();
+        long rawContactId1 = createRawContactWithName(mAccount);
+        long rawContactId2 = createRawContactWithName(mAccount);
 
         // Same name - should be aggregated
         assertAggregated(rawContactId1, rawContactId2);
@@ -1265,8 +1513,7 @@
         long contactId = queryContactId(rawContactId1);
 
         Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1);
-        Uri permanentDeletionUri = uri.buildUpon().appendQueryParameter(
-                ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+        Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
         mResolver.delete(permanentDeletionUri, null, null);
         assertEquals(0, getCount(uri, null, null));
         assertEquals(1, getCount(Contacts.CONTENT_URI, Contacts._ID + "=" + contactId, null));
@@ -1334,7 +1581,6 @@
     public void testContactDeletion() {
         long rawContactId1 = createRawContactWithName("John", "Doe");
         long rawContactId2 = createRawContactWithName("John", "Doe");
-        forceAggregation();
 
         long contactId = queryContactId(rawContactId1);
 
@@ -1352,8 +1598,7 @@
 
         Uri uri = insertStructuredName(rawContactId, "John", "Doe");
         clearDirty(rawContactUri);
-        Uri updateUri = uri.buildUpon()
-                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+        Uri updateUri = setCallerIsSyncAdapter(uri, mAccount);
 
         ContentValues values = new ContentValues();
         values.put(StructuredName.FAMILY_NAME, "Dough");
@@ -1628,6 +1873,72 @@
         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");
+    }
+
+    public void testMissingAccountTypeParameter() {
+        // Try querying for RawContacts only using ACCOUNT_NAME
+        final Uri queryUri = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(
+                RawContacts.ACCOUNT_NAME, "lolwut").build();
+        try {
+            final Cursor cursor = mResolver.query(queryUri, null, null, null, null);
+            fail("Able to query with incomplete account query parameters");
+        } catch (IllegalArgumentException e) {
+            // Expected behavior.
+        }
+    }
+
+    public void testInsertInconsistentAccountType() {
+        // Try inserting RawContact with inconsistent Accounts
+        final Account red = new Account("red", "red");
+        final Account blue = new Account("blue", "blue");
+
+        final ContentValues values = new ContentValues();
+        values.put(RawContacts.ACCOUNT_NAME, red.name);
+        values.put(RawContacts.ACCOUNT_TYPE, red.type);
+
+        final Uri insertUri = maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, blue);
+        try {
+            mResolver.insert(insertUri, values);
+            fail("Able to insert RawContact with inconsistent account details");
+        } catch (IllegalArgumentException e) {
+            // Expected behavior.
+        }
+    }
+
+    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/GlobalSearchSupportTest.java b/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
index d8e3205..fe15306 100644
--- a/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
+++ b/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
@@ -18,37 +18,28 @@
 
 import android.accounts.Account;
 import android.app.SearchManager;
-import android.content.ContentUris;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.net.Uri;
 import android.provider.ContactsContract;
-import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Intents;
-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.Im;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import java.io.IOException;
-
 /**
  * Unit tests for {@link GlobalSearchSupport}.
- *
+ * <p>
  * Run the test like this:
- * <code>
+ * <p>
+ * <code><pre>
  * adb shell am instrument -e class com.android.providers.contacts.GlobalSearchSupportTest -w \
  *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
- * </code>
+ * </pre></code>
  */
 @LargeTest
 public class GlobalSearchSupportTest extends BaseContactsProvider2Test {
+
     public void testSearchSuggestionsNotInVisibleGroup() throws Exception {
         Account account = new Account("actname", "acttype");
         long rawContactId = createRawContact(account);
@@ -63,233 +54,61 @@
         c.close();
     }
 
-    public void testSearchSuggestionsByName() throws Exception {
-        long groupId = createGroup(mAccount, "gsid1", "title1");
-
-        assertSearchSuggestion(groupId,
-                true,  // name
-                false, // nickname
-                true,  // photo
-                false, // company
-                false, // title
-                false, // phone
-                false, // email
-                "D",   // query
-                true,  // expect icon URI
-                null, "Deer Dough", null);
-
-        assertSearchSuggestion(groupId,
-                true,  // name
-                false, // nickname
-                true,  // photo
-                true,  // company
-                false, // title
-                false, // phone
-                false, // email
-                "D",   // query
-                true,  // expect icon URI
-                null, "Deer Dough", "Google");
-
-        assertSearchSuggestion(groupId,
-                true,  // name
-                false, // nickname
-                true,  // photo
-                false, // company
-                false, // title
-                true,  // phone
-                false, // email
-                "D",   // query
-                true,  // expect icon URI
-                null, "Deer Dough", "1-800-4664-411");
-
-        assertSearchSuggestion(groupId,
-                true,  // name
-                false, // nickname
-                true,  // photo
-                false, // company
-                false, // title
-                false, // phone
-                true,  // email
-                "D",   // query
-                true,  // expect icon URI
-                String.valueOf(StatusUpdates.getPresenceIconResourceId(StatusUpdates.OFFLINE)),
-                "Deer Dough", "foo@acme.com");
-
-        assertSearchSuggestion(groupId,
-                true,  // name
-                false, // nickname
-                false, // photo
-                true,  // company
-                false, // title
-                false, // phone
-                false, // email
-                "D",   // query
-                false, // expect icon URI
-                null, "Deer Dough", "Google");
-
-        // Nickname is searchale
-        assertSearchSuggestion(groupId,
-                true,  // name
-                true,  // nickname
-                false, // photo
-                true,  // company
-                false, // title
-                false, // phone
-                false, // email
-                "L",   // query
-                false, // expect icon URI
-                null, "Deer Dough", "Google");
-
-        // Company is searchable
-        assertSearchSuggestion(groupId,
-                true,  // name
-                false, // nickname
-                false, // photo
-                true,  // company
-                false, // title
-                false, // phone
-                false, // email
-                "G",   // query
-                false, // expect icon URI
-                null, "Deer Dough", "Google");
-
-        // Title is searchable
-        assertSearchSuggestion(groupId,
-                true,  // name
-                false, // nickname
-                false, // photo
-                true,  // company
-                true,  // title
-                false, // phone
-                false, // email
-                "S",   // query
-                false, // expect icon URI
-                null, "Deer Dough", "Google");
+    public void testSearchSuggestionsByNameWithPhoto() throws Exception {
+        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
+                loadTestPhoto()).build();
+        new SuggestionTesterBuilder(contact).query("D").expectIcon1Uri(true).expectedText1(
+                "Deer Dough").build().test();
     }
 
-    private void assertSearchSuggestion(long groupId, boolean name, boolean nickname, boolean photo,
-            boolean company, boolean title, boolean phone, boolean email, String query,
-            boolean expectIcon1Uri, String expectedIcon2, String expectedText1,
-            String expectedText2) throws IOException {
-        ContentValues values = new ContentValues();
+    public void testSearchSuggestionsByNameWithPhotoAndCompany() throws Exception {
+        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
+                loadTestPhoto()).company("Google").build();
+        new SuggestionTesterBuilder(contact).query("D").expectIcon1Uri(true).expectedText1(
+                "Deer Dough").expectedText2("Google").build().test();
+    }
 
-        long rawContactId = createRawContact();
-        insertGroupMembership(rawContactId, groupId);
+    public void testSearchSuggestionsByNameWithPhotoAndPhone() {
+        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
+                loadTestPhoto()).phone("1-800-4664-411").build();
+        new SuggestionTesterBuilder(contact).query("D").expectIcon1Uri(true).expectedText1(
+                "Deer Dough").expectedText2("1-800-4664-411").build().test();
+    }
 
-        if (name) {
-            insertStructuredName(rawContactId, "Deer", "Dough");
-        }
+    public void testSearchSuggestionsByNameWithPhotoAndEmail() {
+        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
+                loadTestPhoto()).email("foo@acme.com").build();
+        new SuggestionTesterBuilder(contact).query("D").expectIcon1Uri(true).expectedIcon2(
+                String.valueOf(StatusUpdates.getPresenceIconResourceId(StatusUpdates.OFFLINE)))
+                .expectedText1("Deer Dough").expectedText2("foo@acme.com").build().test();
+    }
 
-        if (nickname) {
-            insertNickname(rawContactId, "Little Fawn");
-        }
+    public void testSearchSuggestionsByNameWithCompany() {
+        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").company("Google")
+                .build();
+        new SuggestionTesterBuilder(contact).query("D").expectedText1("Deer Dough").expectedText2(
+                "Google").build().test();
+    }
 
-        final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
-        if (photo) {
-            values.clear();
-            byte[] photoData = loadTestPhoto();
-            values.put(Data.RAW_CONTACT_ID, rawContactId);
-            values.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
-            values.put(Photo.PHOTO, photoData);
-            mResolver.insert(Data.CONTENT_URI, values);
-        }
+    public void testSearchByNicknameWithCompany() {
+        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").nickname(
+                "Little Fawn").company("Google").build();
+        new SuggestionTesterBuilder(contact).query("L").expectedText1("Deer Dough").expectedText2(
+                "Google").build().test();
+    }
 
-        if (company || title) {
-            values.clear();
-            values.put(Data.RAW_CONTACT_ID, rawContactId);
-            values.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
-            values.put(Organization.TYPE, Organization.TYPE_WORK);
-            if (company) {
-                values.put(Organization.COMPANY, "Google");
-            }
-            if (title) {
-                values.put(Organization.TITLE, "Software Engineer");
-            }
-            mResolver.insert(Data.CONTENT_URI, values);
-        }
+    public void testSearchByCompany() {
+        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").company("Google")
+                .build();
+        new SuggestionTesterBuilder(contact).query("G").expectedText1("Deer Dough").expectedText2(
+                "Google").build().test();
+    }
 
-        if (email) {
-            values.clear();
-            values.put(Data.RAW_CONTACT_ID, rawContactId);
-            values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
-            values.put(Email.TYPE, Email.TYPE_WORK);
-            values.put(Email.DATA, "foo@acme.com");
-            mResolver.insert(Data.CONTENT_URI, values);
-
-            int protocol = Im.PROTOCOL_GOOGLE_TALK;
-
-            values.clear();
-            values.put(StatusUpdates.PROTOCOL, protocol);
-            values.put(StatusUpdates.IM_HANDLE, "foo@acme.com");
-            values.put(StatusUpdates.IM_ACCOUNT, "foo");
-            values.put(StatusUpdates.PRESENCE_STATUS, StatusUpdates.OFFLINE);
-            values.put(StatusUpdates.PRESENCE_CUSTOM_STATUS, "Coding for Android");
-            mResolver.insert(StatusUpdates.CONTENT_URI, values);
-        }
-
-        if (phone) {
-            values.clear();
-            values.put(Data.RAW_CONTACT_ID, rawContactId);
-            values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
-            values.put(Data.IS_PRIMARY, 1);
-            values.put(Phone.TYPE, Phone.TYPE_HOME);
-            values.put(Phone.NUMBER, "1-800-4664-411");
-            mResolver.insert(Data.CONTENT_URI, values);
-        }
-
-        long contactId = queryContactId(rawContactId);
-        Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
-                .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath(query).build();
-
-        Cursor c = mResolver.query(searchUri, null, null, null, null);
-        assertEquals(1, c.getCount());
-        c.moveToFirst();
-        values.clear();
-
-        // SearchManager does not declare a constant for _id
-        values.put("_id", contactId);
-        values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
-        values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
-
-        String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
-        if (expectIcon1Uri) {
-            assertTrue(icon1.startsWith("content:"));
-        } else {
-            assertEquals(String.valueOf(com.android.internal.R.drawable.ic_contact_picture), icon1);
-        }
-
-        values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
-        values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, contactId);
-        values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, contactId);
-        assertCursorValues(c, values);
-        c.close();
-
-        // See if the same result is returned by a shortcut refresh
-        Uri shortcutsUri = ContactsContract.AUTHORITY_URI.buildUpon()
-                .appendPath(SearchManager.SUGGEST_URI_PATH_SHORTCUT).build();
-        Uri refreshUri = ContentUris.withAppendedId(shortcutsUri, contactId);
-
-        String[] projection = new String[]{
-                SearchManager.SUGGEST_COLUMN_ICON_1,
-                SearchManager.SUGGEST_COLUMN_ICON_2,
-                SearchManager.SUGGEST_COLUMN_TEXT_1,
-                SearchManager.SUGGEST_COLUMN_TEXT_2,
-                SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
-                SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
-                "_id",
-        };
-
-        c = mResolver.query(refreshUri, projection, null, null, null);
-        try {
-            assertEquals("Record count", 1, c.getCount());
-            c.moveToFirst();
-            assertCursorValues(c, values);
-        } finally {
-            c.close();
-        }
-
-        // Cleanup
-        mResolver.delete(rawContactUri, null, null);
+    public void testSearchByTitleWithCompany() {
+        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").company("Google")
+                .title("Software Engineer").build();
+        new SuggestionTesterBuilder(contact).query("S").expectIcon1Uri(false).expectedText1(
+                "Deer Dough").expectedText2("Google").build().test();
     }
 
     public void testSearchSuggestionsByPhoneNumber() throws Exception {
@@ -327,5 +146,225 @@
         assertCursorValues(c, values);
         c.close();
     }
-}
 
+    /**
+     * Tests that the quick search suggestion returns the expected contact
+     * information.
+     */
+    private final class SuggestionTester {
+
+        private final GoldenContact contact;
+
+        private final String query;
+
+        private final boolean expectIcon1Uri;
+
+        private final String expectedIcon2;
+
+        private final String expectedText1;
+
+        private final String expectedText2;
+
+        public SuggestionTester(SuggestionTesterBuilder builder) {
+            contact = builder.contact;
+            query = builder.query;
+            expectIcon1Uri = builder.expectIcon1Uri;
+            expectedIcon2 = builder.expectedIcon2;
+            expectedText1 = builder.expectedText1;
+            expectedText2 = builder.expectedText2;
+        }
+
+        /**
+         * Tests suggest and refresh queries from quick search box, then deletes the contact from
+         * the data base.
+         */
+        public void test() {
+
+            testQsbSuggest();
+            testContactIdQsbRefresh();
+            testLookupKeyQsbRefresh();
+
+            // Cleanup
+            contact.delete();
+        }
+
+        /**
+         * Tests that the contacts provider return the appropriate information from the golden
+         * contact in response to the suggestion query from the quick search box.
+         */
+        private void testQsbSuggest() {
+
+            Uri searchUri = new Uri.Builder().scheme("content").authority(
+                    ContactsContract.AUTHORITY).appendPath(SearchManager.SUGGEST_URI_PATH_QUERY)
+                    .appendPath(query).build();
+
+            Cursor c = mResolver.query(searchUri, null, null, null, null);
+            assertEquals(1, c.getCount());
+            c.moveToFirst();
+
+            String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
+            if (expectIcon1Uri) {
+                assertTrue(icon1.startsWith("content:"));
+            } else {
+                assertEquals(String.valueOf(com.android.internal.R.drawable.ic_contact_picture),
+                        icon1);
+            }
+
+            // SearchManager does not declare a constant for _id
+            ContentValues values = getContactValues();
+            assertCursorValues(c, values);
+            c.close();
+        }
+
+        /**
+         * Returns the expected Quick Search Box content values for the golden contact.
+         */
+        private ContentValues getContactValues() {
+
+            ContentValues values = new ContentValues();
+            values.put("_id", contact.getContactId());
+            values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
+            values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
+
+            values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
+            values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, contact.getLookupKey());
+            values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, contact.getLookupKey());
+            return values;
+        }
+
+        /**
+         * Performs the refresh query and returns a cursor to the results.
+         *
+         * @param refreshId the final component path of the refresh query, which identifies which
+         *        contact to refresh.
+         */
+        private Cursor refreshQuery(String refreshId) {
+
+            // See if the same result is returned by a shortcut refresh
+            Uri refershUri = ContactsContract.AUTHORITY_URI.buildUpon().appendPath(
+                    SearchManager.SUGGEST_URI_PATH_SHORTCUT)
+                    .appendPath(refreshId).build();
+
+            String[] projection = new String[] {
+                    SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_ICON_2,
+                    SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_2,
+                    SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
+                    SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "_id",
+            };
+
+            return mResolver.query(refershUri, projection, null, null, null);
+        }
+
+        /**
+         * Tests that the contacts provider returns an empty result in response to a refresh query
+         * from the quick search box that uses the contact id to identify the contact.  The empty
+         * result indicates that the shortcut is no longer valid, and the QSB will replace it with
+         * a new-style shortcut the next time they click on the contact.
+         *
+         * @see #testLookupKeyQsbRefresh()
+         */
+        private void testContactIdQsbRefresh() {
+
+            Cursor c = refreshQuery(String.valueOf(contact.getContactId()));
+            try {
+                assertEquals("Record count", 0, c.getCount());
+            } finally {
+                c.close();
+            }
+        }
+
+        /**
+         * Tests that the contacts provider return the appropriate information from the golden
+         * contact in response to the refresh query from the quick search box.  The refresh query
+         * uses the currently-supported mechanism of identifying the contact by the lookup key,
+         * which is more stable than the previously used contact id.
+         */
+        private void testLookupKeyQsbRefresh() {
+
+            Cursor c = refreshQuery(contact.getLookupKey());
+            try {
+                assertEquals("Record count", 1, c.getCount());
+                c.moveToFirst();
+                assertCursorValues(c, getContactValues());
+            } finally {
+                c.close();
+            }
+        }
+    }
+
+    /**
+     * Builds {@link SuggestionTester} objects. Unspecified boolean objects default to
+     * false. Unspecified String objects default to null.
+     */
+    private final class SuggestionTesterBuilder {
+
+        private final GoldenContact contact;
+
+        private String query;
+
+        private boolean expectIcon1Uri;
+
+        private String expectedIcon2;
+
+        private String expectedText1;
+
+        private String expectedText2;
+
+        public SuggestionTesterBuilder(GoldenContact contact) {
+            this.contact = contact;
+        }
+
+        /**
+         * Builds the {@link SuggestionTester} specified by this builder.
+         */
+        public SuggestionTester build() {
+            return new SuggestionTester(this);
+        }
+
+        /**
+         * The text of the user's query to quick search (i.e., what they typed
+         * in the search box).
+         */
+        public SuggestionTesterBuilder query(String value) {
+            query = value;
+            return this;
+        }
+
+        /**
+         * Whether to set Icon1, which in practice is the contact's photo.
+         * <p>
+         * TODO(tomo): Replace with actual expected value? This might be hard
+         * because the values look non-deterministic, such as
+         * "content://com.android.contacts/contacts/2015/photo"
+         */
+        public SuggestionTesterBuilder expectIcon1Uri(boolean value) {
+            expectIcon1Uri = value;
+            return this;
+        }
+
+        /**
+         * The value for Icon2, which in practice is the contact's Chat status
+         * (available, busy, etc.)
+         */
+        public SuggestionTesterBuilder expectedIcon2(String value) {
+            expectedIcon2 = value;
+            return this;
+        }
+
+        /**
+         * First line of suggestion text expected to be returned (required).
+         */
+        public SuggestionTesterBuilder expectedText1(String value) {
+            expectedText1 = value;
+            return this;
+        }
+
+        /**
+         * Second line of suggestion text expected to return (optional).
+         */
+        public SuggestionTesterBuilder expectedText2(String value) {
+            expectedText2 = value;
+            return this;
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/GroupsTest.java b/tests/src/com/android/providers/contacts/GroupsTest.java
index 085fef6..ebec559 100644
--- a/tests/src/com/android/providers/contacts/GroupsTest.java
+++ b/tests/src/com/android/providers/contacts/GroupsTest.java
@@ -156,8 +156,7 @@
         Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
                 createGroup(mAccount, "gsid1", "title1"));
         clearDirty(uri);
-        Uri updateUri = uri.buildUpon()
-                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+        Uri updateUri = setCallerIsSyncAdapter(uri, mAccount);
 
         ContentValues values = new ContentValues();
         values.put(Groups.NOTES, "New notes");
@@ -189,8 +188,7 @@
         assertEquals(1, getCount(uri, null, null));
         assertStoredValue(uri, Groups.DELETED, "1");
 
-        Uri permanentDeletionUri = uri.buildUpon()
-                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+        Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
         mResolver.delete(permanentDeletionUri, null, null);
         assertEquals(0, getCount(uri, null, null));
     }
@@ -201,8 +199,7 @@
         Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
 
         assertEquals(1, getCount(uri, null, null));
-        Uri permanentDeletionUri = uri.buildUpon()
-                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+        Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
         mResolver.delete(permanentDeletionUri, null, null);
         assertEquals(0, getCount(uri, null, null));
     }
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/LegacyContactsProviderTest.java b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
index 55649d0..85ed6a7 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
@@ -52,11 +52,9 @@
 @SuppressWarnings("deprecation")
 public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
 
-    private static final boolean USE_LEGACY_PROVIDER = false;
-
     @Override
     protected Class<? extends ContentProvider> getProviderClass() {
-       return USE_LEGACY_PROVIDER ? ContactsProvider.class : SynchronousContactsProvider2.class;
+       return SynchronousContactsProvider2.class;
     }
 
     @Override
@@ -730,23 +728,8 @@
      * Capturing the search suggestion requirements in test cases as a reference.
      */
     public void testSearchSuggestionsNotInMyContacts() throws Exception {
-
         // We don't provide compatibility for search suggestions
-        if (!USE_LEGACY_PROVIDER) {
-            return;
-        }
-
-        ContentValues values = new ContentValues();
-        putContactValues(values);
-        mResolver.insert(People.CONTENT_URI, values);
-
-        Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
-                .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("D").build();
-
-        // If the contact is not in the "my contacts" group, nothing should be found
-        Cursor c = mResolver.query(searchUri, null, null, null, null);
-        assertEquals(0, c.getCount());
-        c.close();
+        return;
     }
 
     /**
@@ -755,50 +738,7 @@
     public void testSearchSuggestionsByName() throws Exception {
 
         // We don't provide compatibility for search suggestions
-        if (!USE_LEGACY_PROVIDER) {
-            return;
-        }
-
-        assertSearchSuggestion(
-                true,  // name
-                true,  // photo
-                true,  // organization
-                false, // phone
-                false, // email
-                "D",   // query
-                true,  // expect icon URI
-                null, "Deer Dough", "Google");
-
-        assertSearchSuggestion(
-                true,  // name
-                true,  // photo
-                false, // organization
-                true,  // phone
-                false, // email
-                "D",   // query
-                true,  // expect icon URI
-                null, "Deer Dough", "1-800-4664-411");
-
-        assertSearchSuggestion(
-                true,  // name
-                true,  // photo
-                false, // organization
-                false, // phone
-                true,  // email
-                "D",   // query
-                true,  // expect icon URI
-                String.valueOf(Presence.getPresenceIconResourceId(Presence.OFFLINE)),
-                "Deer Dough", "foo@acme.com");
-
-        assertSearchSuggestion(
-                true,  // name
-                false, // photo
-                true,  // organization
-                false, // phone
-                false, // email
-                "D",   // query
-                false, // expect icon URI
-                null, "Deer Dough", "Google");
+        return;
     }
 
     private void assertSearchSuggestion(boolean name, boolean photo, boolean organization,
@@ -897,44 +837,8 @@
      * Capturing the search suggestion requirements in test cases as a reference.
      */
     public void testSearchSuggestionsByPhoneNumber() throws Exception {
-
         // We don't provide compatibility for search suggestions
-        if (!USE_LEGACY_PROVIDER) {
-            return;
-        }
-
-        ContentValues values = new ContentValues();
-
-        Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
-                .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("12345").build();
-
-        Cursor c = mResolver.query(searchUri, null, null, null, null);
-        assertEquals(2, c.getCount());
-        c.moveToFirst();
-
-        values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Execute");
-        values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "");
-        values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
-                String.valueOf(com.android.internal.R.drawable.call_contact));
-        values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
-                Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
-        values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
-        values.putNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
-        assertCursorValues(c, values);
-
-        c.moveToNext();
-        values.clear();
-        values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Dial number");
-        values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345");
-        values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
-                String.valueOf(com.android.internal.R.drawable.create_contact));
-        values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
-                Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
-        values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
-        values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
-                SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
-        assertCursorValues(c, values);
-        c.close();
+        return;
     }
 
     public void testSettings() throws Exception {
diff --git a/tests/src/com/android/providers/contacts/NameSplitterTest.java b/tests/src/com/android/providers/contacts/NameSplitterTest.java
index 9fa0d96..87c897e 100644
--- a/tests/src/com/android/providers/contacts/NameSplitterTest.java
+++ b/tests/src/com/android/providers/contacts/NameSplitterTest.java
@@ -18,6 +18,8 @@
 
 import com.android.providers.contacts.NameSplitter.Name;
 
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhoneticNameStyle;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import java.util.Locale;
@@ -40,9 +42,12 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        createNameSplitter(Locale.US);
+    }
 
-        mNameSplitter = new NameSplitter("Mr, Ms, Mrs", "d', st, st., von", "Jr, M.D., MD, D.D.S.",
-                "&, AND", Locale.getDefault());
+    private void createNameSplitter(Locale locale) {
+        mNameSplitter = new NameSplitter("Mr, Ms, Mrs", "d', st, st., von", "Jr., M.D., MD, D.D.S.",
+                "&, AND", locale);
     }
 
     public void testNull() {
@@ -60,7 +65,7 @@
         assertJoinedName(null, null, null, null, null, null);
     }
 
-    public void testLastName() {
+    public void testFamilyName() {
         assertSplitName("Smith", null, "Smith", null, null, null);
         assertJoinedName("Smith", null, "Smith", null, null, null);
     }
@@ -70,104 +75,301 @@
         assertJoinedName("MD", "Ms", null, null, "MD", null);
     }
 
-    public void testFirstLastName() {
+    public void testGivenFamilyName() {
         assertSplitName("John Smith", null, "John", null, "Smith", null);
         assertJoinedName("John Smith", null, "John", null, "Smith", null);
     }
 
-    public void testFirstMiddleLastName() {
+    public void testGivenMiddleFamilyName() {
         assertSplitName("John Edward Smith", null, "John", "Edward", "Smith", null);
-        assertJoinedName("John Smith", null, "John", "Edward", "Smith", null);
+        assertJoinedName("John Edward Smith", null, "John", "Edward", "Smith", null);
     }
 
-    public void testThreeNamesAndLastName() {
+    public void testThreeNamesAndFamilyName() {
         assertSplitName("John Edward Kevin Smith", null, "John Edward", "Kevin", "Smith", null);
-        assertJoinedName("John Edward Smith", null, "John Edward", "Kevin", "Smith", null);
+        assertJoinedName("John Edward Kevin Smith", null, "John Edward", "Kevin", "Smith", null);
     }
 
-    public void testPrefixFirstLastName() {
+    public void testPrefixFivenFamilyName() {
         assertSplitName("Mr. John Smith", "Mr", "John", null, "Smith", null);
         assertJoinedName("John Smith", "Mr", "John", null, "Smith", null);
         assertSplitName("Mr.John Smith", "Mr", "John", null, "Smith", null);
         assertJoinedName("John Smith", "Mr", "John", null, "Smith", null);
     }
 
-    public void testFirstLastNameSuffix() {
-        assertSplitName("John Smith Jr.", null, "John", null, "Smith", "Jr");
-        assertJoinedName("John Smith", null, "John", null, "Smith", "Jr");
+    public void testFivenFamilyNameSuffix() {
+        assertSplitName("John Smith Jr", null, "John", null, "Smith", "Jr");
+        assertJoinedName("John Smith, Jr.", null, "John", null, "Smith", "Jr");
     }
 
-    public void testFirstLastNameSuffixWithDot() {
+    public void testGivenFamilyNameSuffixWithDot() {
         assertSplitName("John Smith M.D.", null, "John", null, "Smith", "M.D.");
-        assertJoinedName("John Smith", null, "John", null, "Smith", "M.D.");
+        assertJoinedName("John Smith, M.D.", null, "John", null, "Smith", "M.D.");
         assertSplitName("John Smith D D S", null, "John", null, "Smith", "D D S");
-        assertJoinedName("John Smith", null, "John", null, "Smith", "D D S");
+        assertJoinedName("John Smith, D D S", null, "John", null, "Smith", "D D S");
     }
 
-    public void testFirstSuffixLastName() {
+    public void testGivenSuffixFamilyName() {
         assertSplitName("John von Smith", null, "John", null, "von Smith", null);
         assertJoinedName("John von Smith", null, "John", null, "von Smith", null);
     }
 
-    public void testFirstSuffixLastNameWithDot() {
+    public void testGivenSuffixFamilyNameWithDot() {
         assertSplitName("John St.Smith", null, "John", null, "St. Smith", null);
         assertJoinedName("John St. Smith", null, "John", null, "St. Smith", null);
     }
 
-    public void testPrefixFirstMiddleLast() {
+    public void testPrefixGivenMiddleFamily() {
         assertSplitName("Mr. John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
-        assertJoinedName("John Smith", "Mr", "John", "Kevin", "Smith", null);
+        assertJoinedName("John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
         assertSplitName("Mr.John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
-        assertJoinedName("John Smith", "Mr", "John", "Kevin", "Smith", null);
+        assertJoinedName("John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
     }
 
-    public void testPrefixFirstMiddleLastSuffix() {
-        assertSplitName("Mr. John Kevin Smith Jr.", "Mr", "John", "Kevin", "Smith", "Jr");
-        assertJoinedName("John Smith", "Mr", "John", "Kevin", "Smith", "Jr");
+    public void testPrefixGivenMiddleFamilySuffix() {
+        assertSplitName("Mr. John Kevin Smith Jr.", "Mr", "John", "Kevin", "Smith", "Jr.");
+        assertJoinedName("John Kevin Smith, Jr.", "Mr", "John", "Kevin", "Smith", "Jr");
     }
 
-    public void testPrefixFirstMiddlePrefixLastSuffixWrongCapitalization() {
-        assertSplitName("MR. john keVin VON SmiTh JR.", "MR", "john", "keVin", "VON SmiTh", "JR");
-        assertJoinedName("john VON SmiTh", "MR", "john", "keVin", "VON SmiTh", "JR");
+    public void testPrefixGivenMiddlePrefixFamilySuffixWrongCapitalization() {
+        assertSplitName("MR. john keVin VON SmiTh JR.", "MR", "john", "keVin", "VON SmiTh", "JR.");
+        assertJoinedName("john keVin VON SmiTh, JR.", "MR", "john", "keVin", "VON SmiTh", "JR");
     }
 
-    public void testPrefixLastSuffix() {
-        assertSplitName("von Smith Jr.", null, null, null, "von Smith", "Jr");
-        assertJoinedName("von Smith", null, null, null, "von Smith", "Jr");
+    public void testPrefixFamilySuffix() {
+        assertSplitName("von Smith Jr.", null, null, null, "von Smith", "Jr.");
+        assertJoinedName("von Smith, Jr.", null, null, null, "von Smith", "Jr");
     }
 
-    public void testTwoNamesAndLastNameWithAmpersand() {
+    public void testFamilyNameGiven() {
+        assertSplitName("Smith, John", null, "John", null, "Smith", null);
+        assertSplitName("Smith  , John", null, "John", null, "Smith", null);
+        assertSplitName("Smith, John Kimble", null, "John", "Kimble", "Smith", null);
+        assertSplitName("Smith, John K.", null, "John", "K.", "Smith", null);
+        assertSplitName("Smith, John, Jr.", null, "John", null, "Smith", "Jr.");
+        assertSplitName("Smith, John Kimble, Jr.", null, "John", "Kimble", "Smith", "Jr.");
+        assertSplitName("von Braun, John, Jr.", null, "John", null, "von Braun", "Jr.");
+        assertSplitName("von Braun, John Kimble, Jr.", null, "John", "Kimble", "von Braun", "Jr.");
+    }
+
+    public void testTwoNamesAndFamilyNameWithAmpersand() {
         assertSplitName("John & Edward Smith", null, "John & Edward", null, "Smith", null);
         assertJoinedName("John & Edward Smith", null, "John & Edward", null, "Smith", null);
         assertSplitName("John and Edward Smith", null, "John and Edward", null, "Smith", null);
+        assertSplitName("Smith, John and Edward", null, "John and Edward", null, "Smith", null);
         assertJoinedName("John and Edward Smith", null, "John and Edward", null, "Smith", null);
     }
 
     public void testWithMiddleInitialAndNoDot() {
-        assertSplitName("John E. Smith", null, "John", "E", "Smith", null);
-        assertJoinedName("John Smith", null, "John", "E", "Smith", null);
+        assertSplitName("John E. Smith", null, "John", "E.", "Smith", null);
+        assertJoinedName("John E Smith", null, "John", "E", "Smith", null);
     }
 
-    public void testWithLongFirstNameAndDot() {
-        assertSplitName("John Ed. K. Smith", null, "John Ed.", "K", "Smith", null);
-        assertJoinedName("John Ed. Smith", null, "John Ed.", "K", "Smith", null);
+    public void testWithLongGivenNameAndDot() {
+        assertSplitName("John Ed. K. Smith", null, "John Ed.", "K.", "Smith", null);
+        assertJoinedName("John Ed. K Smith", null, "John Ed.", "K", "Smith", null);
+    }
+
+    public void testGuessFullNameStyleEmpty() {
+        assertFullNameStyle(FullNameStyle.UNDEFINED, null);
+        assertFullNameStyle(FullNameStyle.UNDEFINED, "");
+    }
+
+    public void testGuessFullNameStyleWestern() {
+
+        // Latin letters
+        assertFullNameStyle(FullNameStyle.WESTERN, "John Doe");
+
+        // Starts with a Latin letter, but contains Japanese letters
+        assertFullNameStyle(FullNameStyle.JAPANESE, "A\u3080\u308D\u306A\u307F\u3048");
+
+        // Starts with an Extended Latin letter "Latin Capital Ligature OE"
+        assertFullNameStyle(FullNameStyle.WESTERN, "\u0152uvre");
+
+        // Non-letters don't make a difference. This one starts with a vertical line
+        assertFullNameStyle(FullNameStyle.WESTERN, "\uFF5C.?+Smith");
+    }
+
+    public void testGuessFullNameStyleJapanese() {
+        createNameSplitter(Locale.JAPAN);
+
+        // Hiragana: always Japanese
+        assertFullNameStyle(FullNameStyle.JAPANESE, "\u3042\u3080\u308D\u306A\u307F\u3048");
+
+        // Katakana: always Japanese
+        assertFullNameStyle(FullNameStyle.JAPANESE, "\u30A2\u30E0\u30ED \u30CA\u30DF\u30A8");
+
+        // Half-width Katakana: always Japanese
+        assertFullNameStyle(FullNameStyle.JAPANESE, "\uFF71\uFF91\uFF9B \uFF85\uFF90\uFF74");
+
+        // Kanji: we cannot tell if this is Japanese, Chinese or Korean, but we are
+        // in Locale.JAPAN, so assume Japanese
+        assertFullNameStyle(FullNameStyle.JAPANESE, "\u5B89\u5BA4\u5948\u7F8E\u6075");
+
+        // TODO: mix
+
+        // Accompanied by a phonetic name in Hiragana, we can safely assume that the
+        // name is Japanese
+        assertFullNameStyle(FullNameStyle.JAPANESE, "\u5B89\u5BA4\u5948\u7F8E\u6075",
+                "\u3042\u3080\u308D", null, "\u306A\u307F\u3048");
+
+        // Starts with a latin letter - not Western
+        assertFullNameStyle(FullNameStyle.JAPANESE, "A\u3080\u308D\u306A\u307F\u3048");
+    }
+
+    public void testGuessFullNameStyleChinese() {
+        createNameSplitter(Locale.CHINA);
+
+        // Hanzi: we cannot tell if this is Chinese, Japanese or Korean,
+        // but we are in Locale.CHINA, so assume this is Chinese
+        assertFullNameStyle(FullNameStyle.CHINESE, "\u675C\u9D51");
+
+        // Accompanied by a phonetic name in Pinyin, we can safely assume that the
+        // name is Chinese
+        assertFullNameStyle(FullNameStyle.CHINESE, "\u675C\u9D51",
+                "du4", null, "juan1");
+
+        // Non-letters don't make a difference. This one starts with a vertical line
+        assertFullNameStyle(FullNameStyle.CHINESE, "\uFF5C--(\u675C\u9D51)");
+    }
+
+
+    public void testGuessPhoneticNameStyle() {
+
+        // Hiragana
+        assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, "\u3042\u3080\u308D", null, null);
+        assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, null, "\u3042\u3080\u308D", null);
+        assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, null, null, "\u306A\u307F\u3048");
+        assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, "\u3042\u3080\u308D", null,
+                "\u306A\u307F\u3048");
+
+        // Katakana
+        assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, "\u30A2\u30E0\u30ED", null,
+                "\u30CA\u30DF\u30A8");
+
+        // Half-width Katakana
+        assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, "\u30A2\u30E0\u30ED", null,
+                "\u30CA\u30DF\u30A8");
+
+        // Chinese
+        assertPhoneticNameStyle(PhoneticNameStyle.PINYIN, "du4", null, "juan1");
+    }
+
+    public void testSplitJapaneseName() {
+        createNameSplitter(Locale.JAPAN);
+
+        // One word is interpreted as given name only
+        assertSplitName("\u3042\u3080\u308D", null, "\u3042\u3080\u308D", null, null, null);
+
+        // Two words are interpreted as family + give name
+        assertSplitName("\u3042\u3080\u308D \u306A\u307F\u3048", null, "\u306A\u307F\u3048", null,
+                "\u3042\u3080\u308D", null);
+
+        // Multiple words are interpreted as "family - given names"
+        assertSplitName("\u3042\u3080\u308D \u3068\u304A\u308B \u306A\u307F\u3048", null,
+                "\u3068\u304A\u308B \u306A\u307F\u3048", null, "\u3042\u3080\u308D", null);
+
+        // Hanzi characters without spaces: lump them all in the given name
+        assertSplitName("\u6BB5\u5C0F\u6D9B", null, "\u6BB5\u5C0F\u6D9B", null, null, null);
+    }
+
+    public void testSplitChineseName() {
+        createNameSplitter(Locale.CHINA);
+
+        // Two Hanzi characters: familyName+givenName
+        assertSplitName("\u6BB5\u5C0F", null, "\u5C0F", null, "\u6BB5", null);
+
+        // Two Hanzi characters: familyName+middleName+givenName
+        assertSplitName("\u6BB5\u5C0F\u6D9B", null, "\u6D9B", "\u5C0F", "\u6BB5", null);
+
+        // Two Hanzi characters: familyName(2)+middleName+givenName
+        assertSplitName("\u6BB5\u5C0F\u6D9B\u6D9C", null, "\u6D9C", "\u6D9B", "\u6BB5\u5C0F", null);
+    }
+
+    public void testJoinJapaneseName() {
+        createNameSplitter(Locale.JAPAN);
+
+        assertJoinedName("\u3042\u3080\u308D", FullNameStyle.JAPANESE, null, "\u3042\u3080\u308D",
+                null, null, null, true);
+
+        // Given-name-first flag is ignored for CJK locales
+        assertJoinedName("\u3084\u307E\u3056\u304D \u3068\u304A\u308B", FullNameStyle.JAPANESE,
+                null, "\u3068\u304A\u308B", null, "\u3084\u307E\u3056\u304D", null, false);
+        assertJoinedName("\u3084\u307E\u3056\u304D \u3068\u304A\u308B \u3068\u304A\u308B",
+                FullNameStyle.JAPANESE, null, "\u3068\u304A\u308B", "\u3068\u304A\u308B",
+                "\u3084\u307E\u3056\u304D", null, false);
+    }
+
+    public void testJoinChineseName() {
+        createNameSplitter(Locale.CHINA);
+
+        // Given-name-first flag is ignored for CJK locales
+        assertJoinedName("\u6BB5\u5C0F\u6D9B", FullNameStyle.CHINESE, null,
+                "\u6D9B", "\u5C0F", "\u6BB5", null, true);
+        assertJoinedName("\u6BB5\u5C0F\u6D9B", FullNameStyle.CHINESE, null,
+                "\u6D9B", "\u5C0F", "\u6BB5", null, false);
     }
 
     private void assertSplitName(String fullName, String prefix, String givenNames,
-            String middleName, String lastName, String suffix) {
+            String middleName, String familyName, String suffix) {
         final Name name = new Name();
         mNameSplitter.split(name, fullName);
         assertEquals(prefix, name.getPrefix());
         assertEquals(givenNames, name.getGivenNames());
         assertEquals(middleName, name.getMiddleName());
-        assertEquals(lastName, name.getFamilyName());
+        assertEquals(familyName, name.getFamilyName());
         assertEquals(suffix, name.getSuffix());
     }
 
-    private void assertJoinedName(String fullName, String prefix, String givenNames, String middleName,
-            String lastName, String suffix) {
-        final Name name = new Name(prefix, givenNames, middleName, lastName, suffix);
-        final String joined = mNameSplitter.join(name);
-        assertEquals(fullName, joined);
+    private void assertJoinedName(String expected, String prefix, String givenNames,
+            String middleName, String familyName, String suffix) {
+        assertJoinedName(expected, FullNameStyle.WESTERN, prefix, givenNames, middleName,
+                familyName, suffix, true);
+    }
+
+    private void assertJoinedName(String expected, int nameStyle, String prefix, String givenNames,
+            String middleName, String familyName, String suffix, boolean givenNameFirst) {
+        Name name = new Name();
+        name.fullNameStyle = nameStyle;
+        name.prefix = prefix;
+        name.givenNames = givenNames;
+        name.middleName = middleName;
+        name.familyName = familyName;
+        name.suffix = suffix;
+        String actual = mNameSplitter.join(name, givenNameFirst);
+        assertEquals(expected, actual);
+    }
+
+    private void assertFullNameStyle(int expectedFullNameStyle, String fullName) {
+        Name name = new Name();
+        mNameSplitter.split(name, fullName);
+        mNameSplitter.guessNameStyle(name);
+
+        assertEquals(expectedFullNameStyle, name.fullNameStyle);
+    }
+
+    private void assertFullNameStyle(int expectedFullNameStyle, String fullName,
+            String phoneticFamilyName, String phoneticMiddleName, String phoneticGivenName) {
+        Name name = new Name();
+        mNameSplitter.split(name, fullName);
+        name.phoneticFamilyName = phoneticFamilyName;
+        name.phoneticMiddleName = phoneticMiddleName;
+        name.phoneticGivenName = phoneticGivenName;
+
+        mNameSplitter.guessNameStyle(name);
+
+        assertEquals(expectedFullNameStyle, name.fullNameStyle);
+    }
+
+    private void assertPhoneticNameStyle(int expectedPhoneticNameStyle, String phoneticFamilyName,
+            String phoneticMiddleName, String phoneticGivenName) {
+        Name name = new Name();
+        name.phoneticFamilyName = phoneticFamilyName;
+        name.phoneticMiddleName = phoneticMiddleName;
+        name.phoneticGivenName = phoneticGivenName;
+
+        mNameSplitter.guessNameStyle(name);
+
+        assertEquals(expectedPhoneticNameStyle, name.phoneticNameStyle);
     }
 }
diff --git a/tests/src/com/android/providers/contacts/PostalSplitterForJapaneseTest.java b/tests/src/com/android/providers/contacts/PostalSplitterForJapaneseTest.java
new file mode 100644
index 0000000..b4be173
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/PostalSplitterForJapaneseTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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 com.android.providers.contacts.PostalSplitter.Postal;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.Locale;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link PostalSplitter}, especially for ja_JP locale.
+ * This class depends on the assumption that all the tests in {@link NameSplitterTest} pass.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -e class com.android.providers.contacts.PostalSplitterForJapaneseTest -w
+ *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@SmallTest
+public class PostalSplitterForJapaneseTest extends TestCase {
+    private PostalSplitter mPostalSplitter;
+
+    // Postal address for Tokyo Metropolitan City Hall (Tokyo-Tocho) as of 2009 + pseudo PO box.
+    // Japanese don't use neighborhood, so it is not used in this test suite.
+    //
+    // "Nihon" in Kanji
+    private static final String COUNTRY = "\u65E5\u672C";
+    private static final String POSTCODE = "163-8001";
+    // "Tokyo-to" in Kanji
+    private static final String REGION = "\u6771\u4EAC\u90FD";
+    // "Sinjuku-ku" in Kanji
+    private static final String CITY = "\u65B0\u5BBF\u533A";
+    // Nishi-Sinjuku 2-8-1
+    private static final String STREET = "\u897F\u65B0\u5BBF 2-8-1";
+    // Pseudo PO box for test: "Sisyobako 404"
+    private static final String POBOX = "\u79C1\u66F8\u7BB1";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mPostalSplitter = new PostalSplitter(Locale.JAPAN);
+    }
+
+    public void testNull() {
+        assertSplitPostal(null, null, null, null, null, null, null, null);
+        assertJoinedPostal(null, null, null, null, null, null, null, null);
+    }
+
+    public void testEmpty() {
+        assertSplitPostal("", null, null, null, null, null, null, null);
+        assertJoinedPostal(null, null, null, null, null, null, null, null);
+    }
+
+    public void testSpaces() {
+        assertSplitPostal(" ", " ", null, null, null, null, null, null);
+        assertJoinedPostal(" ", " ", null, null, null, null, null, null);
+    }
+
+    public void testPobox() {
+        assertJoinedPostal(CITY + "\n" + POBOX, null, POBOX, null, CITY, null, null, null);
+    }
+
+    public void testNormal() {
+        assertJoinedPostal(POSTCODE + "\n" + REGION + " " + CITY + "\n" + STREET,
+                STREET, null, null, CITY, REGION, POSTCODE, null);
+    }
+
+    public void testMissingRegion() {
+        assertJoinedPostal(POSTCODE + "\n" + REGION + "\n" + STREET,
+                STREET, null, null, REGION, null, POSTCODE, null);
+
+        assertJoinedPostal(POSTCODE + "\n" + STREET,
+                STREET, null, null, null, null, POSTCODE, null);
+
+        assertJoinedPostal(COUNTRY + " " + POSTCODE + "\n" + STREET,
+                STREET, null, null, null, null, POSTCODE, COUNTRY);
+    }
+
+    public void testMissingPostcode() {
+        assertJoinedPostal(REGION + " " + CITY + "\n" + STREET,
+                STREET, null, null, CITY, REGION, null, null);
+
+        assertJoinedPostal(COUNTRY + "\n" + REGION + " " + CITY + "\n" + STREET,
+                STREET, null, null, CITY, REGION, null, COUNTRY);
+
+        assertJoinedPostal(COUNTRY + "\n" + STREET,
+                STREET, null, null, null, null, null, COUNTRY);
+    }
+
+    public void testMissingStreet() {
+        assertJoinedPostal(COUNTRY + "\n" + STREET,
+                null, null, STREET, null, null, null, COUNTRY);
+    }
+
+    private void assertSplitPostal(String formattedPostal, String street, String pobox,
+            String neighborhood, String city, String region, String postcode, String country) {
+        final Postal postal = new Postal();
+        mPostalSplitter.split(postal, formattedPostal);
+        assertEquals(street, postal.street);
+        assertEquals(pobox, postal.pobox);
+        assertEquals(neighborhood, postal.neighborhood);
+        assertEquals(city, postal.city);
+        assertEquals(region, postal.region);
+        assertEquals(postcode, postal.postcode);
+        assertEquals(country, postal.country);
+    }
+
+    private void assertJoinedPostal(String formattedPostal, String street, String pobox,
+            String neighborhood, String city, String region, String postcode, String country) {
+        final Postal postal = new Postal();
+        postal.street = street;
+        postal.pobox = pobox;
+        postal.neighborhood = neighborhood;
+        postal.city = city;
+        postal.region = region;
+        postal.postcode = postcode;
+        postal.country = country;
+
+        final String joined = mPostalSplitter.join(postal);
+        assertEquals(formattedPostal, joined);
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/PostalSplitterTest.java b/tests/src/com/android/providers/contacts/PostalSplitterTest.java
index 52dbc28..6778b79 100644
--- a/tests/src/com/android/providers/contacts/PostalSplitterTest.java
+++ b/tests/src/com/android/providers/contacts/PostalSplitterTest.java
@@ -25,7 +25,7 @@
 import junit.framework.TestCase;
 
 /**
- * Tests for {@link PostalSplitter}.
+ * Tests for {@link PostalSplitter}, especially for en_US locale.
  *
  * Run the test like this:
  * <code>
diff --git a/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
index 7928d22..dd7c626 100644
--- a/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
+++ b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
@@ -225,13 +225,15 @@
         final long greyContact = mGrey.createContact(true, GENERIC_NAME);
         final long greyPhone = mGrey.createPhone(greyContact, PHONE_GREY);
 
-        final Uri greyUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, greyContact);
-        final Uri redUri = greyUri.buildUpon().appendQueryParameter(
+        Uri greyUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, greyContact);
+        greyUri = Uri.withAppendedPath(greyUri, RawContacts.Entity.CONTENT_DIRECTORY);
+        Uri redUri = greyUri.buildUpon().appendQueryParameter(
                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY, mRed.packageName).build();
 
         // When calling normally, we have access to protected
         mGrey.ensureCallingPackage();
-        EntityIterator iterator = mGrey.resolver.queryEntities(greyUri, null, null, null);
+        EntityIterator iterator = RawContacts.newEntityIterator(
+                mGrey.resolver.query(greyUri, null, null, null, null));
         while (iterator.hasNext()) {
             final Entity entity = iterator.next();
             final long rawContactId = entity.getEntityValues().getAsLong(RawContacts._ID);
@@ -240,7 +242,8 @@
 
         // When calling on behalf of another package, protected is omitted
         mGrey.ensureCallingPackage();
-        iterator = mGrey.resolver.queryEntities(redUri, null, null, null);
+        iterator = RawContacts.newEntityIterator(
+                mGrey.resolver.query(redUri, null, null, null, null));
         while (iterator.hasNext()) {
             final Entity entity = iterator.next();
             final long rawContactId = entity.getEntityValues().getAsLong(RawContacts._ID);
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index f5a3572..865a4c9 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -20,6 +20,8 @@
 import android.content.Context;
 import android.database.sqlite.SQLiteDatabase;
 
+import java.util.Locale;
+
 /**
  * A version of {@link ContactsProvider2} class that performs aggregation
  * synchronously and wipes all data at construction time.
@@ -31,14 +33,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) {
@@ -97,6 +91,11 @@
     }
 
     @Override
+    protected Locale getLocale() {
+        return Locale.US;
+    }
+
+    @Override
     protected boolean isWritableAccount(Account account) {
         return true;
     }
@@ -135,31 +134,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();
-        }
-
-    }
 }