auto import from //depot/cupcake/@135843
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..8b34359
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,13 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := user development
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_JAVA_LIBRARIES := ext
+
+LOCAL_PACKAGE_NAME := ContactsProvider
+LOCAL_CERTIFICATE := shared
+
+include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..0194108
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.providers.contacts"
+        android:sharedUserId="android.uid.shared">
+
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.READ_SYNC_STATS" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
+    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.cp" />
+    <uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_READ" />
+    <uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_WRITE" />
+
+    <application android:process="android.process.acore"
+                 android:label="@string/app_label"
+                 android:icon="@drawable/app_icon">
+        <provider android:name="ContactsProvider" android:authorities="contacts;call_log"
+                android:syncable="false" android:multiprocess="false"
+                android:readPermission="android.permission.READ_CONTACTS"
+                android:writePermission="android.permission.WRITE_CONTACTS" />
+    </application>
+</manifest>
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/res/drawable/app_icon.png b/res/drawable/app_icon.png
new file mode 100644
index 0000000..826656f
--- /dev/null
+++ b/res/drawable/app_icon.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..799a73c
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- This is the label for the application that stores contacts data -->
+    <string name="app_label">Contacts Storage</string>
+
+    <!-- Strings for search suggestions -->
+    <string name="dialNumber">Dial number</string>
+    <string name="createNewContact">New contact</string>
+    <string name="usingNumber">Using <xliff:g id="number">%s</xliff:g></string>    
+</resources>
diff --git a/src/com/android/providers/contacts/ContactsProvider.java b/src/com/android/providers/contacts/ContactsProvider.java
new file mode 100644
index 0000000..209a98c
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactsProvider.java
@@ -0,0 +1,4121 @@
+/*
+ * 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.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 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;
+
+        // 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 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_JOIN);
+                qb.setProjectionMap(sPeopleWithMaxTimesContactedProjectionMap);
+                final String starredQuery = qb.buildQuery(projectionIn, "starred = 1",
+                        null, null, null, null,
+                        null /* limit */);
+
+                qb = new SQLiteQueryBuilder();
+                qb.setTables(PEOPLE_PHONES_JOIN);
+                qb.setProjectionMap(sPeopleProjectionMap);
+                final String frequentQuery = qb.buildQuery(projectionIn,
+                        "times_contacted > 0 AND starred = 0", null, null, null, null, null);
+
+                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, "people");
+                if ((c != null) && !isTemporary()) {
+                    c.setNotificationUri(getContext().getContentResolver(), url);
+                }
+                return c;
+            }
+            case PEOPLE_STREQUENT_FILTER: {
+                // Build the first query for starred
+                qb.setTables(PEOPLE_PHONES_JOIN);
+                qb.setProjectionMap(sPeopleWithMaxTimesContactedProjectionMap);
+                if (url.getPathSegments().size() > 3) {
+                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
+                }
+                qb.appendWhere(" AND starred = 1");
+                final String starredQuery = qb.buildQuery(projectionIn, null, null, null, null,
+                        null, null);
+
+                qb = new SQLiteQueryBuilder();
+                qb.setTables(PEOPLE_PHONES_JOIN);
+                qb.setProjectionMap(sPeopleProjectionMap);
+                if (url.getPathSegments().size() > 3) {
+                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
+                }
+                qb.appendWhere(" AND times_contacted > 0 AND starred = 0");
+                final String frequentQuery = qb.buildQuery(projectionIn, null, null, null, null,
+                        null, null);
+
+                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(), url);
+                }
+                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);
+                StringBuilder 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,
+                null, 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,
+                };
+
+/*
+ *  TODO: figure out how to localize things so myFaves can read the constants when sub classing
+ */
+                ArrayList dialNumber = new ArrayList();
+                dialNumber.add("Dial number");
+                dialNumber.add("Using " + searchClause);
+                dialNumber.add("tel:" + searchClause);
+                dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
+
+                ArrayList createContact = new ArrayList();
+                createContact.add("Create contact");
+                createContact.add("Using " + searchClause);
+                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 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;
+    /** Used to force items to the top of a times_contacted list */
+    private static final HashMap<String, String> sPeopleWithMaxTimesContactedProjectionMap;
+    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[] 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/#", 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);
+
+        // 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;
+
+        // 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);
+        sPeopleWithMaxTimesContactedProjectionMap = 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, 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);
+        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 + ")";
+    }
+}