aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commit0f58cfe01bb0f491330f7dcf85d54d0081459b57 (patch)
treec27bb62da8008501f3ccfd2f35d7afb50f8a5937
downloadContactsProvider-release-1.0.tar.gz
-rw-r--r--Android.mk13
-rw-r--r--AndroidManifest.xml21
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--NOTICE190
-rw-r--r--res/values/strings.xml22
-rw-r--r--src/com/android/providers/contacts/ContactsProvider.java3998
6 files changed, 4244 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 00000000..8b34359f
--- /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 00000000..795a5c88
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<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">
+ <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 00000000..e69de29b
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 00000000..c5b1efa7
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 00000000..a0f8cb0c
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?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">
+ <!-- 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 00000000..f2a9d8ef
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactsProvider.java
@@ -0,0 +1,3998 @@
+/*
+ * 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 com.android.internal.database.ArrayListCursor;
+import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
+
+import android.app.SearchManager;
+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.SyncableContentProvider;
+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.Contacts;
+import android.provider.SyncConstValue;
+import android.provider.CallLog.Calls;
+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.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+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 SyncableContentProvider {
+ 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 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 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);
+ 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;
+ }
+
+ 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" +
+ ");");
+
+ 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,"
+ + 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," +
+ "_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," +
+ "_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_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;
+
+ 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;
+
+ // TODO: the following commented-out code is being replaced with code that ignores
+ // the account.
+ // This is a short-term fix that is known not to work with multiple-accounts but is
+ // sufficient for V1 and safer than the real fix. This code must not be merged back into
+ // the trunk, which already contains the real fix.
+// 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};
+// }
+ whereString = "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);
+ 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);
+ 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
+ mValues.clear();
+ mValues.put(Photos.PERSON_ID, rowID);
+ mValues.put(Photos._SYNC_ACCOUNT, account);
+ mValues.put(Photos._SYNC_ID, syncId);
+ mDb.delete(sPhotosTable, "person=" + rowID, null);
+ 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;
+ }
+
+ 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);
+ // 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()) {
+ // insert into deleted table
+ mDeletedPeopleInserter.prepareForInsert();
+ mDeletedPeopleInserter.bind(dstIdxSyncId, cursor.getString(idxSyncId));
+ 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);
+ if (!mValues.containsKey(Photos._SYNC_DIRTY)) 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.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.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 = 78;
+
+ 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 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 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 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> 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 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_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 ";
+
+ 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/#/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/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);
+
+ // 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.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_ID);
+ map.put(Phones.ISPRIMARY, Phones.ISPRIMARY);
+ map.putAll(peopleColumns);
+ sPhonesProjectionMap = 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;
+
+ // 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 + ")";
+ }
+}
+