diff options
-rw-r--r-- | Android.mk | 13 | ||||
-rw-r--r-- | AndroidManifest.xml | 23 | ||||
-rw-r--r-- | MODULE_LICENSE_APACHE2 | 0 | ||||
-rw-r--r-- | NOTICE | 190 | ||||
-rw-r--r-- | res/drawable/app_icon.png | bin | 0 -> 2995 bytes | |||
-rw-r--r-- | res/values/strings.xml | 26 | ||||
-rw-r--r-- | src/com/android/providers/contacts/ContactsProvider.java | 4121 |
7 files changed, 4373 insertions, 0 deletions
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 @@ -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 Binary files differnew file mode 100644 index 0000000..826656f --- /dev/null +++ b/res/drawable/app_icon.png 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 + ")"; + } +} |