FileDocCategorySizeDatePackage
ContactsProvider.javaAPI DocAndroid 1.5 API204145Wed May 06 22:42:48 BST 2009com.android.providers.contacts

ContactsProvider.java

/*
 * 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.Resources;
import android.database.Cursor;
import android.database.CursorJoiner;
import android.database.DatabaseUtils;
import android.database.SQLException;
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.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.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Config;
import android.util.Log;

import com.android.internal.database.ArrayListCursor;
import com.google.android.collect.Maps;
import com.google.android.collect.Sets;

import java.io.FileNotFoundException;
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 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 String[] mAccounts = new String[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 mIndexPeopleSyncAccount;
    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 mIndexPhotosSyncAccount;
    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 mIndexGroupMembershipGroupSyncAccount;
    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;

    public ContactsProvider() {
        super(DATABASE_NAME, DATABASE_VERSION, Contacts.CONTENT_URI);
    }

    @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);
        mIndexPeopleSyncAccount = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT);
        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);
        mIndexPhotosSyncAccount = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT);
        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);
        mIndexGroupMembershipGroupSyncAccount = mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT);
        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);
            getContext().getContentResolver().startSync(Contacts.CONTENT_URI, 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;
        }

        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_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_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 + ")" +
                ");");

        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)" +
                    ");");
        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_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_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_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," +
                    "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_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_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_version) " +
                            "VALUES (old._sync_id, old._sync_account, " +
                            "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, ' ');" +
                    "END");
        db.execSQL("CREATE TRIGGER peopleLookup_insert AFTER INSERT ON people " +
                    "BEGIN " +
                        "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ');" +
                    "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;
        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("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.NAME + "="
                        + DatabaseUtils.sqlEscapeString(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("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(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 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(")");
                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(")");
                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("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.NAME + "="
                        + DatabaseUtils.sqlEscapeString(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);
        if ((c != null) && !isTemporary()) {
            c.setNotificationUri(getContext().getContentResolver(), notificationUri);
        }
        return c;
    }

    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");
        qb.setProjectionMap(sSearchSuggestionsProjectionMap);
        if (url.getPathSegments().size() > 1) {
            // A search term was entered, use it to filter
            final String searchClause = url.getLastPathSegment();
            if (!TextUtils.isDigitsOnly(searchClause)) {
                qb.appendWhere(buildPeopleLookupWhereClause(searchClause));
            } else {
                final String[] columnNames = new String[] {
                        SearchManager.SUGGEST_COLUMN_TEXT_1,
                        SearchManager.SUGGEST_COLUMN_TEXT_2,
                        SearchManager.SUGGEST_COLUMN_INTENT_DATA,
                        SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
                };

                Resources r = getContext().getResources();
                String s;
                int i;

                ArrayList dialNumber = new ArrayList();
                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("tel:" + searchClause);
                dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);  

                ArrayList createContact = new ArrayList();
                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("tel:" + searchClause);
                createContact.add(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);

                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 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";
            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(String account) {
        String whereString;
        String[] whereArgs;
        if (!TextUtils.isEmpty(account)) {
            whereString = "_sync_account=? AND name=?";
            whereArgs = new String[]{account, 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},
                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));
                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 account = newMap.getAsString(Groups._SYNC_ACCOUNT);
                        if (!TextUtils.isEmpty(account)) {
                            final ContentResolver cr = getContext().getContentResolver();
                            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 account = mValues.getAsString(People._SYNC_ACCOUNT);
                        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, account);
                        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(String[] accountsArray) {
        super.onAccountsChanged(accountsArray);
        synchronized (mAccountsLock) {
            mAccounts = new String[accountsArray.length];
            System.arraycopy(accountsArray, 0, mAccounts, 0, mAccounts.length);
        }
    }

    private void ensureSyncAccountIsSet(ContentValues values) {
        synchronized (mAccountsLock) {
            String account = values.getAsString(SyncConstValue._SYNC_ACCOUNT);
            if (account == null && mAccounts.length > 0) {
                values.put(SyncConstValue._SYNC_ACCOUNT, mAccounts[0]);
            }
        }
    }

    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 groupSyncAccount = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT);
        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(groupSyncAccount) || TextUtils.isEmpty(groupSyncId)) {
                throw new IllegalArgumentException(
                        "insertIntoGroupmembership: no GROUP_ID wasn't specified and non-empty "
                        + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT fields weren't specifid, "
                        + values);
            }
            if (0 != DatabaseUtils.longForQuery(getDatabase(), ""
                    + "SELECT COUNT(*) "
                    + "FROM groupmembership "
                    + "WHERE group_sync_id=? AND person=?",
                    new String[]{groupSyncId, 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(groupSyncAccount) || !TextUtils.isEmpty(groupSyncId)) {
                throw new IllegalArgumentException(
                        "insertIntoGroupmembership: GROUP_ID was specified but "
                        + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT 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(String 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);
                long groupId = mGroupsInserter.insert(mValuesLocal);
                starredGroupInfo = new ContentValues();
                starredGroupInfo.put(Groups._ID, groupId);
                starredGroupInfo.put(Groups._SYNC_ACCOUNT, account);
                // 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 syncAccount = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);

            // 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, syncAccount);
            mGroupMembershipInserter.insert(mValuesLocal);
        } else {
            if (starredGroupInfo != null) {
                // delete the groupmembership rows for this person that match the starred group id
                String syncAccount = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
                String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID);
                if (!TextUtils.isEmpty(syncId)) {
                    mDb.delete(sGroupmembershipTable,
                            "person=? AND group_sync_id=? AND group_sync_account=?",
                            new String[]{String.valueOf(personId), syncId, syncAccount});
                } 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 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 idxSyncAccount = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
            final int idxSyncVersion = cursor.getColumnIndexOrThrow(People._SYNC_VERSION);
            final int dstIdxSyncId = mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ID);
            final int dstIdxSyncAccount =
                    mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ACCOUNT);
            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(dstIdxSyncAccount, cursor.getString(idxSyncAccount));
                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<String> 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 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 syncAccount = cursor.getString(indexSyncAccount);
                String syncId = cursor.getString(indexSyncId);
                boolean shouldSync = cursor.getLong(indexShouldSync) != 0;
                long id = cursor.getLong(indexId);
                fixupPeopleStarredOnGroupRename(oldName, null, id);
                if (!TextUtils.isEmpty(syncAccount) && !TextUtils.isEmpty(syncId)) {
                    fixupPeopleStarredOnGroupRename(oldName, null, syncAccount, syncId);
                }
                if (!TextUtils.isEmpty(syncAccount) && shouldSync) {
                    modifiedAccounts.add(syncAccount);
                }
            }
        } finally {
            cursor.close();
        }

        int numRows = mDb.delete(sGroupsTable, where, whereArgs);
        if (numRows > 0) {
            if (!isTemporary()) {
                final ContentResolver cr = getContext().getContentResolver();
                for (String 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, String 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 indexAccount = c.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
                            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) {
                                    fixupGroupMembershipAfterPeopleUpdate(c.getString(indexAccount),
                                            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 account = values.getAsString(Contacts.Settings._SYNC_ACCOUNT);
        final String key = values.getAsString(Contacts.Settings.KEY);
        if (key == null) {
            throw new IllegalArgumentException("you must specify the key when updating settings");
        }
        if (account == null) {
            db.delete(sSettingsTable, "_sync_account IS NULL AND key=?", new String[]{key});
        } else {
            if (TextUtils.isEmpty(account)) {
                throw new IllegalArgumentException("account cannot be the empty string, " + values);
            }
            db.delete(sSettingsTable, "_sync_account=? AND key=?", new String[]{account, 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<String> 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 indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID);
                final int indexId = cursor.getColumnIndexOrThrow(Groups._ID);
                while (cursor.moveToNext()) {
                    String syncAccount = cursor.getString(indexSyncAccount);
                    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(syncAccount) && !TextUtils.isEmpty(syncId)) {
                            fixupPeopleStarredOnGroupRename(oldName, newName, syncAccount, syncId);
                        }
                    }
                    if (!TextUtils.isEmpty(syncAccount) && values.containsKey(Groups.SHOULD_SYNC)) {
                        modifiedAccounts.add(syncAccount);
                    }
                }
            } finally {
                cursor.close();
            }
        }

        int numRows = db.update(sGroupsTable, values, where, whereArgs);
        if (numRows > 0) {
            if (!isTemporary()) {
                final ContentResolver cr = getContext().getContentResolver();
                for (String 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,
            String syncAccount, String syncId) {
        fixupPeopleStarredOnGroupRename(oldName, newName, "_sync_account=? AND _sync_id=?",
                new String[]{syncAccount, 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, mIndexPeopleSyncAccount);
            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, mIndexPhotosSyncAccount);
                    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 accountIndex =
                    c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ACCOUNT);
                final int idIndex = c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ID);
                while(c.moveToNext()) {
                    mGroupMembershipInserter.prepareForInsert();
                    mGroupMembershipInserter.bind(mIndexGroupMembershipPersonId, localPersonID);
                    mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccount, c.getString(accountIndex));
                    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_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);
                    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.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.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 account = mValues.getAsString(Groups._SYNC_ACCOUNT);
                onLocalChangesForAccount(cr, account, false);
            }

            String oldName = null;
            String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME));
            String account = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
            String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
            // this must come after the insert, otherwise the join won't work
            fixupPeopleStarredOnGroupRename(oldName, newName, account, 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 account = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
            String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
            // this can come before or after the delete
            fixupPeopleStarredOnGroupRename(oldName, newName, account, 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.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 account = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
                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, account, 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 = 80;

    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 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 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[] 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/#/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, "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);

        // 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_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_INTENT_DATA_ID,
                People._ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
        map.put(People._ID, 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);

        sPhonesKeyColumns = new String[]{Phones.NUMBER};
        sContactMethodsKeyColumns = new String[]{ContactMethods.DATA, ContactMethods.KIND};
        sOrganizationsKeyColumns = new String[]{Organizations.COMPANY};
        sGroupmembershipKeyColumns = new String[]{GroupMembership.GROUP_SYNC_ACCOUNT};
        sExtensionsKeyColumns = new String[]{Extensions.NAME};

        String groupJoinByLocalId = "groups._id=groupmembership.group_id";
        String groupJoinByServerId = "("
                + "groups._sync_account=groupmembership.group_sync_account"
                + " AND "
                + "groups._sync_id=groupmembership.group_sync_id"
                + ")";
        sGroupsJoinString = "(" + groupJoinByLocalId + " OR " + groupJoinByServerId + ")";
    }
}