1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.providers.contacts;
18
19import android.app.SearchManager;
20import android.content.AbstractSyncableContentProvider;
21import android.content.AbstractTableMerger;
22import android.content.ContentProvider;
23import android.content.ContentResolver;
24import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.SharedPreferences;
28import android.content.UriMatcher;
29import android.content.res.AssetFileDescriptor;
30import android.content.res.Resources;
31import android.database.Cursor;
32import android.database.CursorJoiner;
33import android.database.DatabaseUtils;
34import android.database.SQLException;
35import android.database.sqlite.SQLiteContentHelper;
36import android.database.sqlite.SQLiteCursor;
37import android.database.sqlite.SQLiteDatabase;
38import android.database.sqlite.SQLiteDoneException;
39import android.database.sqlite.SQLiteException;
40import android.database.sqlite.SQLiteQueryBuilder;
41import android.database.sqlite.SQLiteStatement;
42import android.net.Uri;
43import android.os.Bundle;
44import android.os.MemoryFile;
45import android.os.ParcelFileDescriptor;
46import android.provider.CallLog;
47import android.provider.Contacts;
48import android.provider.LiveFolders;
49import android.provider.SyncConstValue;
50import android.provider.CallLog.Calls;
51import android.provider.Contacts.ContactMethods;
52import android.provider.Contacts.Extensions;
53import android.provider.Contacts.GroupMembership;
54import android.provider.Contacts.Groups;
55import android.provider.Contacts.GroupsColumns;
56import android.provider.Contacts.Intents;
57import android.provider.Contacts.Organizations;
58import android.provider.Contacts.People;
59import android.provider.Contacts.PeopleColumns;
60import android.provider.Contacts.Phones;
61import android.provider.Contacts.Photos;
62import android.provider.Contacts.Presence;
63import android.provider.Contacts.PresenceColumns;
64import android.telephony.PhoneNumberUtils;
65import android.text.TextUtils;
66import android.util.Config;
67import android.util.Log;
68
69import com.google.android.collect.Maps;
70import com.google.android.collect.Sets;
71
72import com.android.internal.database.ArrayListCursor;
73
74import java.io.FileNotFoundException;
75import java.io.IOException;
76import java.util.ArrayList;
77import java.util.HashMap;
78import java.util.HashSet;
79import java.util.Locale;
80import java.util.Map;
81import java.util.Set;
82
83public class ContactsProvider extends AbstractSyncableContentProvider {
84    private static final String STREQUENT_ORDER_BY = "times_contacted DESC, display_name ASC";
85    private static final String STREQUENT_LIMIT =
86            "(SELECT COUNT(*) FROM people WHERE starred = 1) + 25";
87
88    private static final String PEOPLE_PHONES_JOIN =
89            "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
90            + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id)";
91
92    private static final String PEOPLE_PHONES_PHOTOS_JOIN =
93            "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
94            + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id) "
95            + "LEFT OUTER JOIN photos ON (photos." + Photos.PERSON_ID + "=people._id)";
96
97    private static final String PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN =
98            "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
99            + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id) "
100            + "LEFT OUTER JOIN photos ON (photos." + Photos.PERSON_ID + "=people._id) "
101            + "LEFT OUTER JOIN organizations ON (organizations._id=people.primary_organization)";
102
103    private static final String GTALK_PROTOCOL_STRING =
104            ContactMethods.encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
105
106    private static final String[] ID_TYPE_PROJECTION = new String[]{"_id", "type"};
107
108    private static final String[] sIsPrimaryProjectionWithoutKind =
109            new String[]{"isprimary", "person", "_id"};
110    private static final String[] sIsPrimaryProjectionWithKind =
111            new String[]{"isprimary", "person", "_id", "kind"};
112
113    private static final String WHERE_ID = "_id=?";
114
115    private static final String sGroupsJoinString;
116
117    private static final String PREFS_NAME_OWNER = "owner-info";
118    private static final String PREF_OWNER_ID = "owner-id";
119
120    /** this is suitable for use by insert/update/delete/query and may be passed
121     * as a method call parameter. Only insert/update/delete/query should call .clear() on it */
122    private final ContentValues mValues = new ContentValues();
123
124    /** this is suitable for local use in methods and should never be passed as a parameter to
125     * other methods (other than the DB layer) */
126    private final ContentValues mValuesLocal = new ContentValues();
127
128    private String[] mAccounts = new String[0];
129    private final Object mAccountsLock = new Object();
130
131    private DatabaseUtils.InsertHelper mDeletedPeopleInserter;
132    private DatabaseUtils.InsertHelper mPeopleInserter;
133    private int mIndexPeopleSyncId;
134    private int mIndexPeopleSyncTime;
135    private int mIndexPeopleSyncVersion;
136    private int mIndexPeopleSyncDirty;
137    private int mIndexPeopleSyncAccount;
138    private int mIndexPeopleName;
139    private int mIndexPeoplePhoneticName;
140    private int mIndexPeopleNotes;
141    private DatabaseUtils.InsertHelper mGroupsInserter;
142    private DatabaseUtils.InsertHelper mPhotosInserter;
143    private int mIndexPhotosPersonId;
144    private int mIndexPhotosSyncId;
145    private int mIndexPhotosSyncTime;
146    private int mIndexPhotosSyncVersion;
147    private int mIndexPhotosSyncDirty;
148    private int mIndexPhotosSyncAccount;
149    private int mIndexPhotosExistsOnServer;
150    private int mIndexPhotosSyncError;
151    private DatabaseUtils.InsertHelper mContactMethodsInserter;
152    private int mIndexContactMethodsPersonId;
153    private int mIndexContactMethodsLabel;
154    private int mIndexContactMethodsKind;
155    private int mIndexContactMethodsType;
156    private int mIndexContactMethodsData;
157    private int mIndexContactMethodsAuxData;
158    private int mIndexContactMethodsIsPrimary;
159    private DatabaseUtils.InsertHelper mOrganizationsInserter;
160    private int mIndexOrganizationsPersonId;
161    private int mIndexOrganizationsLabel;
162    private int mIndexOrganizationsType;
163    private int mIndexOrganizationsCompany;
164    private int mIndexOrganizationsTitle;
165    private int mIndexOrganizationsIsPrimary;
166    private DatabaseUtils.InsertHelper mExtensionsInserter;
167    private int mIndexExtensionsPersonId;
168    private int mIndexExtensionsName;
169    private int mIndexExtensionsValue;
170    private DatabaseUtils.InsertHelper mGroupMembershipInserter;
171    private int mIndexGroupMembershipPersonId;
172    private int mIndexGroupMembershipGroupSyncAccount;
173    private int mIndexGroupMembershipGroupSyncId;
174    private DatabaseUtils.InsertHelper mCallsInserter;
175    private DatabaseUtils.InsertHelper mPhonesInserter;
176    private int mIndexPhonesPersonId;
177    private int mIndexPhonesLabel;
178    private int mIndexPhonesType;
179    private int mIndexPhonesNumber;
180    private int mIndexPhonesNumberKey;
181    private int mIndexPhonesIsPrimary;
182
183    private static HashMap<String, String> mSearchSuggestionsProjectionMap;
184    private static String mSearchSuggestionLanguage;
185
186    public ContactsProvider() {
187        super(DATABASE_NAME, DATABASE_VERSION, Contacts.CONTENT_URI);
188        mSearchSuggestionLanguage = Locale.getDefault().getLanguage();
189        // Search suggestions projection map
190        mSearchSuggestionsProjectionMap = new HashMap<String, String>();
191        updateSuggestColumnTexts();
192        mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_ICON_1,
193                "(CASE WHEN " + Photos.DATA + " IS NOT NULL"
194                + " THEN '" + People.CONTENT_URI + "/' || people._id ||"
195                        + " '/" + Photos.CONTENT_DIRECTORY + "/data'"
196                + " ELSE " + com.android.internal.R.drawable.ic_contact_picture
197                + " END) AS " + SearchManager.SUGGEST_COLUMN_ICON_1);
198        mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_ICON_2,
199                PRESENCE_ICON_SQL + " AS " + SearchManager.SUGGEST_COLUMN_ICON_2);
200        mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
201                "people._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
202        mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
203                "people._id AS " + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
204        mSearchSuggestionsProjectionMap.put(People._ID, "people._id AS " + People._ID);
205    }
206
207    @Override
208    protected void onDatabaseOpened(SQLiteDatabase db) {
209        maybeCreatePresenceTable(db);
210
211        // Mark all the tables as syncable
212        db.markTableSyncable(sPeopleTable, sDeletedPeopleTable);
213        db.markTableSyncable(sPhonesTable, Phones.PERSON_ID, sPeopleTable);
214        db.markTableSyncable(sContactMethodsTable, ContactMethods.PERSON_ID, sPeopleTable);
215        db.markTableSyncable(sOrganizationsTable, Organizations.PERSON_ID, sPeopleTable);
216        db.markTableSyncable(sGroupmembershipTable, GroupMembership.PERSON_ID, sPeopleTable);
217        db.markTableSyncable(sExtensionsTable, Extensions.PERSON_ID, sPeopleTable);
218        db.markTableSyncable(sGroupsTable, sDeletedGroupsTable);
219
220        mDeletedPeopleInserter = new DatabaseUtils.InsertHelper(db, sDeletedPeopleTable);
221        mPeopleInserter = new DatabaseUtils.InsertHelper(db, sPeopleTable);
222        mIndexPeopleSyncId = mPeopleInserter.getColumnIndex(People._SYNC_ID);
223        mIndexPeopleSyncTime = mPeopleInserter.getColumnIndex(People._SYNC_TIME);
224        mIndexPeopleSyncVersion = mPeopleInserter.getColumnIndex(People._SYNC_VERSION);
225        mIndexPeopleSyncDirty = mPeopleInserter.getColumnIndex(People._SYNC_DIRTY);
226        mIndexPeopleSyncAccount = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT);
227        mIndexPeopleName = mPeopleInserter.getColumnIndex(People.NAME);
228        mIndexPeoplePhoneticName = mPeopleInserter.getColumnIndex(People.PHONETIC_NAME);
229        mIndexPeopleNotes = mPeopleInserter.getColumnIndex(People.NOTES);
230
231        mGroupsInserter = new DatabaseUtils.InsertHelper(db, sGroupsTable);
232
233        mPhotosInserter = new DatabaseUtils.InsertHelper(db, sPhotosTable);
234        mIndexPhotosPersonId = mPhotosInserter.getColumnIndex(Photos.PERSON_ID);
235        mIndexPhotosSyncId = mPhotosInserter.getColumnIndex(Photos._SYNC_ID);
236        mIndexPhotosSyncTime = mPhotosInserter.getColumnIndex(Photos._SYNC_TIME);
237        mIndexPhotosSyncVersion = mPhotosInserter.getColumnIndex(Photos._SYNC_VERSION);
238        mIndexPhotosSyncDirty = mPhotosInserter.getColumnIndex(Photos._SYNC_DIRTY);
239        mIndexPhotosSyncAccount = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT);
240        mIndexPhotosSyncError = mPhotosInserter.getColumnIndex(Photos.SYNC_ERROR);
241        mIndexPhotosExistsOnServer = mPhotosInserter.getColumnIndex(Photos.EXISTS_ON_SERVER);
242
243        mContactMethodsInserter = new DatabaseUtils.InsertHelper(db, sContactMethodsTable);
244        mIndexContactMethodsPersonId = mContactMethodsInserter.getColumnIndex(ContactMethods.PERSON_ID);
245        mIndexContactMethodsLabel = mContactMethodsInserter.getColumnIndex(ContactMethods.LABEL);
246        mIndexContactMethodsKind = mContactMethodsInserter.getColumnIndex(ContactMethods.KIND);
247        mIndexContactMethodsType = mContactMethodsInserter.getColumnIndex(ContactMethods.TYPE);
248        mIndexContactMethodsData = mContactMethodsInserter.getColumnIndex(ContactMethods.DATA);
249        mIndexContactMethodsAuxData = mContactMethodsInserter.getColumnIndex(ContactMethods.AUX_DATA);
250        mIndexContactMethodsIsPrimary = mContactMethodsInserter.getColumnIndex(ContactMethods.ISPRIMARY);
251
252        mOrganizationsInserter = new DatabaseUtils.InsertHelper(db, sOrganizationsTable);
253        mIndexOrganizationsPersonId = mOrganizationsInserter.getColumnIndex(Organizations.PERSON_ID);
254        mIndexOrganizationsLabel = mOrganizationsInserter.getColumnIndex(Organizations.LABEL);
255        mIndexOrganizationsType = mOrganizationsInserter.getColumnIndex(Organizations.TYPE);
256        mIndexOrganizationsCompany = mOrganizationsInserter.getColumnIndex(Organizations.COMPANY);
257        mIndexOrganizationsTitle = mOrganizationsInserter.getColumnIndex(Organizations.TITLE);
258        mIndexOrganizationsIsPrimary = mOrganizationsInserter.getColumnIndex(Organizations.ISPRIMARY);
259
260        mExtensionsInserter = new DatabaseUtils.InsertHelper(db, sExtensionsTable);
261        mIndexExtensionsPersonId = mExtensionsInserter.getColumnIndex(Extensions.PERSON_ID);
262        mIndexExtensionsName = mExtensionsInserter.getColumnIndex(Extensions.NAME);
263        mIndexExtensionsValue = mExtensionsInserter.getColumnIndex(Extensions.VALUE);
264
265        mGroupMembershipInserter = new DatabaseUtils.InsertHelper(db, sGroupmembershipTable);
266        mIndexGroupMembershipPersonId = mGroupMembershipInserter.getColumnIndex(GroupMembership.PERSON_ID);
267        mIndexGroupMembershipGroupSyncAccount = mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT);
268        mIndexGroupMembershipGroupSyncId = mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ID);
269
270        mCallsInserter = new DatabaseUtils.InsertHelper(db, sCallsTable);
271
272        mPhonesInserter = new DatabaseUtils.InsertHelper(db, sPhonesTable);
273        mIndexPhonesPersonId = mPhonesInserter.getColumnIndex(Phones.PERSON_ID);
274        mIndexPhonesLabel = mPhonesInserter.getColumnIndex(Phones.LABEL);
275        mIndexPhonesType = mPhonesInserter.getColumnIndex(Phones.TYPE);
276        mIndexPhonesNumber = mPhonesInserter.getColumnIndex(Phones.NUMBER);
277        mIndexPhonesNumberKey = mPhonesInserter.getColumnIndex(Phones.NUMBER_KEY);
278        mIndexPhonesIsPrimary = mPhonesInserter.getColumnIndex(Phones.ISPRIMARY);
279    }
280
281    @Override
282    protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
283        boolean upgradeWasLossless = true;
284        if (oldVersion < 71) {
285            Log.w(TAG, "Upgrading database from version " + oldVersion + " to " +
286                    newVersion + ", which will destroy all old data");
287            dropTables(db);
288            bootstrapDatabase(db);
289            return false; // this was lossy
290        }
291        if (oldVersion == 71) {
292            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
293                    newVersion + ", which will preserve existing data");
294
295            db.delete("_sync_state", null, null);
296            mValuesLocal.clear();
297            mValuesLocal.putNull(Photos._SYNC_VERSION);
298            mValuesLocal.putNull(Photos._SYNC_TIME);
299            db.update(sPhotosTable, mValuesLocal, null, null);
300            getContext().getContentResolver().startSync(Contacts.CONTENT_URI, new Bundle());
301            oldVersion = 72;
302        }
303        if (oldVersion == 72) {
304            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
305                    newVersion + ", which will preserve existing data");
306
307            // use new token format from 73
308            db.execSQL("delete from peopleLookup");
309            try {
310                // With longForQuery(), _TOKERNIZE() is called just once, toward the first entry
311                // in "people" table. This may be a bug. Instead, we use rawQuery() for now.
312                // DatabaseUtils.longForQuery(db, query, null);
313
314                // Cursors objects are lazily executed. So we have to call some method which forces
315                // the cursor to run the query.
316                Cursor cursor =
317                    db.rawQuery("SELECT _TOKENIZE('peopleLookup', _id, name, ' ') from people",
318                            null);
319                try {
320                    int rows = cursor.getCount();
321                    Log.i(TAG, "Processed " + rows + " contacts.");
322                } finally {
323                    if (cursor != null) {
324                        cursor.close();
325                    }
326                }
327            } catch (SQLiteDoneException ex) {
328                // it is ok to throw this,
329                // it just means you don't have data in people table
330            }
331            oldVersion = 73;
332        }
333        // There was a bug for a while in the upgrade logic where going from 72 to 74 would skip
334        // the step from 73 to 74, so 74 to 75 just tries the same steps, and gracefully handles
335        // errors in case the device was started freshly at 74.
336        if (oldVersion == 73 || oldVersion == 74) {
337            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
338                    newVersion + ", which will preserve existing data");
339
340            try {
341                db.execSQL("ALTER TABLE calls ADD name TEXT;");
342                db.execSQL("ALTER TABLE calls ADD numbertype INTEGER;");
343                db.execSQL("ALTER TABLE calls ADD numberlabel TEXT;");
344            } catch (SQLiteException sqle) {
345                // Maybe the table was altered already... Shouldn't be an issue.
346            }
347            oldVersion = 75;
348        }
349        // There were some indices added in version 76
350        if (oldVersion == 75) {
351            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
352                    newVersion + ", which will preserve existing data");
353
354            // add the new indices
355            db.execSQL("CREATE INDEX IF NOT EXISTS groupsSyncDirtyIndex"
356                    + " ON groups (" + Groups._SYNC_DIRTY + ");");
357            db.execSQL("CREATE INDEX IF NOT EXISTS photosSyncDirtyIndex"
358                    + " ON photos (" + Photos._SYNC_DIRTY + ");");
359            db.execSQL("CREATE INDEX IF NOT EXISTS peopleSyncDirtyIndex"
360                    + " ON people (" + People._SYNC_DIRTY + ");");
361            oldVersion = 76;
362        }
363
364        if (oldVersion == 76 || oldVersion == 77) {
365            db.execSQL("DELETE FROM people");
366            db.execSQL("DELETE FROM groups");
367            db.execSQL("DELETE FROM photos");
368            db.execSQL("DELETE FROM _deleted_people");
369            db.execSQL("DELETE FROM _deleted_groups");
370            upgradeWasLossless = false;
371            oldVersion = 78;
372        }
373
374        if (oldVersion == 78) {
375            db.execSQL("UPDATE photos SET _sync_dirty=0 where _sync_dirty is null;");
376            oldVersion = 79;
377        }
378
379        if (oldVersion == 79) {
380            try {
381                db.execSQL("ALTER TABLE people ADD phonetic_name TEXT COLLATE LOCALIZED;");
382            } catch (SQLiteException sqle) {
383                // Maybe the table was altered already... Shouldn't be an issue.
384            }
385            oldVersion = 80;
386        }
387
388        // Because of historical reason, version 81 have two types.
389        // 1) One type already has "peopleLookupWithPhoneticName" table but does not have
390        // "peopleLookup" table with token_index.
391        // 2) Another type has "peopleLookup" table with token_index but does not have
392        // "peopleLookupWithPhoneticName" table.
393        // For simplicity, both databases are once dropped here.
394        // This is slow but should be done just once anyway...
395        if (oldVersion == 80 || oldVersion == 81) {
396            Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
397                    newVersion + ", which will preserve existing data");
398
399            recreatePeopleLookupTable(db);
400            try {
401                String query = "SELECT _TOKENIZE('peopleLookup', _id, name, ' ', 1) FROM people";
402                Cursor cursor = db.rawQuery(query, null);
403                try {
404                    int rows = cursor.getCount();
405                    Log.i(TAG, "Processed " + rows + " contacts.");
406                } finally {
407                    if (cursor != null) {
408                        cursor.close();
409                    }
410                }
411            } catch (SQLiteException e) {
412                Log.e(TAG, e.toString() + ": " + e.getMessage());
413            }
414
415            recreatePeopleLookupWithPhoneticNameTable(db);
416            try {
417                String query = "SELECT _TOKENIZE('peopleLookupWithPhoneticName', _id, "
418                    + PHONETIC_LOOKUP_SQL_SIMPLE +
419                    ", ' ', 1) FROM people";
420                Cursor cursor = db.rawQuery(query, null);
421                try {
422                    int rows = cursor.getCount();
423                    Log.i(TAG, "Processed " + rows + " contacts.");
424                } finally {
425                    if (cursor != null) {
426                        cursor.close();
427                    }
428                }
429            } catch (SQLiteException e) {
430                Log.e(TAG, e.toString() + ": " + e.getMessage());
431            }
432
433            oldVersion = 82;
434        }
435
436        return upgradeWasLossless;
437    }
438
439    protected void dropTables(SQLiteDatabase db) {
440        db.execSQL("DROP TABLE IF EXISTS people");
441        db.execSQL("DROP TABLE IF EXISTS peopleLookup");
442        db.execSQL("DROP TABLE IF EXISTS peopleLookupWithPhoneticName");
443        db.execSQL("DROP TABLE IF EXISTS _deleted_people");
444        db.execSQL("DROP TABLE IF EXISTS phones");
445        db.execSQL("DROP TABLE IF EXISTS contact_methods");
446        db.execSQL("DROP TABLE IF EXISTS calls");
447        db.execSQL("DROP TABLE IF EXISTS organizations");
448        db.execSQL("DROP TABLE IF EXISTS voice_dialer_timestamp");
449        db.execSQL("DROP TABLE IF EXISTS groups");
450        db.execSQL("DROP TABLE IF EXISTS _deleted_groups");
451        db.execSQL("DROP TABLE IF EXISTS groupmembership");
452        db.execSQL("DROP TABLE IF EXISTS photos");
453        db.execSQL("DROP TABLE IF EXISTS extensions");
454        db.execSQL("DROP TABLE IF EXISTS settings");
455    }
456
457    private void recreatePeopleLookupTable(SQLiteDatabase db) {
458        db.execSQL("DROP TABLE IF EXISTS peopleLookup");
459        db.execSQL("DROP INDEX IF EXISTS peopleLookupIndex");
460        db.execSQL("DROP TRIGGER IF EXISTS peopleLookup_update");
461        db.execSQL("DROP TRIGGER IF EXISTS peopleLookup_insert");
462
463        db.execSQL("CREATE TABLE peopleLookup (" +
464                "token TEXT," +
465                "source INTEGER REFERENCES people(_id)," +
466                "token_index INTEGER" +
467                ");");
468        db.execSQL("CREATE INDEX peopleLookupIndex ON peopleLookup (" +
469                "token," +
470                "source" +
471                ");");
472
473        // Triggers to keep the peopleLookup table up to date
474        db.execSQL("CREATE TRIGGER peopleLookup_update UPDATE OF name ON people " +
475                    "BEGIN " +
476                        "DELETE FROM peopleLookup WHERE source = new._id;" +
477                        "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
478                    "END");
479        db.execSQL("CREATE TRIGGER peopleLookup_insert AFTER INSERT ON people " +
480                    "BEGIN " +
481                        "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
482                    "END");
483    }
484
485    private void recreatePeopleLookupWithPhoneticNameTable(SQLiteDatabase db) {
486        db.execSQL("DROP TABLE IF EXISTS peopleLookupWithPhoneticName");
487        db.execSQL("DROP INDEX IF EXISTS peopleLookupwithPhoneticNameIndex");
488        db.execSQL("DROP TRIGGER IF EXISTS peopleLookupWithPhoneticName_update");
489        db.execSQL("DROP TRIGGER IF EXISTS peopleLookupWithPhoneticName_insert");
490
491        db.execSQL("CREATE TABLE peopleLookupWithPhoneticName (" +
492                "token TEXT," +
493                "source INTEGER REFERENCES people(_id)," +
494                "token_index INTEGER" +
495                ");");
496        db.execSQL("CREATE INDEX peopleLookupWithPhoneticNameIndex ON " +
497                "peopleLookupWithPhoneticName (" +
498                "token," +
499                "source" +
500                ");");
501
502        // Triggers to keep the peopleLookupWithPhoneticName table up to date
503        db.execSQL("CREATE TRIGGER peopleLookupWithPhoneticName_update UPDATE OF " +
504                    "name, phonetic_name ON people " +
505                    "BEGIN " +
506                        "DELETE FROM peopleLookupWithPhoneticName WHERE source = new._id;" +
507                        "SELECT _TOKENIZE('peopleLookupWithPhoneticName', new._id, " +
508                        PHONETIC_LOOKUP_SQL_SIMPLE_WITH_NEW +
509                        ", ' ', 1);" +
510                    "END");
511        db.execSQL("CREATE TRIGGER peopleLookupWithPhoneticName_insert AFTER INSERT ON people " +
512                    "BEGIN " +
513                        "SELECT _TOKENIZE('peopleLookupWithPhoneticName', new._id, " +
514                        PHONETIC_LOOKUP_SQL_SIMPLE_WITH_NEW +
515                        ", ' ', 1);" +
516                    "END");
517    }
518
519    @Override
520    protected void bootstrapDatabase(SQLiteDatabase db) {
521        super.bootstrapDatabase(db);
522        db.execSQL("CREATE TABLE people (" +
523                    People._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
524                    People._SYNC_ACCOUNT + " TEXT," + // From the sync source
525                    People._SYNC_ID + " TEXT," + // From the sync source
526                    People._SYNC_TIME + " TEXT," + // From the sync source
527                    People._SYNC_VERSION + " TEXT," + // From the sync source
528                    People._SYNC_LOCAL_ID + " INTEGER," + // Used while syncing, not persistent
529                    People._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
530                                                       // if syncable, non-zero if the record
531                                                       // has local, unsynced, changes
532                    People._SYNC_MARK + " INTEGER," + // Used to filter out new rows
533
534                    People.NAME + " TEXT COLLATE LOCALIZED," +
535                    People.NOTES + " TEXT COLLATE LOCALIZED," +
536                    People.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
537                    People.LAST_TIME_CONTACTED + " INTEGER," +
538                    People.STARRED + " INTEGER NOT NULL DEFAULT 0," +
539                    People.PRIMARY_PHONE_ID + " INTEGER REFERENCES phones(_id)," +
540                    People.PRIMARY_ORGANIZATION_ID + " INTEGER REFERENCES organizations(_id)," +
541                    People.PRIMARY_EMAIL_ID + " INTEGER REFERENCES contact_methods(_id)," +
542                    People.PHOTO_VERSION + " TEXT," +
543                    People.CUSTOM_RINGTONE + " TEXT," +
544                    People.SEND_TO_VOICEMAIL + " INTEGER," +
545                    People.PHONETIC_NAME + " TEXT COLLATE LOCALIZED" +
546                    ");");
547
548        db.execSQL("CREATE INDEX peopleNameIndex ON people (" + People.NAME + ");");
549        db.execSQL("CREATE INDEX peopleSyncDirtyIndex ON people (" + People._SYNC_DIRTY + ");");
550        db.execSQL("CREATE INDEX peopleSyncIdIndex ON people (" + People._SYNC_ID + ");");
551
552        db.execSQL("CREATE TRIGGER people_timesContacted UPDATE OF last_time_contacted ON people " +
553                    "BEGIN " +
554                        "UPDATE people SET "
555                            + People.TIMES_CONTACTED + " = (new." + People.TIMES_CONTACTED + " + 1)"
556                            + " WHERE _id = new._id;" +
557                    "END");
558
559        // table of all the groups that exist for an account
560        db.execSQL("CREATE TABLE groups (" +
561                Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
562                Groups._SYNC_ACCOUNT + " TEXT," + // From the sync source
563                Groups._SYNC_ID + " TEXT," + // From the sync source
564                Groups._SYNC_TIME + " TEXT," + // From the sync source
565                Groups._SYNC_VERSION + " TEXT," + // From the sync source
566                Groups._SYNC_LOCAL_ID + " INTEGER," + // Used while syncing, not persistent
567                Groups._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
568                                                          // if syncable, non-zero if the record
569                                                          // has local, unsynced, changes
570                Groups._SYNC_MARK + " INTEGER," + // Used to filter out new rows
571
572                Groups.NAME + " TEXT NOT NULL," +
573                Groups.NOTES + " TEXT," +
574                Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 0," +
575                Groups.SYSTEM_ID + " TEXT," +
576                "UNIQUE(" +
577                Groups.NAME + ","  + Groups.SYSTEM_ID + "," + Groups._SYNC_ACCOUNT + ")" +
578                ");");
579
580        db.execSQL("CREATE INDEX groupsSyncDirtyIndex ON groups (" + Groups._SYNC_DIRTY + ");");
581
582        if (!isTemporary()) {
583            // Add the system groups, since we always need them.
584            db.execSQL("INSERT INTO groups (" + Groups.NAME + ", " + Groups.SYSTEM_ID + ") VALUES "
585                    + "('" + Groups.GROUP_MY_CONTACTS + "', '" + Groups.GROUP_MY_CONTACTS + "')");
586        }
587
588        db.execSQL("CREATE TABLE peopleLookup (" +
589                    "token TEXT," +
590                    "source INTEGER REFERENCES people(_id)," +
591                    "token_index INTEGER"+
592                    ");");
593        db.execSQL("CREATE INDEX peopleLookupIndex ON peopleLookup (" +
594                    "token," +
595                    "source" +
596                    ");");
597
598        db.execSQL("CREATE TABLE photos ("
599                + Photos._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
600                + Photos.EXISTS_ON_SERVER + " INTEGER NOT NULL DEFAULT 0,"
601                + Photos.PERSON_ID + " INTEGER REFERENCES people(_id), "
602                + Photos.LOCAL_VERSION + " TEXT,"
603                + Photos.DATA + " BLOB,"
604                + Photos.SYNC_ERROR + " TEXT,"
605                + Photos._SYNC_ACCOUNT + " TEXT,"
606                + Photos._SYNC_ID + " TEXT,"
607                + Photos._SYNC_TIME + " TEXT,"
608                + Photos._SYNC_VERSION + " TEXT,"
609                + Photos._SYNC_LOCAL_ID + " INTEGER,"
610                + Photos._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0,"
611                + Photos._SYNC_MARK + " INTEGER,"
612                + "UNIQUE(" + Photos.PERSON_ID + ") "
613                + ")");
614
615        db.execSQL("CREATE INDEX photosSyncDirtyIndex ON photos (" + Photos._SYNC_DIRTY + ");");
616        db.execSQL("CREATE INDEX photoPersonIndex ON photos (person);");
617
618        // Delete the photo row when the people row is deleted
619        db.execSQL(""
620                + " CREATE TRIGGER peopleDeleteAndPhotos DELETE ON people "
621                + " BEGIN"
622                + "   DELETE FROM photos WHERE person=OLD._id;"
623                + " END");
624
625        db.execSQL("CREATE TABLE _deleted_people (" +
626                    "_sync_version TEXT," + // From the sync source
627                    "_sync_id TEXT," +
628                    (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
629                    "_sync_account TEXT," +
630                    "_sync_mark INTEGER)"); // Used to filter out new rows
631
632        db.execSQL("CREATE TABLE _deleted_groups (" +
633                    "_sync_version TEXT," + // From the sync source
634                    "_sync_id TEXT," +
635                    (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
636                    "_sync_account TEXT," +
637                    "_sync_mark INTEGER)"); // Used to filter out new rows
638
639        db.execSQL("CREATE TABLE phones (" +
640                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
641                    "person INTEGER REFERENCES people(_id)," +
642                    "type INTEGER NOT NULL," + // kind specific (home, work, etc)
643                    "number TEXT," +
644                    "number_key TEXT," +
645                    "label TEXT," +
646                    "isprimary INTEGER NOT NULL DEFAULT 0" +
647                    ");");
648        db.execSQL("CREATE INDEX phonesIndex1 ON phones (person);");
649        db.execSQL("CREATE INDEX phonesIndex2 ON phones (number_key);");
650
651        db.execSQL("CREATE TABLE contact_methods (" +
652                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
653                    "person INTEGER REFERENCES people(_id)," +
654                    "kind INTEGER NOT NULL," + // the kind of contact method
655                    "data TEXT," +
656                    "aux_data TEXT," +
657                    "type INTEGER NOT NULL," + // kind specific (home, work, etc)
658                    "label TEXT," +
659                    "isprimary INTEGER NOT NULL DEFAULT 0" +
660                    ");");
661        db.execSQL("CREATE INDEX contactMethodsPeopleIndex "
662                + "ON contact_methods (person);");
663
664        // The table for recent calls is here so we can do table joins
665        // on people, phones, and calls all in one place.
666        db.execSQL("CREATE TABLE calls (" +
667                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
668                    "number TEXT," +
669                    "date INTEGER," +
670                    "duration INTEGER," +
671                    "type INTEGER," +
672                    "new INTEGER," +
673                    "name TEXT," +
674                    "numbertype INTEGER," +
675                    "numberlabel TEXT" +
676                    ");");
677
678        // Various settings for the contacts sync adapter. The _sync_account column may
679        // be null, but it must not be the empty string.
680        db.execSQL("CREATE TABLE settings (" +
681                    "_id INTEGER PRIMARY KEY," +
682                    "_sync_account TEXT," +
683                    "key STRING NOT NULL," +
684                    "value STRING " +
685                    ");");
686
687        // The table for the organizations of a person.
688        db.execSQL("CREATE TABLE organizations (" +
689                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
690                    "company TEXT," +
691                    "title TEXT," +
692                    "isprimary INTEGER NOT NULL DEFAULT 0," +
693                    "type INTEGER NOT NULL," + // kind specific (home, work, etc)
694                    "label TEXT," +
695                    "person INTEGER REFERENCES people(_id)" +
696                    ");");
697        db.execSQL("CREATE INDEX organizationsIndex1 ON organizations (person);");
698
699        // The table for the extensions of a person.
700        db.execSQL("CREATE TABLE extensions (" +
701                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
702                    "name TEXT NOT NULL," +
703                    "value TEXT NOT NULL," +
704                    "person INTEGER REFERENCES people(_id)," +
705                    "UNIQUE(person, name)" +
706                    ");");
707        db.execSQL("CREATE INDEX extensionsIndex1 ON extensions (person, name);");
708
709        // The table for the groups of a person.
710        db.execSQL("CREATE TABLE groupmembership (" +
711                "_id INTEGER PRIMARY KEY," +
712                "person INTEGER REFERENCES people(_id)," +
713                "group_id INTEGER REFERENCES groups(_id)," +
714                "group_sync_account STRING," +
715                "group_sync_id STRING" +
716                ");");
717        db.execSQL("CREATE INDEX groupmembershipIndex1 ON groupmembership (person, group_id);");
718        db.execSQL("CREATE INDEX groupmembershipIndex2 ON groupmembership (group_id, person);");
719        db.execSQL("CREATE INDEX groupmembershipIndex3 ON groupmembership "
720                + "(group_sync_account, group_sync_id);");
721
722        // Trigger to completely remove a contacts data when they're deleted
723        db.execSQL("CREATE TRIGGER contact_cleanup DELETE ON people " +
724                    "BEGIN " +
725                        "DELETE FROM peopleLookup WHERE source = old._id;" +
726                        "DELETE FROM peopleLookupWithPhoneticName WHERE source = old._id;" +
727                        "DELETE FROM phones WHERE person = old._id;" +
728                        "DELETE FROM contact_methods WHERE person = old._id;" +
729                        "DELETE FROM organizations WHERE person = old._id;" +
730                        "DELETE FROM groupmembership WHERE person = old._id;" +
731                        "DELETE FROM extensions WHERE person = old._id;" +
732                    "END");
733
734        // Trigger to disassociate the groupmembership from the groups when an
735        // groups entry is deleted
736        db.execSQL("CREATE TRIGGER groups_cleanup DELETE ON groups " +
737                    "BEGIN " +
738                        "UPDATE groupmembership SET group_id = null WHERE group_id = old._id;" +
739                    "END");
740
741        // Trigger to move an account_people row to _deleted_account_people when it is deleted
742        db.execSQL("CREATE TRIGGER groups_to_deleted DELETE ON groups " +
743                    "WHEN old._sync_id is not null " +
744                    "BEGIN " +
745                        "INSERT INTO _deleted_groups " +
746                            "(_sync_id, _sync_account, _sync_version) " +
747                            "VALUES (old._sync_id, old._sync_account, " +
748                            "old._sync_version);" +
749                    "END");
750
751        recreatePeopleLookupTable(db);
752        recreatePeopleLookupWithPhoneticNameTable(db);
753
754        // Triggers to set the _sync_dirty flag when a phone is changed,
755        // inserted or deleted
756        db.execSQL("CREATE TRIGGER phones_update UPDATE ON phones " +
757                    "BEGIN " +
758                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
759                    "END");
760        db.execSQL("CREATE TRIGGER phones_insert INSERT ON phones " +
761                    "BEGIN " +
762                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person;" +
763                    "END");
764        db.execSQL("CREATE TRIGGER phones_delete DELETE ON phones " +
765                    "BEGIN " +
766                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
767                    "END");
768
769        // Triggers to set the _sync_dirty flag when a contact_method is
770        // changed, inserted or deleted
771        db.execSQL("CREATE TRIGGER contact_methods_update UPDATE ON contact_methods " +
772                    "BEGIN " +
773                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
774                    "END");
775        db.execSQL("CREATE TRIGGER contact_methods_insert INSERT ON contact_methods " +
776                    "BEGIN " +
777                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person;" +
778                    "END");
779        db.execSQL("CREATE TRIGGER contact_methods_delete DELETE ON contact_methods " +
780                    "BEGIN " +
781                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
782                    "END");
783
784        // Triggers for when an organization is changed, inserted or deleted
785        db.execSQL("CREATE TRIGGER organizations_update AFTER UPDATE ON organizations " +
786                    "BEGIN " +
787                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " +
788                    "END");
789        db.execSQL("CREATE TRIGGER organizations_insert INSERT ON organizations " +
790                    "BEGIN " +
791                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " +
792                    "END");
793        db.execSQL("CREATE TRIGGER organizations_delete DELETE ON organizations " +
794                    "BEGIN " +
795                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
796                    "END");
797
798        // Triggers for when an groupmembership is changed, inserted or deleted
799        db.execSQL("CREATE TRIGGER groupmembership_update AFTER UPDATE ON groupmembership " +
800                    "BEGIN " +
801                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " +
802                    "END");
803        db.execSQL("CREATE TRIGGER groupmembership_insert INSERT ON groupmembership " +
804                    "BEGIN " +
805                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " +
806                    "END");
807        db.execSQL("CREATE TRIGGER groupmembership_delete DELETE ON groupmembership " +
808                    "BEGIN " +
809                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
810                    "END");
811
812        // Triggers for when an extension is changed, inserted or deleted
813        db.execSQL("CREATE TRIGGER extensions_update AFTER UPDATE ON extensions " +
814                    "BEGIN " +
815                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " +
816                    "END");
817        db.execSQL("CREATE TRIGGER extensions_insert INSERT ON extensions " +
818                    "BEGIN " +
819                        "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " +
820                    "END");
821        db.execSQL("CREATE TRIGGER extensions_delete DELETE ON extensions " +
822                    "BEGIN " +
823                        "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
824                    "END");
825
826        createTypeLabelTrigger(db, sPhonesTable, "INSERT");
827        createTypeLabelTrigger(db, sPhonesTable, "UPDATE");
828        createTypeLabelTrigger(db, sOrganizationsTable, "INSERT");
829        createTypeLabelTrigger(db, sOrganizationsTable, "UPDATE");
830        createTypeLabelTrigger(db, sContactMethodsTable, "INSERT");
831        createTypeLabelTrigger(db, sContactMethodsTable, "UPDATE");
832
833        // Temporary table that holds a time stamp of the last time data the voice
834        // dialer is interested in has changed so the grammar won't need to be
835        // recompiled when unused data is changed.
836        db.execSQL("CREATE TABLE voice_dialer_timestamp (" +
837                   "_id INTEGER PRIMARY KEY," +
838                   "timestamp INTEGER" +
839                   ");");
840        db.execSQL("INSERT INTO voice_dialer_timestamp (_id, timestamp) VALUES " +
841                       "(1, strftime('%s', 'now'));");
842        db.execSQL("CREATE TRIGGER timestamp_trigger1 AFTER UPDATE ON phones " +
843                   "BEGIN " +
844                       "UPDATE voice_dialer_timestamp SET timestamp=strftime('%s', 'now') "+
845                           "WHERE _id=1;" +
846                   "END");
847        db.execSQL("CREATE TRIGGER timestamp_trigger2 AFTER UPDATE OF name ON people " +
848                   "BEGIN " +
849                       "UPDATE voice_dialer_timestamp SET timestamp=strftime('%s', 'now') " +
850                           "WHERE _id=1;" +
851                   "END");
852    }
853
854    private void createTypeLabelTrigger(SQLiteDatabase db, String table, String operation) {
855        final String name = table + "_" + operation + "_typeAndLabel";
856        db.execSQL("CREATE TRIGGER " + name + " AFTER " + operation + " ON " + table
857                + "   WHEN (NEW.type != 0 AND NEW.label IS NOT NULL) OR "
858                + "        (NEW.type = 0 AND NEW.label IS NULL)"
859                + "   BEGIN "
860                + "     SELECT RAISE (ABORT, 'exactly one of type or label must be set'); "
861                + "   END");
862    }
863
864    private void maybeCreatePresenceTable(SQLiteDatabase db) {
865        // Load the presence table from the presence_db. Just create the table
866        // if we are
867        String cpDbName;
868        if (!isTemporary()) {
869            db.execSQL("ATTACH DATABASE ':memory:' AS presence_db;");
870            cpDbName = "presence_db.";
871        } else {
872            cpDbName = "";
873        }
874        db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + "presence ("+
875                    Presence._ID + " INTEGER PRIMARY KEY," +
876                    Presence.PERSON_ID + " INTEGER REFERENCES people(_id)," +
877                    Presence.IM_PROTOCOL + " TEXT," +
878                    Presence.IM_HANDLE + " TEXT," +
879                    Presence.IM_ACCOUNT + " TEXT," +
880                    Presence.PRESENCE_STATUS + " INTEGER," +
881                    Presence.PRESENCE_CUSTOM_STATUS + " TEXT," +
882                    "UNIQUE(" + Presence.IM_PROTOCOL + ", " + Presence.IM_HANDLE + ", "
883                            + Presence.IM_ACCOUNT + ")" +
884                    ");");
885
886        db.execSQL("CREATE INDEX IF NOT EXISTS " + cpDbName + "presenceIndex ON presence ("
887                + Presence.PERSON_ID + ");");
888    }
889
890    private String buildPeopleLookupWhereClauseCommon(String filterParam, String tableName) {
891        StringBuilder filter = new StringBuilder("people._id IN (SELECT source FROM ");
892        filter.append(tableName);
893        filter.append(" WHERE token GLOB ");
894        // NOTE: Query parameters won't work here since the SQL compiler
895        // needs to parse the actual string to know that it can use the
896        // index to do a prefix scan.
897        DatabaseUtils.appendEscapedSQLString(filter,
898                DatabaseUtils.getHexCollationKey(filterParam) + "*");
899        filter.append(')');
900        return filter.toString();
901    }
902
903    private String buildPeopleLookupWhereClause(String filterParam) {
904        return buildPeopleLookupWhereClauseCommon(filterParam, "peopleLookup");
905    }
906
907    private String buildPeopleLookupWhereClauseForSuggestion(String filterParam) {
908        return buildPeopleLookupWhereClauseCommon(filterParam,
909                usePhoneticNameForPeopleLookup()
910                        ? "peopleLookupWithPhoneticName"
911                        : "peopleLookup");
912    }
913
914    @Override
915    public Cursor queryInternal(Uri url, String[] projectionIn,
916            String selection, String[] selectionArgs, String sort) {
917
918        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
919        Uri notificationUri = Contacts.CONTENT_URI;
920        String limit = getLimit(url);
921        StringBuilder whereClause;
922        String groupBy = null;
923
924        // Generate the body of the query
925        int match = sURIMatcher.match(url);
926
927        if (Config.LOGV) Log.v(TAG, "ContactsProvider.query: url=" + url + ", match is " + match);
928
929        switch (match) {
930            case DELETED_GROUPS:
931                if (!isTemporary()) {
932                    throw new UnsupportedOperationException();
933                }
934
935                qb.setTables(sDeletedGroupsTable);
936                break;
937
938            case GROUPS_ID:
939                qb.appendWhere("_id=");
940                qb.appendWhere(url.getPathSegments().get(1));
941                // fall through
942            case GROUPS:
943                qb.setTables(sGroupsTable);
944                qb.setProjectionMap(sGroupsProjectionMap);
945                break;
946
947            case SETTINGS:
948                qb.setTables(sSettingsTable);
949                break;
950
951            case PEOPLE_GROUPMEMBERSHIP_ID:
952                qb.appendWhere("groupmembership._id=");
953                qb.appendWhere(url.getPathSegments().get(3));
954                qb.appendWhere(" AND ");
955                // fall through
956            case PEOPLE_GROUPMEMBERSHIP:
957                qb.appendWhere(sGroupsJoinString + " AND ");
958                qb.appendWhere("person=" + url.getPathSegments().get(1));
959                qb.setTables("groups, groupmembership");
960                qb.setProjectionMap(sGroupMembershipProjectionMap);
961                break;
962
963            case GROUPMEMBERSHIP_ID:
964                qb.appendWhere("groupmembership._id=");
965                qb.appendWhere(url.getPathSegments().get(1));
966                qb.appendWhere(" AND ");
967                // fall through
968            case GROUPMEMBERSHIP:
969                qb.setTables("groups, groupmembership");
970                qb.setProjectionMap(sGroupMembershipProjectionMap);
971                qb.appendWhere(sGroupsJoinString);
972                break;
973
974            case GROUPMEMBERSHIP_RAW:
975                qb.setTables("groupmembership");
976                break;
977
978            case GROUP_NAME_MEMBERS_FILTER:
979                if (url.getPathSegments().size() > 5) {
980                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
981                    qb.appendWhere(" AND ");
982                }
983                // fall through
984            case GROUP_NAME_MEMBERS:
985                qb.setTables(PEOPLE_PHONES_JOIN);
986                qb.setProjectionMap(sPeopleProjectionMap);
987                qb.appendWhere(buildGroupNameMatchWhereClause(url.getPathSegments().get(2)));
988                break;
989
990            case GROUP_SYSTEM_ID_MEMBERS_FILTER:
991                if (url.getPathSegments().size() > 5) {
992                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
993                    qb.appendWhere(" AND ");
994                }
995                // fall through
996            case GROUP_SYSTEM_ID_MEMBERS:
997                qb.setTables(PEOPLE_PHONES_JOIN);
998                qb.setProjectionMap(sPeopleProjectionMap);
999                qb.appendWhere(buildGroupSystemIdMatchWhereClause(url.getPathSegments().get(2)));
1000                break;
1001
1002            case PEOPLE:
1003                qb.setTables(PEOPLE_PHONES_JOIN);
1004                qb.setProjectionMap(sPeopleProjectionMap);
1005                break;
1006            case PEOPLE_RAW:
1007                qb.setTables(sPeopleTable);
1008                break;
1009
1010            case PEOPLE_OWNER:
1011                return queryOwner(projectionIn);
1012
1013            case PEOPLE_WITH_PHONES_FILTER:
1014
1015                qb.appendWhere("number IS NOT NULL AND ");
1016
1017                // Fall through.
1018
1019            case PEOPLE_FILTER: {
1020                qb.setTables(PEOPLE_PHONES_JOIN);
1021                qb.setProjectionMap(sPeopleProjectionMap);
1022                if (url.getPathSegments().size() > 2) {
1023                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
1024                }
1025                break;
1026            }
1027
1028            case PEOPLE_WITH_EMAIL_OR_IM_FILTER:
1029                String email = url.getPathSegments().get(2);
1030                whereClause = new StringBuilder();
1031
1032                // Match any E-mail or IM contact methods where data exactly
1033                // matches the provided string.
1034                whereClause.append(ContactMethods.DATA);
1035                whereClause.append("=");
1036                DatabaseUtils.appendEscapedSQLString(whereClause, email);
1037                whereClause.append(" AND (kind = " + Contacts.KIND_EMAIL +
1038                        " OR kind = " + Contacts.KIND_IM + ")");
1039                qb.appendWhere(whereClause.toString());
1040
1041                qb.setTables("people INNER JOIN contact_methods on (people._id = contact_methods.person)");
1042                qb.setProjectionMap(sPeopleWithEmailOrImProjectionMap);
1043
1044                // Prevent returning the same person for multiple matches
1045                groupBy = "contact_methods.person";
1046
1047                qb.setDistinct(true);
1048                break;
1049
1050            case PHOTOS_ID:
1051                qb.appendWhere("_id="+url.getPathSegments().get(1));
1052                // Fall through.
1053            case PHOTOS:
1054                qb.setTables(sPhotosTable);
1055                qb.setProjectionMap(sPhotosProjectionMap);
1056                break;
1057
1058            case PEOPLE_PHOTO:
1059                qb.appendWhere("person="+url.getPathSegments().get(1));
1060                qb.setTables(sPhotosTable);
1061                qb.setProjectionMap(sPhotosProjectionMap);
1062                break;
1063
1064            case SEARCH_SUGGESTIONS: {
1065                // Force the default sort order, since the SearchManage doesn't ask for things
1066                // sorted, though they should be
1067                if (sort != null && !People.DEFAULT_SORT_ORDER.equals(sort)) {
1068                    throw new IllegalArgumentException("Sort ordering not allowed for this URI");
1069                }
1070                sort = SearchManager.SUGGEST_COLUMN_TEXT_1 + " COLLATE LOCALIZED ASC";
1071
1072                // This will either setup the query builder so we can run the proper query below
1073                // and return null, or it will return a cursor with the results already in it.
1074                Cursor c = handleSearchSuggestionsQuery(url, qb);
1075                if (c != null) {
1076                    return c;
1077                }
1078                break;
1079            }
1080            case SEARCH_SHORTCUT: {
1081                qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN);
1082                qb.setProjectionMap(getCurrentSearchSuggestionsProjectionMap());
1083                qb.appendWhere(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID + "=");
1084                qb.appendWhere(url.getPathSegments().get(1));
1085                break;
1086            }
1087            case PEOPLE_STREQUENT: {
1088                // Build the first query for starred
1089                qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
1090                qb.setProjectionMap(sStrequentStarredProjectionMap);
1091                final String starredQuery = qb.buildQuery(projectionIn, "starred = 1",
1092                        null, null, null, null,
1093                        null /* limit */);
1094
1095                // Build the second query for frequent
1096                qb = new SQLiteQueryBuilder();
1097                qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
1098                qb.setProjectionMap(sPeopleWithPhotoProjectionMap);
1099                final String frequentQuery = qb.buildQuery(projectionIn,
1100                        "times_contacted > 0 AND starred = 0", null, null, null, null, null);
1101
1102                // Put them together
1103                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
1104                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
1105                final SQLiteDatabase db = getDatabase();
1106                Cursor c = db.rawQueryWithFactory(null, query, null, sPeopleTable);
1107                if ((c != null) && !isTemporary()) {
1108                    c.setNotificationUri(getContext().getContentResolver(), notificationUri);
1109                }
1110                return c;
1111            }
1112            case PEOPLE_STREQUENT_FILTER: {
1113                // Build the first query for starred
1114                qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
1115                qb.setProjectionMap(sStrequentStarredProjectionMap);
1116                if (url.getPathSegments().size() > 3) {
1117                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
1118                }
1119                final String starredQuery = qb.buildQuery(projectionIn, "starred = 1",
1120                        null, null, null, null,
1121                        null /* limit */);
1122
1123                // Build the second query for frequent
1124                qb = new SQLiteQueryBuilder();
1125                qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
1126                qb.setProjectionMap(sPeopleWithPhotoProjectionMap);
1127                if (url.getPathSegments().size() > 3) {
1128                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
1129                }
1130                final String frequentQuery = qb.buildQuery(projectionIn,
1131                        "times_contacted > 0 AND starred = 0", null, null, null, null, null);
1132
1133                // Put them together
1134                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
1135                        STREQUENT_ORDER_BY, null);
1136                final SQLiteDatabase db = getDatabase();
1137                Cursor c = db.rawQueryWithFactory(null, query, null, sPeopleTable);
1138                if ((c != null) && !isTemporary()) {
1139                    c.setNotificationUri(getContext().getContentResolver(), notificationUri);
1140                }
1141                return c;
1142            }
1143            case DELETED_PEOPLE:
1144                if (isTemporary()) {
1145                    qb.setTables("_deleted_people");
1146                    break;
1147                }
1148                throw new UnsupportedOperationException();
1149            case PEOPLE_ID:
1150                qb.setTables("people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
1151                        + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID
1152                        + "=people._id)");
1153                qb.setProjectionMap(sPeopleProjectionMap);
1154                qb.appendWhere("people._id=");
1155                qb.appendWhere(url.getPathSegments().get(1));
1156                break;
1157            case PEOPLE_PHONES:
1158                qb.setTables("phones, people");
1159                qb.setProjectionMap(sPhonesProjectionMap);
1160                qb.appendWhere("people._id = phones.person AND person=");
1161                qb.appendWhere(url.getPathSegments().get(1));
1162                break;
1163            case PEOPLE_PHONES_ID:
1164                qb.setTables("phones, people");
1165                qb.setProjectionMap(sPhonesProjectionMap);
1166                qb.appendWhere("people._id = phones.person AND person=");
1167                qb.appendWhere(url.getPathSegments().get(1));
1168                qb.appendWhere(" AND phones._id=");
1169                qb.appendWhere(url.getPathSegments().get(3));
1170                break;
1171
1172            case PEOPLE_PHONES_WITH_PRESENCE:
1173                qb.appendWhere("people._id=?");
1174                selectionArgs = appendSelectionArg(selectionArgs, url.getPathSegments().get(1));
1175                // Fall through.
1176
1177            case PHONES_WITH_PRESENCE:
1178                qb.setTables("phones JOIN people ON (phones.person = people._id)"
1179                        + " LEFT OUTER JOIN presence ON (presence.person = people._id)");
1180                qb.setProjectionMap(sPhonesWithPresenceProjectionMap);
1181                break;
1182
1183            case PEOPLE_CONTACTMETHODS:
1184                qb.setTables("contact_methods, people");
1185                qb.setProjectionMap(sContactMethodsProjectionMap);
1186                qb.appendWhere("people._id = contact_methods.person AND person=");
1187                qb.appendWhere(url.getPathSegments().get(1));
1188                break;
1189            case PEOPLE_CONTACTMETHODS_ID:
1190                qb.setTables("contact_methods, people");
1191                qb.setProjectionMap(sContactMethodsProjectionMap);
1192                qb.appendWhere("people._id = contact_methods.person AND person=");
1193                qb.appendWhere(url.getPathSegments().get(1));
1194                qb.appendWhere(" AND contact_methods._id=");
1195                qb.appendWhere(url.getPathSegments().get(3));
1196                break;
1197            case PEOPLE_ORGANIZATIONS:
1198                qb.setTables("organizations, people");
1199                qb.setProjectionMap(sOrganizationsProjectionMap);
1200                qb.appendWhere("people._id = organizations.person AND person=");
1201                qb.appendWhere(url.getPathSegments().get(1));
1202                break;
1203            case PEOPLE_ORGANIZATIONS_ID:
1204                qb.setTables("organizations, people");
1205                qb.setProjectionMap(sOrganizationsProjectionMap);
1206                qb.appendWhere("people._id = organizations.person AND person=");
1207                qb.appendWhere(url.getPathSegments().get(1));
1208                qb.appendWhere(" AND organizations._id=");
1209                qb.appendWhere(url.getPathSegments().get(3));
1210                break;
1211            case PHONES:
1212                qb.setTables("phones, people");
1213                qb.appendWhere("people._id = phones.person");
1214                qb.setProjectionMap(sPhonesProjectionMap);
1215                break;
1216            case PHONES_ID:
1217                qb.setTables("phones, people");
1218                qb.appendWhere("people._id = phones.person AND phones._id="
1219                        + url.getPathSegments().get(1));
1220                qb.setProjectionMap(sPhonesProjectionMap);
1221                break;
1222            case ORGANIZATIONS:
1223                qb.setTables("organizations, people");
1224                qb.appendWhere("people._id = organizations.person");
1225                qb.setProjectionMap(sOrganizationsProjectionMap);
1226                break;
1227            case ORGANIZATIONS_ID:
1228                qb.setTables("organizations, people");
1229                qb.appendWhere("people._id = organizations.person AND organizations._id="
1230                        + url.getPathSegments().get(1));
1231                qb.setProjectionMap(sOrganizationsProjectionMap);
1232                break;
1233            case PHONES_MOBILE_FILTER_NAME:
1234                qb.appendWhere("type=" + Contacts.PhonesColumns.TYPE_MOBILE + " AND ");
1235
1236                // Fall through.
1237
1238            case PHONES_FILTER_NAME:
1239                qb.setTables("phones JOIN people ON (people._id = phones.person)");
1240                qb.setProjectionMap(sPhonesProjectionMap);
1241                if (url.getPathSegments().size() > 2) {
1242                    qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
1243                }
1244                break;
1245
1246            case PHONES_FILTER: {
1247                String phoneNumber = url.getPathSegments().get(2);
1248                String indexable = PhoneNumberUtils.toCallerIDMinMatch(phoneNumber);
1249                StringBuilder subQuery = new StringBuilder();
1250                if (TextUtils.isEmpty(sort)) {
1251                    // Default the sort order to something reasonable so we get consistent
1252                    // results when callers don't request an ordering
1253                    sort = People.DEFAULT_SORT_ORDER;
1254                }
1255
1256                subQuery.append("people, (SELECT * FROM phones WHERE (phones.number_key GLOB '");
1257                subQuery.append(indexable);
1258                subQuery.append("*')) AS phones");
1259                qb.setTables(subQuery.toString());
1260                qb.appendWhere("phones.person=people._id AND PHONE_NUMBERS_EQUAL(phones.number, ");
1261                qb.appendWhereEscapeString(phoneNumber);
1262                qb.appendWhere(")");
1263                qb.setProjectionMap(sPhonesProjectionMap);
1264                break;
1265            }
1266            case CONTACTMETHODS:
1267                qb.setTables("contact_methods, people");
1268                qb.setProjectionMap(sContactMethodsProjectionMap);
1269                qb.appendWhere("people._id = contact_methods.person");
1270                break;
1271            case CONTACTMETHODS_ID:
1272                qb.setTables("contact_methods LEFT OUTER JOIN people ON contact_methods.person = people._id");
1273                qb.setProjectionMap(sContactMethodsProjectionMap);
1274                qb.appendWhere("contact_methods._id=");
1275                qb.appendWhere(url.getPathSegments().get(1));
1276                break;
1277            case CONTACTMETHODS_EMAIL_FILTER:
1278                String pattern = url.getPathSegments().get(2);
1279                whereClause = new StringBuilder();
1280
1281                // TODO This is going to be REALLY slow.  Come up with
1282                // something faster.
1283                whereClause.append(ContactMethods.KIND);
1284                whereClause.append('=');
1285                whereClause.append('\'');
1286                whereClause.append(Contacts.KIND_EMAIL);
1287                whereClause.append("' AND (UPPER(");
1288                whereClause.append(ContactMethods.NAME);
1289                whereClause.append(") GLOB ");
1290                DatabaseUtils.appendEscapedSQLString(whereClause, pattern + "*");
1291                whereClause.append(" OR UPPER(");
1292                whereClause.append(ContactMethods.NAME);
1293                whereClause.append(") GLOB ");
1294                DatabaseUtils.appendEscapedSQLString(whereClause, "* " + pattern + "*");
1295                whereClause.append(") AND ");
1296                qb.appendWhere(whereClause.toString());
1297
1298                // Fall through.
1299
1300            case CONTACTMETHODS_EMAIL:
1301                qb.setTables("contact_methods INNER JOIN people on (contact_methods.person = people._id)");
1302                qb.setProjectionMap(sEmailSearchProjectionMap);
1303                qb.appendWhere("kind = " + Contacts.KIND_EMAIL);
1304                qb.setDistinct(true);
1305                break;
1306
1307            case PEOPLE_CONTACTMETHODS_WITH_PRESENCE:
1308                qb.appendWhere("people._id=?");
1309                selectionArgs = appendSelectionArg(selectionArgs, url.getPathSegments().get(1));
1310                // Fall through.
1311
1312            case CONTACTMETHODS_WITH_PRESENCE:
1313                qb.setTables("contact_methods JOIN people ON (contact_methods.person = people._id)"
1314                        + " LEFT OUTER JOIN presence ON "
1315                        // Match gtalk presence items
1316                        + "((kind=" + Contacts.KIND_EMAIL +
1317                            " AND im_protocol='"
1318                            + ContactMethods.encodePredefinedImProtocol(
1319                                    ContactMethods.PROTOCOL_GOOGLE_TALK)
1320                            + "' AND data=im_handle)"
1321                        + " OR "
1322                        // Match IM presence items
1323                        + "(kind=" + Contacts.KIND_IM
1324                            + " AND data=im_handle AND aux_data=im_protocol))");
1325                qb.setProjectionMap(sContactMethodsWithPresenceProjectionMap);
1326                break;
1327
1328            case CALLS:
1329                qb.setTables("calls");
1330                qb.setProjectionMap(sCallsProjectionMap);
1331                notificationUri = CallLog.CONTENT_URI;
1332                break;
1333            case CALLS_ID:
1334                qb.setTables("calls");
1335                qb.setProjectionMap(sCallsProjectionMap);
1336                qb.appendWhere("calls._id=");
1337                qb.appendWhere(url.getPathSegments().get(1));
1338                notificationUri = CallLog.CONTENT_URI;
1339                break;
1340            case CALLS_FILTER: {
1341                qb.setTables("calls");
1342                qb.setProjectionMap(sCallsProjectionMap);
1343
1344                String phoneNumber = url.getPathSegments().get(2);
1345                qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
1346                qb.appendWhereEscapeString(phoneNumber);
1347                qb.appendWhere(")");
1348                notificationUri = CallLog.CONTENT_URI;
1349                break;
1350            }
1351
1352            case PRESENCE:
1353                qb.setTables("presence LEFT OUTER JOIN people on (presence." + Presence.PERSON_ID
1354                        + "= people._id)");
1355                qb.setProjectionMap(sPresenceProjectionMap);
1356                break;
1357            case PRESENCE_ID:
1358                qb.setTables("presence LEFT OUTER JOIN people on (presence." + Presence.PERSON_ID
1359                        + "= people._id)");
1360                qb.appendWhere("presence._id=");
1361                qb.appendWhere(url.getLastPathSegment());
1362                break;
1363            case VOICE_DIALER_TIMESTAMP:
1364                qb.setTables("voice_dialer_timestamp");
1365                qb.appendWhere("_id=1");
1366                break;
1367
1368            case PEOPLE_EXTENSIONS_ID:
1369                qb.appendWhere("extensions._id=" + url.getPathSegments().get(3) + " AND ");
1370                // fall through
1371            case PEOPLE_EXTENSIONS:
1372                qb.appendWhere("person=" + url.getPathSegments().get(1));
1373                qb.setTables(sExtensionsTable);
1374                qb.setProjectionMap(sExtensionsProjectionMap);
1375                break;
1376
1377            case EXTENSIONS_ID:
1378                qb.appendWhere("extensions._id=" + url.getPathSegments().get(1));
1379                // fall through
1380            case EXTENSIONS:
1381                qb.setTables(sExtensionsTable);
1382                qb.setProjectionMap(sExtensionsProjectionMap);
1383                break;
1384
1385            case LIVE_FOLDERS_PEOPLE:
1386                qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
1387                qb.setProjectionMap(sLiveFoldersProjectionMap);
1388                break;
1389
1390            case LIVE_FOLDERS_PEOPLE_WITH_PHONES:
1391                qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
1392                qb.setProjectionMap(sLiveFoldersProjectionMap);
1393                qb.appendWhere(People.PRIMARY_PHONE_ID + " IS NOT NULL");
1394                break;
1395
1396            case LIVE_FOLDERS_PEOPLE_FAVORITES:
1397                qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
1398                qb.setProjectionMap(sLiveFoldersProjectionMap);
1399                qb.appendWhere(People.STARRED + " <> 0");
1400                break;
1401
1402            case LIVE_FOLDERS_PEOPLE_GROUP_NAME:
1403                qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
1404                qb.setProjectionMap(sLiveFoldersProjectionMap);
1405                qb.appendWhere(buildGroupNameMatchWhereClause(url.getLastPathSegment()));
1406                break;
1407
1408            default:
1409                throw new IllegalArgumentException("Unknown URL " + url);
1410        }
1411
1412        // run the query
1413        final SQLiteDatabase db = getDatabase();
1414        Cursor c = qb.query(db, projectionIn, selection, selectionArgs,
1415                groupBy, null, sort, limit);
1416        if ((c != null) && !isTemporary()) {
1417            c.setNotificationUri(getContext().getContentResolver(), notificationUri);
1418        }
1419        return c;
1420    }
1421
1422    /**
1423     * Gets the value of the "limit" URI query parameter.
1424     *
1425     * @return A string containing a non-negative integer, or <code>null</code> if
1426     *         the parameter is not set, or is set to an invalid value.
1427     */
1428    private String getLimit(Uri url) {
1429        String limit = url.getQueryParameter("limit");
1430        if (limit == null) {
1431            return null;
1432        }
1433        // make sure that the limit is a non-negative integer
1434        try {
1435            int l = Integer.parseInt(limit);
1436            if (l < 0) {
1437                Log.w(TAG, "Invalid limit parameter: " + limit);
1438                return null;
1439            }
1440            return String.valueOf(l);
1441        } catch (NumberFormatException ex) {
1442            Log.w(TAG, "Invalid limit parameter: " + limit);
1443            return null;
1444        }
1445    }
1446
1447    /**
1448     * Build a WHERE clause that restricts the query to match people that are a member of
1449     * a particular system group.  The projection map of the query must include {@link People#_ID}.
1450     *
1451     * @param groupSystemId The system group id (e.g {@link Groups#GROUP_MY_CONTACTS})
1452     * @return The where clause.
1453     */
1454    private CharSequence buildGroupSystemIdMatchWhereClause(String groupSystemId) {
1455        return "people._id IN (SELECT person FROM groupmembership JOIN groups " +
1456                "ON (group_id=groups._id OR " +
1457                "(group_sync_id = groups._sync_id AND " +
1458                    "group_sync_account = groups._sync_account)) "+
1459                "WHERE " + Groups.SYSTEM_ID + "="
1460                + DatabaseUtils.sqlEscapeString(groupSystemId) + ")";
1461    }
1462
1463    /**
1464     * Build a WHERE clause that restricts the query to match people that are a member of
1465     * a group with a particular name. The projection map of the query must include
1466     * {@link People#_ID}.
1467     *
1468     * @param groupName The name of the group
1469     * @return The where clause.
1470     */
1471    private CharSequence buildGroupNameMatchWhereClause(String groupName) {
1472        return "people._id IN (SELECT person FROM groupmembership JOIN groups " +
1473                "ON (group_id=groups._id OR " +
1474                "(group_sync_id = groups._sync_id AND " +
1475                    "group_sync_account = groups._sync_account)) "+
1476                "WHERE " + Groups.NAME + "="
1477                + DatabaseUtils.sqlEscapeString(groupName) + ")";
1478    }
1479
1480    private Cursor queryOwner(String[] projection) {
1481        // Check the permissions
1482        getContext().enforceCallingPermission("android.permission.READ_OWNER_DATA",
1483                "No permission to access owner info");
1484
1485        // Read the owner id
1486        SharedPreferences prefs = getContext().getSharedPreferences(PREFS_NAME_OWNER,
1487                Context.MODE_PRIVATE);
1488        long ownerId = prefs.getLong(PREF_OWNER_ID, 0);
1489
1490        // Run the query
1491        return queryInternal(ContentUris.withAppendedId(People.CONTENT_URI, ownerId), projection,
1492                null, null, null);
1493    }
1494
1495    /**
1496     * Append a string to a selection args array
1497     *
1498     * @param selectionArgs the old arg
1499     * @param newArg the new arg to append
1500     * @return a new string array with all of the args
1501     */
1502    private String[] appendSelectionArg(String[] selectionArgs, String newArg) {
1503        if (selectionArgs == null || selectionArgs.length == 0) {
1504            return new String[] { newArg };
1505        } else {
1506            int length = selectionArgs.length;
1507            String[] newArgs = new String[length + 1];
1508            System.arraycopy(selectionArgs, 0, newArgs, 0, length);
1509            newArgs[length] = newArg;
1510            return newArgs;
1511        }
1512    }
1513
1514    private HashMap<String, String> getCurrentSearchSuggestionsProjectionMap() {
1515        String currentLanguage = Locale.getDefault().getLanguage();
1516        synchronized (this) {
1517            if (!currentLanguage.equals(mSearchSuggestionLanguage)) {
1518                mSearchSuggestionLanguage = currentLanguage;
1519                updateSuggestColumnTexts();
1520            }
1521        }
1522        return mSearchSuggestionsProjectionMap;
1523    }
1524
1525    /**
1526     * Either sets up the query builder so we can run the proper query against the database
1527     * and returns null, or returns a cursor with the results already in it.
1528     *
1529     * @param url the URL passed for the suggestion
1530     * @param qb the query builder to use if a query needs to be run on the database
1531     * @return null with qb configured for a query, a cursor with the results already in it.
1532     */
1533    private Cursor handleSearchSuggestionsQuery(Uri url, SQLiteQueryBuilder qb) {
1534        qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN);
1535        qb.setProjectionMap(getCurrentSearchSuggestionsProjectionMap());
1536        if (url.getPathSegments().size() > 1) {
1537            // A search term was entered, use it to filter
1538
1539            // only match within 'my contacts'
1540            // TODO: match the 'display group' instead of hard coding 'my contacts'
1541            // once that information is factored out of the shared prefs of the contacts
1542            // app into this content provider.
1543            qb.appendWhere(buildGroupSystemIdMatchWhereClause(Groups.GROUP_MY_CONTACTS));
1544            qb.appendWhere(" AND ");
1545
1546            // match the query
1547            final String searchClause = url.getLastPathSegment();
1548            if (!TextUtils.isDigitsOnly(searchClause)) {
1549                qb.appendWhere(buildPeopleLookupWhereClauseForSuggestion(searchClause));
1550            } else {
1551                final String[] columnNames = new String[] {
1552                        "_id",
1553                        SearchManager.SUGGEST_COLUMN_TEXT_1,
1554                        SearchManager.SUGGEST_COLUMN_TEXT_2,
1555                        SearchManager.SUGGEST_COLUMN_ICON_1,
1556                        SearchManager.SUGGEST_COLUMN_INTENT_DATA,
1557                        SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
1558                        SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
1559                };
1560
1561                Resources r = getContext().getResources();
1562                String s;
1563                int i;
1564
1565                ArrayList<Object> dialNumber = new ArrayList<Object>();
1566                dialNumber.add(0);  // _id
1567                s = r.getString(com.android.internal.R.string.dial_number_using, searchClause);
1568                i = s.indexOf('\n');
1569                if (i < 0) {
1570                    dialNumber.add(s);
1571                    dialNumber.add("");
1572                } else {
1573                    dialNumber.add(s.substring(0, i));
1574                    dialNumber.add(s.substring(i + 1));
1575                }
1576                dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact));
1577                dialNumber.add("tel:" + searchClause);
1578                dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
1579                dialNumber.add(null);
1580
1581                ArrayList<Object> createContact = new ArrayList<Object>();
1582                createContact.add(1);  // _id
1583                s = r.getString(com.android.internal.R.string.create_contact_using, searchClause);
1584                i = s.indexOf('\n');
1585                if (i < 0) {
1586                    createContact.add(s);
1587                    createContact.add("");
1588                } else {
1589                    createContact.add(s.substring(0, i));
1590                    createContact.add(s.substring(i + 1));
1591                }
1592                createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact));
1593                createContact.add("tel:" + searchClause);
1594                createContact.add(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
1595                createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
1596
1597                ArrayList<ArrayList> rows = new ArrayList<ArrayList>();
1598                rows.add(dialNumber);
1599                rows.add(createContact);
1600
1601                ArrayListCursor cursor = new ArrayListCursor(columnNames, rows);
1602                return cursor;
1603            }
1604        }
1605        return null;
1606    }
1607
1608    @Override
1609    public String getType(Uri url) {
1610        int match = sURIMatcher.match(url);
1611        switch (match) {
1612            case EXTENSIONS:
1613            case PEOPLE_EXTENSIONS:
1614                return Extensions.CONTENT_TYPE;
1615            case EXTENSIONS_ID:
1616            case PEOPLE_EXTENSIONS_ID:
1617                return Extensions.CONTENT_ITEM_TYPE;
1618            case PEOPLE:
1619                return "vnd.android.cursor.dir/person";
1620            case PEOPLE_ID:
1621                return "vnd.android.cursor.item/person";
1622            case PEOPLE_PHONES:
1623                return "vnd.android.cursor.dir/phone";
1624            case PEOPLE_PHONES_ID:
1625                return "vnd.android.cursor.item/phone";
1626            case PEOPLE_CONTACTMETHODS:
1627                return "vnd.android.cursor.dir/contact-methods";
1628            case PEOPLE_CONTACTMETHODS_ID:
1629                return getContactMethodType(url);
1630            case PHONES:
1631                return "vnd.android.cursor.dir/phone";
1632            case PHONES_ID:
1633                return "vnd.android.cursor.item/phone";
1634            case PHONES_FILTER:
1635            case PHONES_FILTER_NAME:
1636            case PHONES_MOBILE_FILTER_NAME:
1637                return "vnd.android.cursor.dir/phone";
1638            case PHOTOS_ID:
1639                return "vnd.android.cursor.item/photo";
1640            case PHOTOS:
1641                return "vnd.android.cursor.dir/photo";
1642            case PEOPLE_PHOTO:
1643                return "vnd.android.cursor.item/photo";
1644            case PEOPLE_PHOTO_DATA:
1645                return "image/png";
1646            case CONTACTMETHODS:
1647                return "vnd.android.cursor.dir/contact-methods";
1648            case CONTACTMETHODS_ID:
1649                return getContactMethodType(url);
1650            case CONTACTMETHODS_EMAIL:
1651            case CONTACTMETHODS_EMAIL_FILTER:
1652                return "vnd.android.cursor.dir/email";
1653            case CALLS:
1654                return "vnd.android.cursor.dir/calls";
1655            case CALLS_ID:
1656                return "vnd.android.cursor.item/calls";
1657            case ORGANIZATIONS:
1658                return "vnd.android.cursor.dir/organizations";
1659            case ORGANIZATIONS_ID:
1660                return "vnd.android.cursor.item/organization";
1661            case CALLS_FILTER:
1662                return "vnd.android.cursor.dir/calls";
1663            case SEARCH_SUGGESTIONS:
1664                return SearchManager.SUGGEST_MIME_TYPE;
1665            case SEARCH_SHORTCUT:
1666                return SearchManager.SHORTCUT_MIME_TYPE;
1667            default:
1668                throw new IllegalArgumentException("Unknown URL");
1669        }
1670    }
1671
1672    private String getContactMethodType(Uri url)
1673    {
1674        String mime = null;
1675
1676        Cursor c = query(url, new String[] {ContactMethods.KIND}, null, null, null);
1677        if (c != null) {
1678            try {
1679                if (c.moveToFirst()) {
1680                    int kind = c.getInt(0);
1681                    switch (kind) {
1682                    case Contacts.KIND_EMAIL:
1683                        mime = "vnd.android.cursor.item/email";
1684                        break;
1685
1686                    case Contacts.KIND_IM:
1687                        mime = "vnd.android.cursor.item/jabber-im";
1688                        break;
1689
1690                    case Contacts.KIND_POSTAL:
1691                        mime = "vnd.android.cursor.item/postal-address";
1692                        break;
1693                    }
1694                }
1695            } finally {
1696                c.close();
1697            }
1698        }
1699        return mime;
1700    }
1701
1702    private ContentValues queryAndroidStarredGroupId(String account) {
1703        String whereString;
1704        String[] whereArgs;
1705        if (!TextUtils.isEmpty(account)) {
1706            whereString = "_sync_account=? AND name=?";
1707            whereArgs = new String[]{account, Groups.GROUP_ANDROID_STARRED};
1708        } else {
1709            whereString = "_sync_account is null AND name=?";
1710            whereArgs = new String[]{Groups.GROUP_ANDROID_STARRED};
1711        }
1712        Cursor cursor = getDatabase().query(sGroupsTable,
1713                new String[]{Groups._ID, Groups._SYNC_ID, Groups._SYNC_ACCOUNT},
1714                whereString, whereArgs, null, null, null);
1715        try {
1716            if (cursor.moveToNext()) {
1717                ContentValues result = new ContentValues();
1718                result.put(Groups._ID, cursor.getLong(0));
1719                result.put(Groups._SYNC_ID, cursor.getString(1));
1720                result.put(Groups._SYNC_ACCOUNT, cursor.getString(2));
1721                return result;
1722            }
1723            return null;
1724        } finally {
1725            cursor.close();
1726        }
1727    }
1728
1729    @Override
1730    public Uri insertInternal(Uri url, ContentValues initialValues) {
1731        Uri resultUri = null;
1732        long rowID;
1733
1734        final SQLiteDatabase db = getDatabase();
1735        int match = sURIMatcher.match(url);
1736        switch (match) {
1737            case PEOPLE_GROUPMEMBERSHIP:
1738            case GROUPMEMBERSHIP: {
1739                mValues.clear();
1740                mValues.putAll(initialValues);
1741                if (match == PEOPLE_GROUPMEMBERSHIP) {
1742                    mValues.put(GroupMembership.PERSON_ID,
1743                            Long.valueOf(url.getPathSegments().get(1)));
1744                }
1745                resultUri = insertIntoGroupmembership(mValues);
1746            }
1747            break;
1748
1749            case PEOPLE_OWNER:
1750                return insertOwner(initialValues);
1751
1752            case PEOPLE_EXTENSIONS:
1753            case EXTENSIONS: {
1754                ContentValues newMap = new ContentValues(initialValues);
1755                if (match == PEOPLE_EXTENSIONS) {
1756                    newMap.put(Extensions.PERSON_ID,
1757                            Long.valueOf(url.getPathSegments().get(1)));
1758                }
1759                rowID = mExtensionsInserter.insert(newMap);
1760                if (rowID > 0) {
1761                    resultUri = ContentUris.withAppendedId(Extensions.CONTENT_URI, rowID);
1762                }
1763            }
1764            break;
1765
1766            case PHOTOS: {
1767                if (!isTemporary()) {
1768                    throw new UnsupportedOperationException();
1769                }
1770                rowID = mPhotosInserter.insert(initialValues);
1771                if (rowID > 0) {
1772                    resultUri = ContentUris.withAppendedId(Photos.CONTENT_URI, rowID);
1773                }
1774            }
1775            break;
1776
1777            case GROUPS: {
1778                ContentValues newMap = new ContentValues(initialValues);
1779                ensureSyncAccountIsSet(newMap);
1780                newMap.put(Groups._SYNC_DIRTY, 1);
1781                // Insert into the groups table
1782                rowID = mGroupsInserter.insert(newMap);
1783                if (rowID > 0) {
1784                    resultUri = ContentUris.withAppendedId(Groups.CONTENT_URI, rowID);
1785                    if (!isTemporary() && newMap.containsKey(Groups.SHOULD_SYNC)) {
1786                        final String account = newMap.getAsString(Groups._SYNC_ACCOUNT);
1787                        if (!TextUtils.isEmpty(account)) {
1788                            final ContentResolver cr = getContext().getContentResolver();
1789                            onLocalChangesForAccount(cr, account, false);
1790                        }
1791                    }
1792                }
1793            }
1794            break;
1795
1796            case PEOPLE_RAW:
1797            case PEOPLE: {
1798                mValues.clear();
1799                mValues.putAll(initialValues);
1800                ensureSyncAccountIsSet(mValues);
1801                mValues.put(People._SYNC_DIRTY, 1);
1802                // Insert into the people table
1803                rowID = mPeopleInserter.insert(mValues);
1804                if (rowID > 0) {
1805                    resultUri = ContentUris.withAppendedId(People.CONTENT_URI, rowID);
1806                    if (!isTemporary()) {
1807                        String account = mValues.getAsString(People._SYNC_ACCOUNT);
1808                        Long starredValue = mValues.getAsLong(People.STARRED);
1809                        final String syncId = mValues.getAsString(People._SYNC_ID);
1810                        boolean isStarred = starredValue != null && starredValue != 0;
1811                        fixupGroupMembershipAfterPeopleUpdate(account, rowID, isStarred);
1812                        // create a photo row for this person
1813                        mDb.delete(sPhotosTable, "person=" + rowID, null);
1814                        mValues.clear();
1815                        mValues.put(Photos.PERSON_ID, rowID);
1816                        mValues.put(Photos._SYNC_ACCOUNT, account);
1817                        mValues.put(Photos._SYNC_ID, syncId);
1818                        mValues.put(Photos._SYNC_DIRTY, 0);
1819                        mPhotosInserter.insert(mValues);
1820                    }
1821                }
1822            }
1823            break;
1824
1825            case DELETED_PEOPLE: {
1826                if (isTemporary()) {
1827                    // Insert into the people table
1828                    rowID = db.insert("_deleted_people", "_sync_id", initialValues);
1829                    if (rowID > 0) {
1830                        resultUri = Uri.parse("content://contacts/_deleted_people/" + rowID);
1831                    }
1832                } else {
1833                    throw new UnsupportedOperationException();
1834                }
1835            }
1836            break;
1837
1838            case DELETED_GROUPS: {
1839                if (isTemporary()) {
1840                    rowID = db.insert(sDeletedGroupsTable, Groups._SYNC_ID,
1841                            initialValues);
1842                    if (rowID > 0) {
1843                        resultUri =ContentUris.withAppendedId(
1844                                Groups.DELETED_CONTENT_URI, rowID);
1845                    }
1846                } else {
1847                    throw new UnsupportedOperationException();
1848                }
1849            }
1850            break;
1851
1852            case PEOPLE_PHONES:
1853            case PHONES: {
1854                mValues.clear();
1855                mValues.putAll(initialValues);
1856                if (match == PEOPLE_PHONES) {
1857                    mValues.put(Contacts.Phones.PERSON_ID,
1858                            Long.valueOf(url.getPathSegments().get(1)));
1859                }
1860                String number = mValues.getAsString(Contacts.Phones.NUMBER);
1861                if (number != null) {
1862                    mValues.put("number_key", PhoneNumberUtils.getStrippedReversed(number));
1863                }
1864
1865                rowID = insertAndFixupPrimary(Contacts.KIND_PHONE, mValues);
1866                resultUri = ContentUris.withAppendedId(Phones.CONTENT_URI, rowID);
1867            }
1868            break;
1869
1870            case CONTACTMETHODS:
1871            case PEOPLE_CONTACTMETHODS: {
1872                mValues.clear();
1873                mValues.putAll(initialValues);
1874                if (match == PEOPLE_CONTACTMETHODS) {
1875                    mValues.put("person", url.getPathSegments().get(1));
1876                }
1877                Integer kind = mValues.getAsInteger(ContactMethods.KIND);
1878                if (kind == null) {
1879                    throw new IllegalArgumentException("you must specify the ContactMethods.KIND");
1880                }
1881                rowID = insertAndFixupPrimary(kind, mValues);
1882                if (rowID > 0) {
1883                    resultUri = ContentUris.withAppendedId(ContactMethods.CONTENT_URI, rowID);
1884                }
1885            }
1886            break;
1887
1888            case CALLS: {
1889                rowID = mCallsInserter.insert(initialValues);
1890                if (rowID > 0) {
1891                    resultUri = Uri.parse("content://call_log/calls/" + rowID);
1892                }
1893            }
1894            break;
1895
1896            case PRESENCE: {
1897                final String handle = initialValues.getAsString(Presence.IM_HANDLE);
1898                final String protocol = initialValues.getAsString(Presence.IM_PROTOCOL);
1899                if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
1900                    throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required");
1901                }
1902
1903                // Look for the contact for this presence update
1904                StringBuilder query = new StringBuilder("SELECT ");
1905                query.append(ContactMethods.PERSON_ID);
1906                query.append(" FROM contact_methods WHERE (kind=");
1907                query.append(Contacts.KIND_IM);
1908                query.append(" AND ");
1909                query.append(ContactMethods.DATA);
1910                query.append("=? AND ");
1911                query.append(ContactMethods.AUX_DATA);
1912                query.append("=?)");
1913
1914                String[] selectionArgs;
1915                if (GTALK_PROTOCOL_STRING.equals(protocol)) {
1916                    // For gtalk accounts we usually don't have an explicit IM
1917                    // entry, so also look for the email address as well
1918                    query.append(" OR (");
1919                    query.append("kind=");
1920                    query.append(Contacts.KIND_EMAIL);
1921                    query.append(" AND ");
1922                    query.append(ContactMethods.DATA);
1923                    query.append("=?)");
1924                    selectionArgs = new String[] { handle, protocol, handle };
1925                } else {
1926                    selectionArgs = new String[] { handle, protocol };
1927                }
1928
1929                Cursor c = db.rawQueryWithFactory(null, query.toString(), selectionArgs, null);
1930
1931                long personId = 0;
1932                try {
1933                    if (c.moveToFirst()) {
1934                        personId = c.getLong(0);
1935                    } else {
1936                        // No contact found, return a null URI
1937                        return null;
1938                    }
1939                } finally {
1940                    c.close();
1941                }
1942
1943                mValues.clear();
1944                mValues.putAll(initialValues);
1945                mValues.put(Presence.PERSON_ID, personId);
1946
1947                // Insert the presence update
1948                rowID = db.replace("presence", null, mValues);
1949                if (rowID > 0) {
1950                    resultUri = Uri.parse("content://contacts/presence/" + rowID);
1951                }
1952            }
1953            break;
1954
1955            case PEOPLE_ORGANIZATIONS:
1956            case ORGANIZATIONS: {
1957                ContentValues newMap = new ContentValues(initialValues);
1958                if (match == PEOPLE_ORGANIZATIONS) {
1959                    newMap.put(Contacts.Phones.PERSON_ID,
1960                            Long.valueOf(url.getPathSegments().get(1)));
1961                }
1962                rowID = insertAndFixupPrimary(Contacts.KIND_ORGANIZATION, newMap);
1963                if (rowID > 0) {
1964                    resultUri = Uri.parse("content://contacts/organizations/" + rowID);
1965                }
1966            }
1967            break;
1968            default:
1969                throw new UnsupportedOperationException("Cannot insert into URL: " + url);
1970        }
1971
1972        return resultUri;
1973    }
1974
1975    @Override
1976    protected void onAccountsChanged(String[] accountsArray) {
1977        super.onAccountsChanged(accountsArray);
1978        synchronized (mAccountsLock) {
1979            mAccounts = new String[accountsArray.length];
1980            System.arraycopy(accountsArray, 0, mAccounts, 0, mAccounts.length);
1981        }
1982    }
1983
1984    private void ensureSyncAccountIsSet(ContentValues values) {
1985        synchronized (mAccountsLock) {
1986            String account = values.getAsString(SyncConstValue._SYNC_ACCOUNT);
1987            if (account == null && mAccounts.length > 0) {
1988                values.put(SyncConstValue._SYNC_ACCOUNT, mAccounts[0]);
1989            }
1990        }
1991    }
1992
1993    private Uri insertOwner(ContentValues values) {
1994        // Check the permissions
1995        getContext().enforceCallingPermission("android.permission.WRITE_OWNER_DATA",
1996                "No permission to set owner info");
1997
1998        // Insert the owner info
1999        Uri uri = insertInternal(People.CONTENT_URI, values);
2000
2001        // Record which person is the owner
2002        long id = ContentUris.parseId(uri);
2003        SharedPreferences.Editor prefs = getContext().getSharedPreferences(PREFS_NAME_OWNER,
2004                Context.MODE_PRIVATE).edit();
2005        prefs.putLong(PREF_OWNER_ID, id);
2006        prefs.commit();
2007        return uri;
2008    }
2009
2010    private Uri insertIntoGroupmembership(ContentValues values) {
2011        String groupSyncAccount = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT);
2012        String groupSyncId = values.getAsString(GroupMembership.GROUP_SYNC_ID);
2013        final Long personId = values.getAsLong(GroupMembership.PERSON_ID);
2014        if (!values.containsKey(GroupMembership.GROUP_ID)) {
2015            if (TextUtils.isEmpty(groupSyncAccount) || TextUtils.isEmpty(groupSyncId)) {
2016                throw new IllegalArgumentException(
2017                        "insertIntoGroupmembership: no GROUP_ID wasn't specified and non-empty "
2018                        + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT fields weren't specifid, "
2019                        + values);
2020            }
2021            if (0 != DatabaseUtils.longForQuery(getDatabase(), ""
2022                    + "SELECT COUNT(*) "
2023                    + "FROM groupmembership "
2024                    + "WHERE group_sync_id=? AND person=?",
2025                    new String[]{groupSyncId, String.valueOf(personId)})) {
2026                final String errorMessage =
2027                        "insertIntoGroupmembership: a row with this server key already exists, "
2028                                + values;
2029                if (Config.LOGD) Log.d(TAG, errorMessage);
2030                return null;
2031            }
2032        } else {
2033            long groupId = values.getAsLong(GroupMembership.GROUP_ID);
2034            if (!TextUtils.isEmpty(groupSyncAccount) || !TextUtils.isEmpty(groupSyncId)) {
2035                throw new IllegalArgumentException(
2036                        "insertIntoGroupmembership: GROUP_ID was specified but "
2037                        + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT fields were also specifid, "
2038                        + values);
2039            }
2040            if (0 != DatabaseUtils.longForQuery(getDatabase(),
2041                    "SELECT COUNT(*) FROM groupmembership where group_id=? AND person=?",
2042                    new String[]{String.valueOf(groupId), String.valueOf(personId)})) {
2043                final String errorMessage =
2044                        "insertIntoGroupmembership: a row with this local key already exists, "
2045                                + values;
2046                if (Config.LOGD) Log.d(TAG, errorMessage);
2047                return null;
2048            }
2049        }
2050
2051        long rowId = mGroupMembershipInserter.insert(values);
2052        if (rowId <= 0) {
2053            final String errorMessage = "insertIntoGroupmembership: the insert failed, values are "
2054                    + values;
2055            if (Config.LOGD) Log.d(TAG, errorMessage);
2056            return null;
2057        }
2058
2059        // set the STARRED column in the people row if this group is the GROUP_ANDROID_STARRED
2060        if (!isTemporary() && queryGroupMembershipContainsStarred(personId)) {
2061            fixupPeopleStarred(personId, true);
2062        }
2063
2064        return ContentUris.withAppendedId(GroupMembership.CONTENT_URI, rowId);
2065    }
2066
2067    private void fixupGroupMembershipAfterPeopleUpdate(String account, long personId,
2068            boolean makeStarred) {
2069        ContentValues starredGroupInfo = queryAndroidStarredGroupId(account);
2070        if (makeStarred) {
2071            if (starredGroupInfo == null) {
2072                // we need to add the starred group
2073                mValuesLocal.clear();
2074                mValuesLocal.put(Groups.NAME, Groups.GROUP_ANDROID_STARRED);
2075                mValuesLocal.put(Groups._SYNC_DIRTY, 1);
2076                mValuesLocal.put(Groups._SYNC_ACCOUNT, account);
2077                long groupId = mGroupsInserter.insert(mValuesLocal);
2078                starredGroupInfo = new ContentValues();
2079                starredGroupInfo.put(Groups._ID, groupId);
2080                starredGroupInfo.put(Groups._SYNC_ACCOUNT, account);
2081                // don't put the _SYNC_ID in here since we don't know it yet
2082            }
2083
2084            final Long groupId = starredGroupInfo.getAsLong(Groups._ID);
2085            final String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID);
2086            final String syncAccount = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
2087
2088            // check that either groupId is set or the syncId/Account is set
2089            final boolean hasSyncId = !TextUtils.isEmpty(syncId);
2090            final boolean hasGroupId = groupId != null;
2091            if (!hasGroupId && !hasSyncId) {
2092                throw new IllegalStateException("at least one of the groupId or "
2093                        + "the syncId must be set, " + starredGroupInfo);
2094            }
2095
2096            // now add this person to the group
2097            mValuesLocal.clear();
2098            mValuesLocal.put(GroupMembership.PERSON_ID, personId);
2099            mValuesLocal.put(GroupMembership.GROUP_ID, groupId);
2100            mValuesLocal.put(GroupMembership.GROUP_SYNC_ID, syncId);
2101            mValuesLocal.put(GroupMembership.GROUP_SYNC_ACCOUNT, syncAccount);
2102            mGroupMembershipInserter.insert(mValuesLocal);
2103        } else {
2104            if (starredGroupInfo != null) {
2105                // delete the groupmembership rows for this person that match the starred group id
2106                String syncAccount = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
2107                String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID);
2108                if (!TextUtils.isEmpty(syncId)) {
2109                    mDb.delete(sGroupmembershipTable,
2110                            "person=? AND group_sync_id=? AND group_sync_account=?",
2111                            new String[]{String.valueOf(personId), syncId, syncAccount});
2112                } else {
2113                    mDb.delete(sGroupmembershipTable, "person=? AND group_id=?",
2114                            new String[]{
2115                                    Long.toString(personId),
2116                                    Long.toString(starredGroupInfo.getAsLong(Groups._ID))});
2117                }
2118            }
2119        }
2120    }
2121
2122    private int fixupPeopleStarred(long personId, boolean inStarredGroup) {
2123        mValuesLocal.clear();
2124        mValuesLocal.put(People.STARRED, inStarredGroup ? 1 : 0);
2125        return getDatabase().update(sPeopleTable, mValuesLocal, WHERE_ID,
2126                new String[]{String.valueOf(personId)});
2127    }
2128
2129    private String kindToTable(int kind) {
2130        switch (kind) {
2131            case Contacts.KIND_EMAIL: return sContactMethodsTable;
2132            case Contacts.KIND_POSTAL: return sContactMethodsTable;
2133            case Contacts.KIND_IM: return sContactMethodsTable;
2134            case Contacts.KIND_PHONE: return sPhonesTable;
2135            case Contacts.KIND_ORGANIZATION: return sOrganizationsTable;
2136            default: throw new IllegalArgumentException("unknown kind, " + kind);
2137        }
2138    }
2139
2140    private DatabaseUtils.InsertHelper kindToInserter(int kind) {
2141        switch (kind) {
2142            case Contacts.KIND_EMAIL: return mContactMethodsInserter;
2143            case Contacts.KIND_POSTAL: return mContactMethodsInserter;
2144            case Contacts.KIND_IM: return mContactMethodsInserter;
2145            case Contacts.KIND_PHONE: return mPhonesInserter;
2146            case Contacts.KIND_ORGANIZATION: return mOrganizationsInserter;
2147            default: throw new IllegalArgumentException("unknown kind, " + kind);
2148        }
2149    }
2150
2151    private long insertAndFixupPrimary(int kind, ContentValues values) {
2152        final String table = kindToTable(kind);
2153        boolean isPrimary = false;
2154        Long personId = null;
2155
2156        if (!isTemporary()) {
2157            // when you add a item, if isPrimary or if there is no primary,
2158            // make this it, set the isPrimary flag, and clear other primary flags
2159            isPrimary = values.containsKey("isprimary")
2160                    && (values.getAsInteger("isprimary") != 0);
2161            personId = values.getAsLong("person");
2162            if (!isPrimary) {
2163                // make it primary anyway if this person doesn't have any rows of this type yet
2164                StringBuilder sb = new StringBuilder("person=" + personId);
2165                if (sContactMethodsTable.equals(table)) {
2166                    sb.append(" AND kind=");
2167                    sb.append(kind);
2168                }
2169                final boolean isFirstRowOfType = DatabaseUtils.longForQuery(getDatabase(),
2170                        "SELECT count(*) FROM " + table + " where " + sb.toString(), null) == 0;
2171                isPrimary = isFirstRowOfType;
2172            }
2173
2174            values.put("isprimary", isPrimary ? 1 : 0);
2175        }
2176
2177        // do the actual insert
2178        long newRowId = kindToInserter(kind).insert(values);
2179
2180        if (newRowId <= 0) {
2181            throw new RuntimeException("error while inserting into " + table + ", " + values);
2182        }
2183
2184        if (!isTemporary()) {
2185            // If this row was made the primary then clear the other isprimary flags and update
2186            // corresponding people row, if necessary.
2187            if (isPrimary) {
2188                clearOtherIsPrimary(kind, personId, newRowId);
2189                if (kind == Contacts.KIND_PHONE) {
2190                    updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newRowId);
2191                } else if (kind == Contacts.KIND_EMAIL) {
2192                    updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newRowId);
2193                } else if (kind == Contacts.KIND_ORGANIZATION) {
2194                    updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newRowId);
2195                }
2196            }
2197        }
2198
2199        return newRowId;
2200    }
2201
2202    @Override
2203    public int deleteInternal(Uri url, String userWhere, String[] whereArgs) {
2204        String tableToChange;
2205        String changedItemId;
2206
2207        final int matchedUriId = sURIMatcher.match(url);
2208        switch (matchedUriId) {
2209            case GROUPMEMBERSHIP_ID:
2210                return deleteFromGroupMembership(Long.parseLong(url.getPathSegments().get(1)),
2211                        userWhere, whereArgs);
2212            case GROUPS:
2213                return deleteFromGroups(userWhere, whereArgs);
2214            case GROUPS_ID:
2215                changedItemId = url.getPathSegments().get(1);
2216                return deleteFromGroups(addIdToWhereClause(changedItemId, userWhere), whereArgs);
2217            case EXTENSIONS:
2218                tableToChange = sExtensionsTable;
2219                changedItemId = null;
2220                break;
2221            case EXTENSIONS_ID:
2222                tableToChange = sExtensionsTable;
2223                changedItemId = url.getPathSegments().get(1);
2224                break;
2225            case PEOPLE_RAW:
2226            case PEOPLE:
2227                return deleteFromPeople(null, userWhere, whereArgs);
2228            case PEOPLE_ID:
2229                return deleteFromPeople(url.getPathSegments().get(1), userWhere, whereArgs);
2230            case PEOPLE_PHONES_ID:
2231                tableToChange = sPhonesTable;
2232                changedItemId = url.getPathSegments().get(3);
2233                break;
2234            case PEOPLE_CONTACTMETHODS_ID:
2235                tableToChange = sContactMethodsTable;
2236                changedItemId = url.getPathSegments().get(3);
2237                break;
2238            case PHONES_ID:
2239                tableToChange = sPhonesTable;
2240                changedItemId = url.getPathSegments().get(1);
2241                break;
2242            case ORGANIZATIONS_ID:
2243                tableToChange = sOrganizationsTable;
2244                changedItemId = url.getPathSegments().get(1);
2245                break;
2246            case CONTACTMETHODS_ID:
2247                tableToChange = sContactMethodsTable;
2248                changedItemId = url.getPathSegments().get(1);
2249                break;
2250            case PRESENCE:
2251                tableToChange = "presence";
2252                changedItemId = null;
2253                break;
2254            case CALLS:
2255                tableToChange = "calls";
2256                changedItemId = null;
2257                break;
2258            default:
2259                throw new UnsupportedOperationException("Cannot delete that URL: " + url);
2260        }
2261
2262        String where = addIdToWhereClause(changedItemId, userWhere);
2263        IsPrimaryInfo oldPrimaryInfo = null;
2264        switch (matchedUriId) {
2265            case PEOPLE_PHONES_ID:
2266            case PHONES_ID:
2267            case ORGANIZATIONS_ID:
2268                oldPrimaryInfo = lookupIsPrimaryInfo(tableToChange,
2269                        sIsPrimaryProjectionWithoutKind, where, whereArgs);
2270                break;
2271
2272            case PEOPLE_CONTACTMETHODS_ID:
2273            case CONTACTMETHODS_ID:
2274                oldPrimaryInfo = lookupIsPrimaryInfo(tableToChange,
2275                        sIsPrimaryProjectionWithKind, where, whereArgs);
2276                break;
2277        }
2278
2279        final SQLiteDatabase db = getDatabase();
2280        int count = db.delete(tableToChange, where, whereArgs);
2281        if (count > 0) {
2282            if (oldPrimaryInfo != null && oldPrimaryInfo.isPrimary) {
2283                fixupPrimaryAfterDelete(oldPrimaryInfo.kind,
2284                        oldPrimaryInfo.id, oldPrimaryInfo.person);
2285            }
2286        }
2287
2288        return count;
2289    }
2290
2291    @Override
2292    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
2293        int match = sURIMatcher.match(uri);
2294        switch (match) {
2295            case PEOPLE_PHOTO_DATA:
2296                if (!"r".equals(mode)) {
2297                    throw new FileNotFoundException("Mode " + mode + " not supported.");
2298                }
2299                String person = uri.getPathSegments().get(1);
2300                String sql = "SELECT " + Photos.DATA + " FROM " + sPhotosTable
2301                        + " WHERE " + Photos.PERSON_ID + "=?";
2302                String[] selectionArgs = { person };
2303                return SQLiteContentHelper.getBlobColumnAsAssetFile(getDatabase(), sql,
2304                        selectionArgs);
2305            default:
2306                throw new FileNotFoundException("No file at: " + uri);
2307        }
2308    }
2309
2310    @Override
2311    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
2312        int match = sURIMatcher.match(uri);
2313        switch (match) {
2314            default:
2315                throw new UnsupportedOperationException(uri.toString());
2316        }
2317    }
2318
2319    private int deleteFromGroupMembership(long rowId, String where, String[] whereArgs) {
2320        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2321        qb.setTables("groups, groupmembership");
2322        qb.setProjectionMap(sGroupMembershipProjectionMap);
2323        qb.appendWhere(sGroupsJoinString);
2324        qb.appendWhere(" AND groupmembership._id=" + rowId);
2325        Cursor cursor = qb.query(getDatabase(), null, where, whereArgs, null, null, null);
2326        try {
2327            final int indexPersonId = cursor.getColumnIndexOrThrow(GroupMembership.PERSON_ID);
2328            final int indexName = cursor.getColumnIndexOrThrow(GroupMembership.NAME);
2329            while (cursor.moveToNext()) {
2330                if (Groups.GROUP_ANDROID_STARRED.equals(cursor.getString(indexName))) {
2331                    fixupPeopleStarred(cursor.getLong(indexPersonId), false);
2332                }
2333            }
2334        } finally {
2335            cursor.close();
2336        }
2337
2338        return mDb.delete(sGroupmembershipTable,
2339                addIdToWhereClause(String.valueOf(rowId), where),
2340                whereArgs);
2341    }
2342
2343    private int deleteFromPeople(String rowId, String where, String[] whereArgs) {
2344        final SQLiteDatabase db = getDatabase();
2345        where = addIdToWhereClause(rowId, where);
2346        Cursor cursor = db.query(sPeopleTable, null, where, whereArgs, null, null, null);
2347        try {
2348            final int idxSyncId = cursor.getColumnIndexOrThrow(People._SYNC_ID);
2349            final int idxSyncAccount = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
2350            final int idxSyncVersion = cursor.getColumnIndexOrThrow(People._SYNC_VERSION);
2351            final int dstIdxSyncId = mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ID);
2352            final int dstIdxSyncAccount =
2353                    mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ACCOUNT);
2354            final int dstIdxSyncVersion =
2355                    mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_VERSION);
2356            while (cursor.moveToNext()) {
2357                final String syncId = cursor.getString(idxSyncId);
2358                if (TextUtils.isEmpty(syncId)) continue;
2359                // insert into deleted table
2360                mDeletedPeopleInserter.prepareForInsert();
2361                mDeletedPeopleInserter.bind(dstIdxSyncId, syncId);
2362                mDeletedPeopleInserter.bind(dstIdxSyncAccount, cursor.getString(idxSyncAccount));
2363                mDeletedPeopleInserter.bind(dstIdxSyncVersion, cursor.getString(idxSyncVersion));
2364                mDeletedPeopleInserter.execute();
2365            }
2366        } finally {
2367            cursor.close();
2368        }
2369
2370        // perform the actual delete
2371        return db.delete(sPeopleTable, where, whereArgs);
2372    }
2373
2374    private int deleteFromGroups(String where, String[] whereArgs) {
2375        HashSet<String> modifiedAccounts = Sets.newHashSet();
2376        Cursor cursor = getDatabase().query(sGroupsTable, null, where, whereArgs,
2377                null, null, null);
2378        try {
2379            final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME);
2380            final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT);
2381            final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID);
2382            final int indexId = cursor.getColumnIndexOrThrow(Groups._ID);
2383            final int indexShouldSync = cursor.getColumnIndexOrThrow(Groups.SHOULD_SYNC);
2384            while (cursor.moveToNext()) {
2385                String oldName = cursor.getString(indexName);
2386                String syncAccount = cursor.getString(indexSyncAccount);
2387                String syncId = cursor.getString(indexSyncId);
2388                boolean shouldSync = cursor.getLong(indexShouldSync) != 0;
2389                long id = cursor.getLong(indexId);
2390                fixupPeopleStarredOnGroupRename(oldName, null, id);
2391                if (!TextUtils.isEmpty(syncAccount) && !TextUtils.isEmpty(syncId)) {
2392                    fixupPeopleStarredOnGroupRename(oldName, null, syncAccount, syncId);
2393                }
2394                if (!TextUtils.isEmpty(syncAccount) && shouldSync) {
2395                    modifiedAccounts.add(syncAccount);
2396                }
2397            }
2398        } finally {
2399            cursor.close();
2400        }
2401
2402        int numRows = mDb.delete(sGroupsTable, where, whereArgs);
2403        if (numRows > 0) {
2404            if (!isTemporary()) {
2405                final ContentResolver cr = getContext().getContentResolver();
2406                for (String account : modifiedAccounts) {
2407                    onLocalChangesForAccount(cr, account, true);
2408                }
2409            }
2410        }
2411        return numRows;
2412    }
2413
2414    /**
2415     * Called when local changes are made, so subclasses have
2416     * an opportunity to react as they see fit.
2417     *
2418     * @param resolver the content resolver to use
2419     * @param account the account the changes are tied to
2420     */
2421    protected void onLocalChangesForAccount(final ContentResolver resolver, String account,
2422            boolean groupsModified) {
2423        // Do nothing
2424    }
2425
2426    private void fixupPrimaryAfterDelete(int kind, Long itemId, Long personId) {
2427        final String table = kindToTable(kind);
2428        // when you delete an item with isPrimary,
2429        // select a new one as isPrimary and clear the primary if no more items
2430        Long newPrimaryId = findNewPrimary(kind, personId, itemId);
2431
2432        // we found a new primary, set its isprimary flag
2433        if (newPrimaryId != null) {
2434            mValuesLocal.clear();
2435            mValuesLocal.put("isprimary", 1);
2436            if (getDatabase().update(table, mValuesLocal, "_id=" + newPrimaryId, null) != 1) {
2437                throw new RuntimeException("error updating " + table + ", _id "
2438                        + newPrimaryId + ", values " + mValuesLocal);
2439            }
2440        }
2441
2442        // if this kind's primary status should be reflected in the people row, update it
2443        if (kind == Contacts.KIND_PHONE) {
2444            updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newPrimaryId);
2445        } else if (kind == Contacts.KIND_EMAIL) {
2446            updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newPrimaryId);
2447        } else if (kind == Contacts.KIND_ORGANIZATION) {
2448            updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newPrimaryId);
2449        }
2450    }
2451
2452    @Override
2453    public int updateInternal(Uri url, ContentValues values, String userWhere, String[] whereArgs) {
2454        final SQLiteDatabase db = getDatabase();
2455        String tableToChange;
2456        String changedItemId;
2457        final int matchedUriId = sURIMatcher.match(url);
2458        switch (matchedUriId) {
2459            case GROUPS_ID:
2460                changedItemId = url.getPathSegments().get(1);
2461                return updateGroups(values,
2462                        addIdToWhereClause(changedItemId, userWhere), whereArgs);
2463
2464            case PEOPLE_EXTENSIONS_ID:
2465                tableToChange = sExtensionsTable;
2466                changedItemId = url.getPathSegments().get(3);
2467                break;
2468
2469            case EXTENSIONS_ID:
2470                tableToChange = sExtensionsTable;
2471                changedItemId = url.getPathSegments().get(1);
2472                break;
2473
2474            case PEOPLE_UPDATE_CONTACT_TIME:
2475                if (values.size() != 1 || !values.containsKey(People.LAST_TIME_CONTACTED)) {
2476                    throw new IllegalArgumentException(
2477                            "You may only use " + url + " to update People.LAST_TIME_CONTACTED");
2478                }
2479                tableToChange = sPeopleTable;
2480                changedItemId = url.getPathSegments().get(1);
2481                break;
2482
2483            case PEOPLE_ID:
2484                mValues.clear();
2485                mValues.putAll(values);
2486                mValues.put(Photos._SYNC_DIRTY, 1);
2487                values = mValues;
2488                tableToChange = sPeopleTable;
2489                changedItemId = url.getPathSegments().get(1);
2490                break;
2491
2492            case PEOPLE_PHONES_ID:
2493                tableToChange = sPhonesTable;
2494                changedItemId = url.getPathSegments().get(3);
2495                break;
2496
2497            case PEOPLE_CONTACTMETHODS_ID:
2498                tableToChange = sContactMethodsTable;
2499                changedItemId = url.getPathSegments().get(3);
2500                break;
2501
2502            case PHONES_ID:
2503                tableToChange = sPhonesTable;
2504                changedItemId = url.getPathSegments().get(1);
2505                break;
2506
2507            case PEOPLE_PHOTO:
2508            case PHOTOS_ID:
2509                mValues.clear();
2510                mValues.putAll(values);
2511
2512                // The _SYNC_DIRTY flag should only be set if the data was modified and if
2513                // it isn't already provided.
2514                if (!mValues.containsKey(Photos._SYNC_DIRTY) && mValues.containsKey(Photos.DATA)) {
2515                    mValues.put(Photos._SYNC_DIRTY, 1);
2516                }
2517                StringBuilder where;
2518                if (matchedUriId == PEOPLE_PHOTO) {
2519                    where = new StringBuilder("_id=" + url.getPathSegments().get(1));
2520                } else {
2521                    where = new StringBuilder("person=" + url.getPathSegments().get(1));
2522                }
2523                if (!TextUtils.isEmpty(userWhere)) {
2524                    where.append(" AND (");
2525                    where.append(userWhere);
2526                    where.append(')');
2527                }
2528                return db.update(sPhotosTable, mValues, where.toString(), whereArgs);
2529
2530            case ORGANIZATIONS_ID:
2531                tableToChange = sOrganizationsTable;
2532                changedItemId = url.getPathSegments().get(1);
2533                break;
2534
2535            case CONTACTMETHODS_ID:
2536                tableToChange = sContactMethodsTable;
2537                changedItemId = url.getPathSegments().get(1);
2538                break;
2539
2540            case SETTINGS:
2541                if (whereArgs != null) {
2542                    throw new IllegalArgumentException(
2543                            "you aren't allowed to specify where args when updating settings");
2544                }
2545                if (userWhere != null) {
2546                    throw new IllegalArgumentException(
2547                            "you aren't allowed to specify a where string when updating settings");
2548                }
2549                return updateSettings(values);
2550
2551            case CALLS:
2552                tableToChange = "calls";
2553                changedItemId = null;
2554                break;
2555
2556            case CALLS_ID:
2557                tableToChange = "calls";
2558                changedItemId = url.getPathSegments().get(1);
2559                break;
2560
2561            default:
2562                throw new UnsupportedOperationException("Cannot update URL: " + url);
2563        }
2564
2565        String where = addIdToWhereClause(changedItemId, userWhere);
2566        int numRowsUpdated = db.update(tableToChange, values, where, whereArgs);
2567
2568        if (numRowsUpdated > 0 && changedItemId != null) {
2569            long itemId = Long.parseLong(changedItemId);
2570            switch (matchedUriId) {
2571                case ORGANIZATIONS_ID:
2572                    fixupPrimaryAfterUpdate(
2573                            Contacts.KIND_ORGANIZATION, null, itemId,
2574                            values.getAsInteger(Organizations.ISPRIMARY));
2575                    break;
2576
2577                case PHONES_ID:
2578                case PEOPLE_PHONES_ID:
2579                    fixupPrimaryAfterUpdate(
2580                            Contacts.KIND_PHONE, matchedUriId == PEOPLE_PHONES_ID
2581                                    ? Long.parseLong(url.getPathSegments().get(1))
2582                                    : null, itemId,
2583                            values.getAsInteger(Phones.ISPRIMARY));
2584                    break;
2585
2586                case CONTACTMETHODS_ID:
2587                case PEOPLE_CONTACTMETHODS_ID:
2588                    IsPrimaryInfo isPrimaryInfo = lookupIsPrimaryInfo(sContactMethodsTable,
2589                            sIsPrimaryProjectionWithKind, where, whereArgs);
2590                    fixupPrimaryAfterUpdate(
2591                            isPrimaryInfo.kind, isPrimaryInfo.person, itemId,
2592                            values.getAsInteger(ContactMethods.ISPRIMARY));
2593                    break;
2594
2595                case PEOPLE_ID:
2596                    boolean hasStarred = values.containsKey(People.STARRED);
2597                    boolean hasPrimaryPhone = values.containsKey(People.PRIMARY_PHONE_ID);
2598                    boolean hasPrimaryOrganization =
2599                            values.containsKey(People.PRIMARY_ORGANIZATION_ID);
2600                    boolean hasPrimaryEmail = values.containsKey(People.PRIMARY_EMAIL_ID);
2601                    if (hasStarred || hasPrimaryPhone || hasPrimaryOrganization
2602                            || hasPrimaryEmail) {
2603                        Cursor c = mDb.query(sPeopleTable, null,
2604                                where, whereArgs, null, null, null);
2605                        try {
2606                            int indexAccount = c.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
2607                            int indexId = c.getColumnIndexOrThrow(People._ID);
2608                            Long starredValue = values.getAsLong(People.STARRED);
2609                            Long primaryPhone = values.getAsLong(People.PRIMARY_PHONE_ID);
2610                            Long primaryOrganization =
2611                                    values.getAsLong(People.PRIMARY_ORGANIZATION_ID);
2612                            Long primaryEmail = values.getAsLong(People.PRIMARY_EMAIL_ID);
2613                            while (c.moveToNext()) {
2614                                final long personId = c.getLong(indexId);
2615                                if (hasStarred) {
2616                                    fixupGroupMembershipAfterPeopleUpdate(c.getString(indexAccount),
2617                                            personId, starredValue != null && starredValue != 0);
2618                                }
2619
2620                                if (hasPrimaryPhone) {
2621                                    if (primaryPhone == null) {
2622                                        throw new IllegalArgumentException(
2623                                                "the value of PRIMARY_PHONE_ID must not be null");
2624                                    }
2625                                    setIsPrimary(Contacts.KIND_PHONE, personId, primaryPhone);
2626                                }
2627                                if (hasPrimaryOrganization) {
2628                                    if (primaryOrganization == null) {
2629                                        throw new IllegalArgumentException(
2630                                                "the value of PRIMARY_ORGANIZATION_ID must "
2631                                                        + "not be null");
2632                                    }
2633                                    setIsPrimary(Contacts.KIND_ORGANIZATION, personId,
2634                                            primaryOrganization);
2635                                }
2636                                if (hasPrimaryEmail) {
2637                                    if (primaryEmail == null) {
2638                                        throw new IllegalArgumentException(
2639                                                "the value of PRIMARY_EMAIL_ID must not be null");
2640                                    }
2641                                    setIsPrimary(Contacts.KIND_EMAIL, personId, primaryEmail);
2642                                }
2643                            }
2644                        } finally {
2645                            c.close();
2646                        }
2647                    }
2648                    break;
2649            }
2650        }
2651
2652        return numRowsUpdated;
2653    }
2654
2655    private int updateSettings(ContentValues values) {
2656        final SQLiteDatabase db = getDatabase();
2657        final String account = values.getAsString(Contacts.Settings._SYNC_ACCOUNT);
2658        final String key = values.getAsString(Contacts.Settings.KEY);
2659        if (key == null) {
2660            throw new IllegalArgumentException("you must specify the key when updating settings");
2661        }
2662        if (account == null) {
2663            db.delete(sSettingsTable, "_sync_account IS NULL AND key=?", new String[]{key});
2664        } else {
2665            if (TextUtils.isEmpty(account)) {
2666                throw new IllegalArgumentException("account cannot be the empty string, " + values);
2667            }
2668            db.delete(sSettingsTable, "_sync_account=? AND key=?", new String[]{account, key});
2669        }
2670        long rowId = db.insert(sSettingsTable, Contacts.Settings.KEY, values);
2671        if (rowId < 0) {
2672            throw new SQLException("error updating settings with " + values);
2673        }
2674        return 1;
2675    }
2676
2677    private int updateGroups(ContentValues values, String where, String[] whereArgs) {
2678        for (Map.Entry<String, Object> entry : values.valueSet()) {
2679            final String column = entry.getKey();
2680            if (!Groups.NAME.equals(column) && !Groups.NOTES.equals(column)
2681                    && !Groups.SYSTEM_ID.equals(column) && !Groups.SHOULD_SYNC.equals(column)) {
2682                throw new IllegalArgumentException(
2683                        "you are not allowed to change column " + column);
2684            }
2685        }
2686
2687        Set<String> modifiedAccounts = Sets.newHashSet();
2688        final SQLiteDatabase db = getDatabase();
2689        if (values.containsKey(Groups.NAME) || values.containsKey(Groups.SHOULD_SYNC)) {
2690            String newName = values.getAsString(Groups.NAME);
2691            Cursor cursor = db.query(sGroupsTable, null, where, whereArgs, null, null, null);
2692            try {
2693                final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME);
2694                final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT);
2695                final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID);
2696                final int indexId = cursor.getColumnIndexOrThrow(Groups._ID);
2697                while (cursor.moveToNext()) {
2698                    String syncAccount = cursor.getString(indexSyncAccount);
2699                    if (values.containsKey(Groups.NAME)) {
2700                        String oldName = cursor.getString(indexName);
2701                        String syncId = cursor.getString(indexSyncId);
2702                        long id = cursor.getLong(indexId);
2703                        fixupPeopleStarredOnGroupRename(oldName, newName, id);
2704                        if (!TextUtils.isEmpty(syncAccount) && !TextUtils.isEmpty(syncId)) {
2705                            fixupPeopleStarredOnGroupRename(oldName, newName, syncAccount, syncId);
2706                        }
2707                    }
2708                    if (!TextUtils.isEmpty(syncAccount) && values.containsKey(Groups.SHOULD_SYNC)) {
2709                        modifiedAccounts.add(syncAccount);
2710                    }
2711                }
2712            } finally {
2713                cursor.close();
2714            }
2715        }
2716
2717        int numRows = db.update(sGroupsTable, values, where, whereArgs);
2718        if (numRows > 0) {
2719            if (!isTemporary()) {
2720                final ContentResolver cr = getContext().getContentResolver();
2721                for (String account : modifiedAccounts) {
2722                    onLocalChangesForAccount(cr, account, true);
2723                }
2724            }
2725        }
2726        return numRows;
2727    }
2728
2729    void fixupPeopleStarredOnGroupRename(String oldName, String newName,
2730            String where, String[] whereArgs) {
2731        if (TextUtils.equals(oldName, newName)) return;
2732
2733        int starredValue;
2734        if (Groups.GROUP_ANDROID_STARRED.equals(newName)) {
2735            starredValue = 1;
2736        } else if (Groups.GROUP_ANDROID_STARRED.equals(oldName)) {
2737            starredValue = 0;
2738        } else {
2739            return;
2740        }
2741
2742        getDatabase().execSQL("UPDATE people SET starred=" + starredValue + " WHERE _id in ("
2743                + "SELECT person "
2744                + "FROM groups, groupmembership "
2745                + "WHERE " + where + " AND " + sGroupsJoinString + ")",
2746                whereArgs);
2747    }
2748
2749    void fixupPeopleStarredOnGroupRename(String oldName, String newName,
2750            String syncAccount, String syncId) {
2751        fixupPeopleStarredOnGroupRename(oldName, newName, "_sync_account=? AND _sync_id=?",
2752                new String[]{syncAccount, syncId});
2753    }
2754
2755    void fixupPeopleStarredOnGroupRename(String oldName, String newName, long groupId) {
2756        fixupPeopleStarredOnGroupRename(oldName, newName, "group_id=?",
2757                new String[]{String.valueOf(groupId)});
2758    }
2759
2760    private void fixupPrimaryAfterUpdate(int kind, Long personId, Long changedItemId,
2761            Integer isPrimaryValue) {
2762        final String table = kindToTable(kind);
2763
2764        // - when you update isPrimary to true,
2765        //   make the changed item the primary, clear others
2766        // - when you update isPrimary to false,
2767        //   select a new one as isPrimary, clear the primary if no more phones
2768        if (isPrimaryValue != null) {
2769            if (personId == null) {
2770                personId = lookupPerson(table, changedItemId);
2771            }
2772
2773            boolean isPrimary = isPrimaryValue != 0;
2774            Long newPrimary = changedItemId;
2775            if (!isPrimary) {
2776                newPrimary = findNewPrimary(kind, personId, changedItemId);
2777            }
2778            clearOtherIsPrimary(kind, personId, changedItemId);
2779
2780            if (kind == Contacts.KIND_PHONE) {
2781                updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newPrimary);
2782            } else if (kind == Contacts.KIND_EMAIL) {
2783                updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newPrimary);
2784            } else if (kind == Contacts.KIND_ORGANIZATION) {
2785                updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newPrimary);
2786            }
2787        }
2788    }
2789
2790    /**
2791     * Queries table to find the value of the person column for the row with _id. There must
2792     * be exactly one row that matches this id.
2793     * @param table the table to query
2794     * @param id the id of the row to query
2795     * @return the value of the person column for the specified row, returned as a String.
2796     */
2797    private long lookupPerson(String table, long id) {
2798        return DatabaseUtils.longForQuery(
2799                getDatabase(),
2800                "SELECT person FROM " + table + " where _id=" + id,
2801                null);
2802    }
2803
2804    /**
2805     * Used to pass around information about a row that has the isprimary column.
2806     */
2807    private class IsPrimaryInfo {
2808        boolean isPrimary;
2809        Long person;
2810        Long id;
2811        Integer kind;
2812    }
2813
2814    /**
2815     * Queries the table to determine the state of the row's isprimary column and the kind.
2816     * The where and whereArgs must be sufficient to match either 0 or 1 row.
2817     * @param table the table of rows to consider, supports "phones" and "contact_methods"
2818     * @param projection the projection to use to get the columns that pertain to table
2819     * @param where used in conjunction with the whereArgs to identify the row
2820     * @param where used in conjunction with the where string to identify the row
2821     * @return the IsPrimaryInfo about the matched row, or null if no row was matched
2822     */
2823    private IsPrimaryInfo lookupIsPrimaryInfo(String table, String[] projection, String where,
2824            String[] whereArgs) {
2825        Cursor cursor = getDatabase().query(table, projection, where, whereArgs, null, null, null);
2826        try {
2827            if (!(cursor.getCount() <= 1)) {
2828                throw new IllegalArgumentException("expected only zero or one rows, got "
2829                        + DatabaseUtils.dumpCursorToString(cursor));
2830            }
2831            if (!cursor.moveToFirst()) return null;
2832            IsPrimaryInfo info = new IsPrimaryInfo();
2833            info.isPrimary = cursor.getInt(0) != 0;
2834            info.person = cursor.getLong(1);
2835            info.id = cursor.getLong(2);
2836            if (projection == sIsPrimaryProjectionWithKind) {
2837                info.kind = cursor.getInt(3);
2838            } else {
2839                if (sPhonesTable.equals(table)) {
2840                    info.kind = Contacts.KIND_PHONE;
2841                } else if (sOrganizationsTable.equals(table)) {
2842                    info.kind = Contacts.KIND_ORGANIZATION;
2843                } else {
2844                    throw new IllegalArgumentException("unexpected table, " + table);
2845                }
2846            }
2847            return info;
2848        } finally {
2849            cursor.close();
2850        }
2851    }
2852
2853    /**
2854     * Returns the rank of the table-specific type, used when deciding which row
2855     * should be primary when none are primary. The lower the rank the better the type.
2856     * @param table supports "phones", "contact_methods" and "organizations"
2857     * @param type the table-specific type from the TYPE column
2858     * @return the rank of the table-specific type, the lower the better
2859     */
2860    private int getRankOfType(String table, int type) {
2861        if (table.equals(sPhonesTable)) {
2862            switch (type) {
2863                case Contacts.Phones.TYPE_MOBILE: return 0;
2864                case Contacts.Phones.TYPE_WORK: return 1;
2865                case Contacts.Phones.TYPE_HOME: return 2;
2866                case Contacts.Phones.TYPE_PAGER: return 3;
2867                case Contacts.Phones.TYPE_CUSTOM: return 4;
2868                case Contacts.Phones.TYPE_OTHER: return 5;
2869                case Contacts.Phones.TYPE_FAX_WORK: return 6;
2870                case Contacts.Phones.TYPE_FAX_HOME: return 7;
2871                default: return 1000;
2872            }
2873        }
2874
2875        if (table.equals(sContactMethodsTable)) {
2876            switch (type) {
2877                case Contacts.ContactMethods.TYPE_HOME: return 0;
2878                case Contacts.ContactMethods.TYPE_WORK: return 1;
2879                case Contacts.ContactMethods.TYPE_CUSTOM: return 2;
2880                case Contacts.ContactMethods.TYPE_OTHER: return 3;
2881                default: return 1000;
2882            }
2883        }
2884
2885        if (table.equals(sOrganizationsTable)) {
2886            switch (type) {
2887                case Organizations.TYPE_WORK: return 0;
2888                case Organizations.TYPE_CUSTOM: return 1;
2889                case Organizations.TYPE_OTHER: return 2;
2890                default: return 1000;
2891            }
2892        }
2893
2894        throw new IllegalArgumentException("unexpected table, " + table);
2895    }
2896
2897    /**
2898     * Determines which of the rows in table for the personId should be picked as the primary
2899     * row based on the rank of the row's type.
2900     * @param kind the kind of contact
2901     * @param personId used to limit the rows to those pertaining to this person
2902     * @param itemId optional, a row to ignore
2903     * @return the _id of the row that should be the new primary. Is null if there are no
2904     *   matching rows.
2905     */
2906    private Long findNewPrimary(int kind, Long personId, Long itemId) {
2907        final String table = kindToTable(kind);
2908        if (personId == null) throw new IllegalArgumentException("personId must not be null");
2909        StringBuilder sb = new StringBuilder();
2910        sb.append("person=");
2911        sb.append(personId);
2912        if (itemId != null) {
2913            sb.append(" and _id!=");
2914            sb.append(itemId);
2915        }
2916        if (sContactMethodsTable.equals(table)) {
2917            sb.append(" and ");
2918            sb.append(ContactMethods.KIND);
2919            sb.append("=");
2920            sb.append(kind);
2921        }
2922
2923        Cursor cursor = getDatabase().query(table, ID_TYPE_PROJECTION, sb.toString(),
2924                null, null, null, null);
2925        try {
2926            Long newPrimaryId = null;
2927            int bestRank = -1;
2928            while (cursor.moveToNext()) {
2929                final int rank = getRankOfType(table, cursor.getInt(1));
2930                if (bestRank == -1 || rank < bestRank) {
2931                    newPrimaryId = cursor.getLong(0);
2932                    bestRank = rank;
2933                }
2934            }
2935            return newPrimaryId;
2936        } finally {
2937            cursor.close();
2938        }
2939    }
2940
2941    private void setIsPrimary(int kind, long personId, long itemId) {
2942        final String table = kindToTable(kind);
2943        StringBuilder sb = new StringBuilder();
2944        sb.append("person=");
2945        sb.append(personId);
2946
2947        if (sContactMethodsTable.equals(table)) {
2948            sb.append(" and ");
2949            sb.append(ContactMethods.KIND);
2950            sb.append("=");
2951            sb.append(kind);
2952        }
2953
2954        final String where = sb.toString();
2955        getDatabase().execSQL(
2956                "UPDATE " + table + " SET isprimary=(_id=" + itemId + ") WHERE " + where);
2957    }
2958
2959    /**
2960     * Clears the isprimary flag for all rows other than the itemId.
2961     * @param kind the kind of item
2962     * @param personId used to limit the updates to rows pertaining to this person
2963     * @param itemId which row to leave untouched
2964     */
2965    private void clearOtherIsPrimary(int kind, Long personId, Long itemId) {
2966        final String table = kindToTable(kind);
2967        if (personId == null) throw new IllegalArgumentException("personId must not be null");
2968        StringBuilder sb = new StringBuilder();
2969        sb.append("person=");
2970        sb.append(personId);
2971        if (itemId != null) {
2972            sb.append(" and _id!=");
2973            sb.append(itemId);
2974        }
2975        if (sContactMethodsTable.equals(table)) {
2976            sb.append(" and ");
2977            sb.append(ContactMethods.KIND);
2978            sb.append("=");
2979            sb.append(kind);
2980        }
2981
2982        mValuesLocal.clear();
2983        mValuesLocal.put("isprimary", 0);
2984        getDatabase().update(table, mValuesLocal, sb.toString(), null);
2985    }
2986
2987    /**
2988     * Set the specified primary column for the person. This is used to make the people
2989     * row reflect the isprimary flag in the people or contactmethods tables, which is
2990     * authoritative.
2991     * @param personId the person to modify
2992     * @param column the name of the primary column (phone or email)
2993     * @param primaryId the new value to write into the primary column
2994     */
2995    private void updatePeoplePrimary(Long personId, String column, Long primaryId) {
2996        mValuesLocal.clear();
2997        mValuesLocal.put(column, primaryId);
2998        getDatabase().update(sPeopleTable, mValuesLocal, "_id=" + personId, null);
2999    }
3000
3001    private static String addIdToWhereClause(String id, String where) {
3002        if (id != null) {
3003            StringBuilder whereSb = new StringBuilder("_id=");
3004            whereSb.append(id);
3005            if (!TextUtils.isEmpty(where)) {
3006                whereSb.append(" AND (");
3007                whereSb.append(where);
3008                whereSb.append(')');
3009            }
3010            return whereSb.toString();
3011        } else {
3012            return where;
3013        }
3014    }
3015
3016    private boolean queryGroupMembershipContainsStarred(long personId) {
3017        // TODO: Part 1 of 2 part hack to work around a bug in reusing SQLiteStatements
3018        SQLiteStatement mGroupsMembershipQuery = null;
3019
3020        if (mGroupsMembershipQuery == null) {
3021            String query =
3022                "SELECT COUNT(*) FROM groups, groupmembership WHERE "
3023                + sGroupsJoinString + " AND person=? AND groups.name=?";
3024            mGroupsMembershipQuery = getDatabase().compileStatement(query);
3025        }
3026        long result = DatabaseUtils.longForQuery(mGroupsMembershipQuery,
3027                new String[]{String.valueOf(personId), Groups.GROUP_ANDROID_STARRED});
3028
3029        // TODO: Part 2 of 2 part hack to work around a bug in reusing SQLiteStatements
3030        mGroupsMembershipQuery.close();
3031
3032        return result != 0;
3033    }
3034
3035    @Override
3036    public boolean changeRequiresLocalSync(Uri uri) {
3037        final int match = sURIMatcher.match(uri);
3038        switch (match) {
3039            // Changes to these URIs cannot cause syncable data to be changed, so don't
3040            // bother trying to sync them.
3041            case CALLS:
3042            case CALLS_FILTER:
3043            case CALLS_ID:
3044            case PRESENCE:
3045            case PRESENCE_ID:
3046            case PEOPLE_UPDATE_CONTACT_TIME:
3047                return false;
3048
3049            default:
3050                return true;
3051        }
3052    }
3053
3054    @Override
3055    protected Iterable<? extends AbstractTableMerger> getMergers() {
3056        ArrayList<AbstractTableMerger> list = new ArrayList<AbstractTableMerger> ();
3057        list.add(new PersonMerger());
3058        list.add(new GroupMerger());
3059        list.add(new PhotoMerger());
3060        return list;
3061    }
3062
3063    protected static String sPeopleTable = "people";
3064    protected static Uri sPeopleRawURL = Uri.parse("content://contacts/people/raw/");
3065    protected static String sDeletedPeopleTable = "_deleted_people";
3066    protected static Uri sDeletedPeopleURL = Uri.parse("content://contacts/deleted_people/");
3067    protected static String sGroupsTable = "groups";
3068    protected static String sSettingsTable = "settings";
3069    protected static Uri sGroupsURL = Uri.parse("content://contacts/groups/");
3070    protected static String sDeletedGroupsTable = "_deleted_groups";
3071    protected static Uri sDeletedGroupsURL =
3072            Uri.parse("content://contacts/deleted_groups/");
3073    protected static String sPhonesTable = "phones";
3074    protected static String sOrganizationsTable = "organizations";
3075    protected static String sContactMethodsTable = "contact_methods";
3076    protected static String sGroupmembershipTable = "groupmembership";
3077    protected static String sPhotosTable = "photos";
3078    protected static Uri sPhotosURL = Uri.parse("content://contacts/photos/");
3079    protected static String sExtensionsTable = "extensions";
3080    protected static String sCallsTable = "calls";
3081
3082    protected class PersonMerger extends AbstractTableMerger
3083    {
3084        private ContentValues mValues = new ContentValues();
3085        Map<String, SQLiteCursor> mCursorMap = Maps.newHashMap();
3086        public PersonMerger()
3087        {
3088            super(getDatabase(),
3089                    sPeopleTable, sPeopleRawURL, sDeletedPeopleTable, sDeletedPeopleURL);
3090        }
3091
3092        @Override
3093        protected void notifyChanges() {
3094            // notify that a change has occurred.
3095            getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI,
3096                    null /* observer */, false /* do not sync to network */);
3097        }
3098
3099        @Override
3100        public void insertRow(ContentProvider diffs, Cursor diffsCursor) {
3101            final SQLiteDatabase db = getDatabase();
3102
3103            Long localPrimaryPhoneId = null;
3104            Long localPrimaryEmailId = null;
3105            Long localPrimaryOrganizationId = null;
3106
3107            // Copy the person
3108            mPeopleInserter.prepareForInsert();
3109            DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_ID, mPeopleInserter, mIndexPeopleSyncId);
3110            DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_TIME, mPeopleInserter, mIndexPeopleSyncTime);
3111            DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_VERSION, mPeopleInserter, mIndexPeopleSyncVersion);
3112            DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_DIRTY, mPeopleInserter, mIndexPeopleSyncDirty);
3113            DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_ACCOUNT, mPeopleInserter, mIndexPeopleSyncAccount);
3114            DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People.NAME, mPeopleInserter, mIndexPeopleName);
3115            DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People.PHONETIC_NAME, mPeopleInserter, mIndexPeoplePhoneticName);
3116            DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People.NOTES, mPeopleInserter, mIndexPeopleNotes);
3117            long localPersonID = mPeopleInserter.execute();
3118
3119            Cursor c;
3120            final SQLiteDatabase diffsDb = ((ContactsProvider) diffs).getDatabase();
3121            long diffsPersonID = diffsCursor.getLong(diffsCursor.getColumnIndexOrThrow(People._ID));
3122
3123            // Copy the Photo info
3124            c = doSubQuery(diffsDb, sPhotosTable, null, diffsPersonID, null);
3125            try {
3126                if (c.moveToNext()) {
3127                    mDb.delete(sPhotosTable, "person=" + localPersonID, null);
3128                    mPhotosInserter.prepareForInsert();
3129                    DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ID,
3130                            mPhotosInserter, mIndexPhotosSyncId);
3131                    DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_TIME,
3132                            mPhotosInserter, mIndexPhotosSyncTime);
3133                    DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_VERSION,
3134                            mPhotosInserter, mIndexPhotosSyncVersion);
3135                    DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ACCOUNT,
3136                            mPhotosInserter, mIndexPhotosSyncAccount);
3137                    DatabaseUtils.cursorStringToInsertHelper(c, Photos.EXISTS_ON_SERVER,
3138                            mPhotosInserter, mIndexPhotosExistsOnServer);
3139                    mPhotosInserter.bind(mIndexPhotosSyncError, (String)null);
3140                    mPhotosInserter.bind(mIndexPhotosSyncDirty, 0);
3141                    mPhotosInserter.bind(mIndexPhotosPersonId, localPersonID);
3142                    mPhotosInserter.execute();
3143                }
3144            } finally {
3145                c.deactivate();
3146            }
3147
3148            // Copy all phones
3149            c = doSubQuery(diffsDb, sPhonesTable, null, diffsPersonID, sPhonesTable + "._id");
3150            if (c != null) {
3151                Long newPrimaryId = null;
3152                int bestRank = -1;
3153                final int labelIndex = c.getColumnIndexOrThrow(Phones.LABEL);
3154                final int typeIndex = c.getColumnIndexOrThrow(Phones.TYPE);
3155                final int numberIndex = c.getColumnIndexOrThrow(Phones.NUMBER);
3156                final int keyIndex = c.getColumnIndexOrThrow(Phones.NUMBER_KEY);
3157                final int primaryIndex = c.getColumnIndexOrThrow(Phones.ISPRIMARY);
3158                while(c.moveToNext()) {
3159                    final int type = c.getInt(typeIndex);
3160                    final int isPrimaryValue = c.getInt(primaryIndex);
3161                    mPhonesInserter.prepareForInsert();
3162                    mPhonesInserter.bind(mIndexPhonesPersonId, localPersonID);
3163                    mPhonesInserter.bind(mIndexPhonesLabel, c.getString(labelIndex));
3164                    mPhonesInserter.bind(mIndexPhonesType, type);
3165                    mPhonesInserter.bind(mIndexPhonesNumber, c.getString(numberIndex));
3166                    mPhonesInserter.bind(mIndexPhonesNumberKey, c.getString(keyIndex));
3167                    mPhonesInserter.bind(mIndexPhonesIsPrimary, isPrimaryValue);
3168                    long rowId = mPhonesInserter.execute();
3169
3170                    if (isPrimaryValue != 0) {
3171                        if (localPrimaryPhoneId != null) {
3172                            throw new IllegalArgumentException(
3173                                    "more than one phone was marked as primary, "
3174                                            + DatabaseUtils.dumpCursorToString(c));
3175                        }
3176                        localPrimaryPhoneId = rowId;
3177                    }
3178
3179                    if (localPrimaryPhoneId == null) {
3180                        final int rank = getRankOfType(sPhonesTable, type);
3181                        if (bestRank == -1 || rank < bestRank) {
3182                            newPrimaryId = rowId;
3183                            bestRank = rank;
3184                        }
3185                    }
3186                }
3187                c.deactivate();
3188
3189                if (localPrimaryPhoneId == null) {
3190                    localPrimaryPhoneId = newPrimaryId;
3191                }
3192            }
3193
3194            // Copy all contact_methods
3195            c = doSubQuery(diffsDb, sContactMethodsTable, null, diffsPersonID,
3196                    sContactMethodsTable + "._id");
3197            if (c != null) {
3198                Long newPrimaryId = null;
3199                int bestRank = -1;
3200                final int labelIndex = c.getColumnIndexOrThrow(ContactMethods.LABEL);
3201                final int kindIndex = c.getColumnIndexOrThrow(ContactMethods.KIND);
3202                final int typeIndex = c.getColumnIndexOrThrow(ContactMethods.TYPE);
3203                final int dataIndex = c.getColumnIndexOrThrow(ContactMethods.DATA);
3204                final int auxDataIndex = c.getColumnIndexOrThrow(ContactMethods.AUX_DATA);
3205                final int primaryIndex = c.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
3206                while(c.moveToNext()) {
3207                    final int type = c.getInt(typeIndex);
3208                    final int kind = c.getInt(kindIndex);
3209                    final int isPrimaryValue = c.getInt(primaryIndex);
3210                    mContactMethodsInserter.prepareForInsert();
3211                    mContactMethodsInserter.bind(mIndexContactMethodsPersonId, localPersonID);
3212                    mContactMethodsInserter.bind(mIndexContactMethodsLabel, c.getString(labelIndex));
3213                    mContactMethodsInserter.bind(mIndexContactMethodsKind, kind);
3214                    mContactMethodsInserter.bind(mIndexContactMethodsType, type);
3215                    mContactMethodsInserter.bind(mIndexContactMethodsData, c.getString(dataIndex));
3216                    mContactMethodsInserter.bind(mIndexContactMethodsAuxData, c.getString(auxDataIndex));
3217                    mContactMethodsInserter.bind(mIndexContactMethodsIsPrimary, isPrimaryValue);
3218                    long rowId = mContactMethodsInserter.execute();
3219                    if ((kind == Contacts.KIND_EMAIL) && (isPrimaryValue != 0)) {
3220                        if (localPrimaryEmailId != null) {
3221                            throw new IllegalArgumentException(
3222                                    "more than one email was marked as primary, "
3223                                            + DatabaseUtils.dumpCursorToString(c));
3224                        }
3225                        localPrimaryEmailId = rowId;
3226                    }
3227
3228                    if (localPrimaryEmailId == null) {
3229                        final int rank = getRankOfType(sContactMethodsTable, type);
3230                        if (bestRank == -1 || rank < bestRank) {
3231                            newPrimaryId = rowId;
3232                            bestRank = rank;
3233                        }
3234                    }
3235                }
3236                c.deactivate();
3237
3238                if (localPrimaryEmailId == null) {
3239                    localPrimaryEmailId = newPrimaryId;
3240                }
3241            }
3242
3243            // Copy all organizations
3244            c = doSubQuery(diffsDb, sOrganizationsTable, null, diffsPersonID,
3245                    sOrganizationsTable + "._id");
3246            try {
3247                Long newPrimaryId = null;
3248                int bestRank = -1;
3249                final int labelIndex = c.getColumnIndexOrThrow(Organizations.LABEL);
3250                final int typeIndex = c.getColumnIndexOrThrow(Organizations.TYPE);
3251                final int companyIndex = c.getColumnIndexOrThrow(Organizations.COMPANY);
3252                final int titleIndex = c.getColumnIndexOrThrow(Organizations.TITLE);
3253                final int primaryIndex = c.getColumnIndexOrThrow(Organizations.ISPRIMARY);
3254                while(c.moveToNext()) {
3255                    final int type = c.getInt(typeIndex);
3256                    final int isPrimaryValue = c.getInt(primaryIndex);
3257                    mOrganizationsInserter.prepareForInsert();
3258                    mOrganizationsInserter.bind(mIndexOrganizationsPersonId, localPersonID);
3259                    mOrganizationsInserter.bind(mIndexOrganizationsLabel, c.getString(labelIndex));
3260                    mOrganizationsInserter.bind(mIndexOrganizationsType, type);
3261                    mOrganizationsInserter.bind(mIndexOrganizationsCompany, c.getString(companyIndex));
3262                    mOrganizationsInserter.bind(mIndexOrganizationsTitle, c.getString(titleIndex));
3263                    mOrganizationsInserter.bind(mIndexOrganizationsIsPrimary, isPrimaryValue);
3264                    long rowId = mOrganizationsInserter.execute();
3265                    if (isPrimaryValue != 0) {
3266                        if (localPrimaryOrganizationId != null) {
3267                            throw new IllegalArgumentException(
3268                                    "more than one organization was marked as primary, "
3269                                            + DatabaseUtils.dumpCursorToString(c));
3270                        }
3271                        localPrimaryOrganizationId = rowId;
3272                    }
3273
3274                    if (localPrimaryOrganizationId == null) {
3275                        final int rank = getRankOfType(sOrganizationsTable, type);
3276                        if (bestRank == -1 || rank < bestRank) {
3277                            newPrimaryId = rowId;
3278                            bestRank = rank;
3279                        }
3280                    }
3281                }
3282
3283                if (localPrimaryOrganizationId == null) {
3284                    localPrimaryOrganizationId = newPrimaryId;
3285                }
3286            } finally {
3287                c.deactivate();
3288            }
3289
3290            // Copy all groupmembership rows
3291            c = doSubQuery(diffsDb, sGroupmembershipTable, null, diffsPersonID,
3292                    sGroupmembershipTable + "._id");
3293            try {
3294                final int accountIndex =
3295                    c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ACCOUNT);
3296                final int idIndex = c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ID);
3297                while(c.moveToNext()) {
3298                    mGroupMembershipInserter.prepareForInsert();
3299                    mGroupMembershipInserter.bind(mIndexGroupMembershipPersonId, localPersonID);
3300                    mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccount, c.getString(accountIndex));
3301                    mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncId, c.getString(idIndex));
3302                    mGroupMembershipInserter.execute();
3303                }
3304            } finally {
3305                c.deactivate();
3306            }
3307
3308            // Copy all extensions rows
3309            c = doSubQuery(diffsDb, sExtensionsTable, null, diffsPersonID, sExtensionsTable + "._id");
3310            try {
3311                final int nameIndex = c.getColumnIndexOrThrow(Extensions.NAME);
3312                final int valueIndex = c.getColumnIndexOrThrow(Extensions.VALUE);
3313                while(c.moveToNext()) {
3314                    mExtensionsInserter.prepareForInsert();
3315                    mExtensionsInserter.bind(mIndexExtensionsPersonId, localPersonID);
3316                    mExtensionsInserter.bind(mIndexExtensionsName, c.getString(nameIndex));
3317                    mExtensionsInserter.bind(mIndexExtensionsValue, c.getString(valueIndex));
3318                    mExtensionsInserter.execute();
3319                }
3320            } finally {
3321                c.deactivate();
3322            }
3323
3324            // Update the _SYNC_DIRTY flag of the person. We have to do this
3325            // after inserting since the updated of the phones, contact
3326            // methods and organizations will fire a sql trigger that will
3327            // cause this flag to be set.
3328            mValues.clear();
3329            mValues.put(People._SYNC_DIRTY, 0);
3330            mValues.put(People.PRIMARY_PHONE_ID, localPrimaryPhoneId);
3331            mValues.put(People.PRIMARY_EMAIL_ID, localPrimaryEmailId);
3332            mValues.put(People.PRIMARY_ORGANIZATION_ID, localPrimaryOrganizationId);
3333            final boolean isStarred = queryGroupMembershipContainsStarred(localPersonID);
3334            mValues.put(People.STARRED, isStarred ? 1 : 0);
3335            db.update(mTable, mValues, People._ID + '=' + localPersonID, null);
3336        }
3337
3338        @Override
3339        public void updateRow(long localPersonID, ContentProvider diffs, Cursor diffsCursor) {
3340            updateOrResolveRow(localPersonID, null, diffs, diffsCursor, false);
3341        }
3342
3343        @Override
3344        public void resolveRow(long localPersonID, String syncID,
3345                ContentProvider diffs, Cursor diffsCursor) {
3346            updateOrResolveRow(localPersonID, syncID, diffs, diffsCursor, true);
3347        }
3348
3349        protected void updateOrResolveRow(long localPersonID, String syncID,
3350                ContentProvider diffs, Cursor diffsCursor, boolean conflicts) {
3351            final SQLiteDatabase db = getDatabase();
3352            // The local version of localPersonId's record has changed. This
3353            // person also has a changed record in the diffs. Merge the changes
3354            // in the following way:
3355            //  - if any fields in the people table changed use the server's
3356            //    version
3357            //  - for phones, emails, addresses, compute the join of all unique
3358            //    subrecords. If any of the subrecords has changes in both
3359            //    places then choose the server version of the subrecord
3360            //
3361            // Limitation: deletes of phones, emails, or addresses are ignored
3362            // when the record has changed on both the client and the server
3363
3364            long diffsPersonID = diffsCursor.getLong(diffsCursor.getColumnIndexOrThrow("_id"));
3365
3366            // Join the server phones, organizations, and contact_methods with the local ones.
3367            //  - Add locally any that exist only on the server.
3368            //  - If the row conflicts, delete locally any that exist only on the client.
3369            //  - If the row doesn't conflict, ignore any that exist only on the client.
3370            //  - Update any that exist in both places.
3371
3372            Map<Integer, Long> primaryLocal = new HashMap<Integer, Long>();
3373            Map<Integer, Long> primaryDiffs = new HashMap<Integer, Long>();
3374
3375            Cursor cRemote;
3376            Cursor cLocal;
3377
3378            // Phones
3379            cRemote = null;
3380            cLocal = null;
3381            final SQLiteDatabase diffsDb = ((ContactsProvider) diffs).getDatabase();
3382            try {
3383                cLocal = doSubQuery(db, sPhonesTable, null, localPersonID, sPhonesKeyOrderBy);
3384                cRemote = doSubQuery(diffsDb, sPhonesTable,
3385                        null, diffsPersonID, sPhonesKeyOrderBy);
3386
3387                final int idColLocal = cLocal.getColumnIndexOrThrow(Phones._ID);
3388                final int isPrimaryColLocal = cLocal.getColumnIndexOrThrow(Phones.ISPRIMARY);
3389                final int isPrimaryColRemote = cRemote.getColumnIndexOrThrow(Phones.ISPRIMARY);
3390
3391                CursorJoiner joiner =
3392                        new CursorJoiner(cLocal, sPhonesKeyColumns, cRemote, sPhonesKeyColumns);
3393                for (CursorJoiner.Result joinResult : joiner) {
3394                    switch(joinResult) {
3395                        case LEFT:
3396                            if (!conflicts) {
3397                                db.delete(sPhonesTable,
3398                                        Phones._ID + "=" + cLocal.getLong(idColLocal), null);
3399                            } else {
3400                                if (cLocal.getLong(isPrimaryColLocal) != 0) {
3401                                    savePrimaryId(primaryLocal, Contacts.KIND_PHONE,
3402                                            cLocal.getLong(idColLocal));
3403                                }
3404                            }
3405                            break;
3406
3407                        case RIGHT:
3408                        case BOTH:
3409                            mValues.clear();
3410                            DatabaseUtils.cursorIntToContentValues(
3411                                    cRemote, Phones.TYPE, mValues);
3412                            DatabaseUtils.cursorStringToContentValues(
3413                                    cRemote, Phones.LABEL, mValues);
3414                            DatabaseUtils.cursorStringToContentValues(
3415                                    cRemote, Phones.NUMBER, mValues);
3416                            DatabaseUtils.cursorStringToContentValues(
3417                                    cRemote, Phones.NUMBER_KEY, mValues);
3418                            DatabaseUtils.cursorIntToContentValues(
3419                                    cRemote, Phones.ISPRIMARY, mValues);
3420
3421                            long localId;
3422                            if (joinResult == CursorJoiner.Result.RIGHT) {
3423                                mValues.put(Phones.PERSON_ID, localPersonID);
3424                                localId = mPhonesInserter.insert(mValues);
3425                            } else {
3426                                localId = cLocal.getLong(idColLocal);
3427                                db.update(sPhonesTable, mValues, "_id =" + localId, null);
3428                            }
3429                            if (cRemote.getLong(isPrimaryColRemote) != 0) {
3430                                savePrimaryId(primaryDiffs, Contacts.KIND_PHONE, localId);
3431                            }
3432                            break;
3433                    }
3434                }
3435            } finally {
3436                if (cRemote != null) cRemote.deactivate();
3437                if (cLocal != null) cLocal.deactivate();
3438            }
3439
3440            // Contact methods
3441            cRemote = null;
3442            cLocal = null;
3443            try {
3444                cLocal = doSubQuery(db,
3445                        sContactMethodsTable, null, localPersonID, sContactMethodsKeyOrderBy);
3446                cRemote = doSubQuery(diffsDb,
3447                        sContactMethodsTable, null, diffsPersonID, sContactMethodsKeyOrderBy);
3448
3449                final int idColLocal = cLocal.getColumnIndexOrThrow(ContactMethods._ID);
3450                final int kindColLocal = cLocal.getColumnIndexOrThrow(ContactMethods.KIND);
3451                final int kindColRemote = cRemote.getColumnIndexOrThrow(ContactMethods.KIND);
3452                final int isPrimaryColLocal =
3453                        cLocal.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
3454                final int isPrimaryColRemote =
3455                        cRemote.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
3456
3457                CursorJoiner joiner = new CursorJoiner(
3458                        cLocal, sContactMethodsKeyColumns, cRemote, sContactMethodsKeyColumns);
3459                for (CursorJoiner.Result joinResult : joiner) {
3460                    switch(joinResult) {
3461                        case LEFT:
3462                            if (!conflicts) {
3463                                db.delete(sContactMethodsTable, ContactMethods._ID + "="
3464                                        + cLocal.getLong(idColLocal), null);
3465                            } else {
3466                                if (cLocal.getLong(isPrimaryColLocal) != 0) {
3467                                    savePrimaryId(primaryLocal, cLocal.getInt(kindColLocal),
3468                                            cLocal.getLong(idColLocal));
3469                                }
3470                            }
3471                            break;
3472
3473                        case RIGHT:
3474                        case BOTH:
3475                            mValues.clear();
3476                            DatabaseUtils.cursorStringToContentValues(cRemote,
3477                                    ContactMethods.LABEL, mValues);
3478                            DatabaseUtils.cursorIntToContentValues(cRemote,
3479                                    ContactMethods.TYPE, mValues);
3480                            DatabaseUtils.cursorIntToContentValues(cRemote,
3481                                    ContactMethods.KIND, mValues);
3482                            DatabaseUtils.cursorStringToContentValues(cRemote,
3483                                    ContactMethods.DATA, mValues);
3484                            DatabaseUtils.cursorStringToContentValues(cRemote,
3485                                    ContactMethods.AUX_DATA, mValues);
3486                            DatabaseUtils.cursorIntToContentValues(cRemote,
3487                                    ContactMethods.ISPRIMARY, mValues);
3488
3489                            long localId;
3490                            if (joinResult == CursorJoiner.Result.RIGHT) {
3491                                mValues.put(ContactMethods.PERSON_ID, localPersonID);
3492                                localId = mContactMethodsInserter.insert(mValues);
3493                            } else {
3494                                localId = cLocal.getLong(idColLocal);
3495                                db.update(sContactMethodsTable, mValues, "_id =" + localId, null);
3496                            }
3497                            if (cRemote.getLong(isPrimaryColRemote) != 0) {
3498                                savePrimaryId(primaryDiffs, cRemote.getInt(kindColRemote), localId);
3499                            }
3500                            break;
3501                    }
3502                }
3503            } finally {
3504                if (cRemote != null) cRemote.deactivate();
3505                if (cLocal != null) cLocal.deactivate();
3506            }
3507
3508            // Organizations
3509            cRemote = null;
3510            cLocal = null;
3511            try {
3512                cLocal = doSubQuery(db,
3513                        sOrganizationsTable, null, localPersonID, sOrganizationsKeyOrderBy);
3514                cRemote = doSubQuery(diffsDb,
3515                        sOrganizationsTable, null, diffsPersonID, sOrganizationsKeyOrderBy);
3516
3517                final int idColLocal = cLocal.getColumnIndexOrThrow(Organizations._ID);
3518                final int isPrimaryColLocal =
3519                        cLocal.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
3520                final int isPrimaryColRemote =
3521                        cRemote.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
3522                CursorJoiner joiner = new CursorJoiner(
3523                        cLocal, sOrganizationsKeyColumns, cRemote, sOrganizationsKeyColumns);
3524                for (CursorJoiner.Result joinResult : joiner) {
3525                    switch(joinResult) {
3526                        case LEFT:
3527                            if (!conflicts) {
3528                                db.delete(sOrganizationsTable,
3529                                        Phones._ID + "=" + cLocal.getLong(idColLocal), null);
3530                            } else {
3531                                if (cLocal.getLong(isPrimaryColLocal) != 0) {
3532                                    savePrimaryId(primaryLocal, Contacts.KIND_ORGANIZATION,
3533                                            cLocal.getLong(idColLocal));
3534                                }
3535                            }
3536                            break;
3537
3538                        case RIGHT:
3539                        case BOTH:
3540                            mValues.clear();
3541                            DatabaseUtils.cursorStringToContentValues(cRemote,
3542                                    Organizations.LABEL, mValues);
3543                            DatabaseUtils.cursorIntToContentValues(cRemote,
3544                                    Organizations.TYPE, mValues);
3545                            DatabaseUtils.cursorStringToContentValues(cRemote,
3546                                    Organizations.COMPANY, mValues);
3547                            DatabaseUtils.cursorStringToContentValues(cRemote,
3548                                    Organizations.TITLE, mValues);
3549                            DatabaseUtils.cursorIntToContentValues(cRemote,
3550                                    Organizations.ISPRIMARY, mValues);
3551                            long localId;
3552                            if (joinResult == CursorJoiner.Result.RIGHT) {
3553                                mValues.put(Organizations.PERSON_ID, localPersonID);
3554                                localId = mOrganizationsInserter.insert(mValues);
3555                            } else {
3556                                localId = cLocal.getLong(idColLocal);
3557                                db.update(sOrganizationsTable, mValues,
3558                                        "_id =" + localId, null /* whereArgs */);
3559                            }
3560                            if (cRemote.getLong(isPrimaryColRemote) != 0) {
3561                                savePrimaryId(primaryDiffs, Contacts.KIND_ORGANIZATION, localId);
3562                            }
3563                            break;
3564                    }
3565                }
3566            } finally {
3567                if (cRemote != null) cRemote.deactivate();
3568                if (cLocal != null) cLocal.deactivate();
3569            }
3570
3571            // Groupmembership
3572            cRemote = null;
3573            cLocal = null;
3574            try {
3575                cLocal = doSubQuery(db,
3576                        sGroupmembershipTable, null, localPersonID, sGroupmembershipKeyOrderBy);
3577                cRemote = doSubQuery(diffsDb,
3578                        sGroupmembershipTable, null, diffsPersonID, sGroupmembershipKeyOrderBy);
3579
3580                final int idColLocal = cLocal.getColumnIndexOrThrow(GroupMembership._ID);
3581                CursorJoiner joiner = new CursorJoiner(
3582                        cLocal, sGroupmembershipKeyColumns, cRemote, sGroupmembershipKeyColumns);
3583                for (CursorJoiner.Result joinResult : joiner) {
3584                    switch(joinResult) {
3585                        case LEFT:
3586                            if (!conflicts) {
3587                                db.delete(sGroupmembershipTable,
3588                                        Phones._ID + "=" + cLocal.getLong(idColLocal), null);
3589                            }
3590                            break;
3591
3592                        case RIGHT:
3593                        case BOTH:
3594                            mValues.clear();
3595                            DatabaseUtils.cursorStringToContentValues(cRemote,
3596                                    GroupMembership.GROUP_SYNC_ACCOUNT, mValues);
3597                            DatabaseUtils.cursorStringToContentValues(cRemote,
3598                                    GroupMembership.GROUP_SYNC_ID, mValues);
3599                            if (joinResult == CursorJoiner.Result.RIGHT) {
3600                                mValues.put(GroupMembership.PERSON_ID, localPersonID);
3601                                mGroupMembershipInserter.insert(mValues);
3602                            } else {
3603                                db.update(sGroupmembershipTable, mValues,
3604                                        "_id =" + cLocal.getLong(idColLocal), null /* whereArgs */);
3605                            }
3606                            break;
3607                    }
3608                }
3609            } finally {
3610                if (cRemote != null) cRemote.deactivate();
3611                if (cLocal != null) cLocal.deactivate();
3612            }
3613
3614            // Extensions
3615            cRemote = null;
3616            cLocal = null;
3617            try {
3618                cLocal = doSubQuery(db,
3619                        sExtensionsTable, null, localPersonID, Extensions.NAME);
3620                cRemote = doSubQuery(diffsDb,
3621                        sExtensionsTable, null, diffsPersonID, Extensions.NAME);
3622
3623                final int idColLocal = cLocal.getColumnIndexOrThrow(Extensions._ID);
3624                CursorJoiner joiner = new CursorJoiner(
3625                        cLocal, sExtensionsKeyColumns, cRemote, sExtensionsKeyColumns);
3626                for (CursorJoiner.Result joinResult : joiner) {
3627                    switch(joinResult) {
3628                        case LEFT:
3629                            if (!conflicts) {
3630                                db.delete(sExtensionsTable,
3631                                        Phones._ID + "=" + cLocal.getLong(idColLocal), null);
3632                            }
3633                            break;
3634
3635                        case RIGHT:
3636                        case BOTH:
3637                            mValues.clear();
3638                            DatabaseUtils.cursorStringToContentValues(cRemote,
3639                                    Extensions.NAME, mValues);
3640                            DatabaseUtils.cursorStringToContentValues(cRemote,
3641                                    Extensions.VALUE, mValues);
3642                            if (joinResult == CursorJoiner.Result.RIGHT) {
3643                                mValues.put(Extensions.PERSON_ID, localPersonID);
3644                                mExtensionsInserter.insert(mValues);
3645                            } else {
3646                                db.update(sExtensionsTable, mValues,
3647                                        "_id =" + cLocal.getLong(idColLocal), null /* whereArgs */);
3648                            }
3649                            break;
3650                    }
3651                }
3652            } finally {
3653                if (cRemote != null) cRemote.deactivate();
3654                if (cLocal != null) cLocal.deactivate();
3655            }
3656
3657            // Copy the Photo's server id and account so that the merger will find it
3658            cRemote = doSubQuery(diffsDb, sPhotosTable, null, diffsPersonID, null);
3659            try {
3660                if(cRemote.moveToNext()) {
3661                    mValues.clear();
3662                    DatabaseUtils.cursorStringToContentValues(cRemote, Photos._SYNC_ID, mValues);
3663                    DatabaseUtils.cursorStringToContentValues(cRemote, Photos._SYNC_ACCOUNT, mValues);
3664                    db.update(sPhotosTable, mValues, Photos.PERSON_ID + '=' + localPersonID, null);
3665                }
3666            } finally {
3667                cRemote.deactivate();
3668            }
3669
3670            // make sure there is exactly one primary set for each of these types
3671            Long primaryPhoneId = setSinglePrimary(
3672                    primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_PHONE);
3673
3674            Long primaryEmailId = setSinglePrimary(
3675                    primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_EMAIL);
3676
3677            Long primaryOrganizationId = setSinglePrimary(
3678                    primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_ORGANIZATION);
3679
3680            setSinglePrimary(primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_IM);
3681
3682            setSinglePrimary(primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_POSTAL);
3683
3684            // Update the person
3685            mValues.clear();
3686            DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_ID, mValues);
3687            DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_TIME, mValues);
3688            DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_VERSION, mValues);
3689            DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_ACCOUNT, mValues);
3690            DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NAME, mValues);
3691            DatabaseUtils.cursorStringToContentValues(diffsCursor, People.PHONETIC_NAME, mValues);
3692            DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NOTES, mValues);
3693            mValues.put(People.PRIMARY_PHONE_ID, primaryPhoneId);
3694            mValues.put(People.PRIMARY_EMAIL_ID, primaryEmailId);
3695            mValues.put(People.PRIMARY_ORGANIZATION_ID, primaryOrganizationId);
3696            final boolean isStarred = queryGroupMembershipContainsStarred(localPersonID);
3697            mValues.put(People.STARRED, isStarred ? 1 : 0);
3698            mValues.put(People._SYNC_DIRTY, conflicts ? 1 : 0);
3699            db.update(mTable, mValues, People._ID + '=' + localPersonID, null);
3700        }
3701
3702        private void savePrimaryId(Map<Integer, Long> primaryDiffs, Integer kind, long localId) {
3703            if (primaryDiffs.containsKey(kind)) {
3704                throw new IllegalArgumentException("more than one of kind "
3705                        + kind + " was marked as primary");
3706            }
3707            primaryDiffs.put(kind, localId);
3708        }
3709
3710        private Long setSinglePrimary(
3711                Map<Integer, Long> diffsMap,
3712                Map<Integer, Long> localMap,
3713                long localPersonID, int kind) {
3714            Long primaryId = diffsMap.containsKey(kind) ? diffsMap.get(kind) : null;
3715            if (primaryId == null) {
3716                primaryId = localMap.containsKey(kind) ? localMap.get(kind) : null;
3717            }
3718            if (primaryId == null) {
3719                primaryId = findNewPrimary(kind, localPersonID, null);
3720            }
3721            clearOtherIsPrimary(kind, localPersonID, primaryId);
3722            return primaryId;
3723        }
3724
3725        /**
3726         * Returns a cursor on the specified table that selects rows where
3727         * the "person" column is equal to the personId parameter. The cursor
3728         * is also saved and may be returned in future calls where db and table
3729         * parameter are the same. In that case the projection and orderBy parameters
3730         * are ignored, so one must take care to not change those parameters across
3731         * multiple calls to the same db/table.
3732         * <p>
3733         * Since the cursor may be saced by this call, the caller must be sure to not
3734         * close the cursor, though they still must deactivate it when they are done
3735         * with it.
3736         */
3737        private Cursor doSubQuery(SQLiteDatabase db, String table, String[] projection,
3738                long personId, String orderBy) {
3739            final String[] selectArgs = new String[]{Long.toString(personId)};
3740            final String key = (db == getDatabase() ? "local_" : "remote_") + table;
3741            SQLiteCursor cursor = mCursorMap.get(key);
3742
3743            // don't use the cached cursor if it is from a different DB
3744            if (cursor != null && cursor.getDatabase() != db) {
3745                cursor.close();
3746                cursor = null;
3747            }
3748
3749            // If we can't find a cached cursor then create a new one and add it to the cache.
3750            // Otherwise just change the selection arguments and requery it.
3751            if (cursor == null) {
3752                cursor = (SQLiteCursor)db.query(table, projection, "person=?", selectArgs,
3753                        null, null, orderBy);
3754                mCursorMap.put(key, cursor);
3755            } else {
3756                cursor.setSelectionArguments(selectArgs);
3757                cursor.requery();
3758            }
3759            return cursor;
3760        }
3761    }
3762
3763    protected class GroupMerger extends AbstractTableMerger {
3764        private ContentValues mValues = new ContentValues();
3765
3766        private static final String UNSYNCED_GROUP_BY_NAME_WHERE_CLAUSE =
3767                Groups._SYNC_ID + " is null AND "
3768                        + Groups._SYNC_ACCOUNT + " is null AND "
3769                        + Groups.NAME + "=?";
3770
3771        private static final String UNSYNCED_GROUP_BY_SYSTEM_ID_WHERE_CLAUSE =
3772                Groups._SYNC_ID + " is null AND "
3773                        + Groups._SYNC_ACCOUNT + " is null AND "
3774                        + Groups.SYSTEM_ID + "=?";
3775
3776        public GroupMerger()
3777        {
3778            super(getDatabase(), sGroupsTable, sGroupsURL, sDeletedGroupsTable, sDeletedGroupsURL);
3779        }
3780
3781        @Override
3782        protected void notifyChanges() {
3783            // notify that a change has occurred.
3784            getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI,
3785                    null /* observer */, false /* do not sync to network */);
3786        }
3787
3788        @Override
3789        public void insertRow(ContentProvider diffs, Cursor cursor) {
3790            // if an unsynced group with this name already exists then update it, otherwise
3791            // insert a new group
3792            mValues.clear();
3793            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ID, mValues);
3794            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues);
3795            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues);
3796            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues);
3797            DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues);
3798            DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues);
3799            DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues);
3800            mValues.put(Groups._SYNC_DIRTY, 0);
3801
3802            final String systemId = mValues.getAsString(Groups.SYSTEM_ID);
3803            boolean rowUpdated = false;
3804            if (TextUtils.isEmpty(systemId)) {
3805                rowUpdated = getDatabase().update(mTable, mValues,
3806                    UNSYNCED_GROUP_BY_NAME_WHERE_CLAUSE,
3807                    new String[]{mValues.getAsString(Groups.NAME)}) > 0;
3808            } else {
3809                rowUpdated = getDatabase().update(mTable, mValues,
3810                    UNSYNCED_GROUP_BY_SYSTEM_ID_WHERE_CLAUSE,
3811                    new String[]{systemId}) > 0;
3812            }
3813            if (!rowUpdated) {
3814                mGroupsInserter.insert(mValues);
3815            } else {
3816                // We may have just synced the metadata for a groups we previously marked for
3817                // syncing.
3818                final ContentResolver cr = getContext().getContentResolver();
3819                final String account = mValues.getAsString(Groups._SYNC_ACCOUNT);
3820                onLocalChangesForAccount(cr, account, false);
3821            }
3822
3823            String oldName = null;
3824            String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME));
3825            String account = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
3826            String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
3827            // this must come after the insert, otherwise the join won't work
3828            fixupPeopleStarredOnGroupRename(oldName, newName, account, syncId);
3829        }
3830
3831        @Override
3832        public void updateRow(long localId, ContentProvider diffs, Cursor diffsCursor) {
3833            updateOrResolveRow(localId, null, diffs, diffsCursor, false);
3834        }
3835
3836        @Override
3837        public void resolveRow(long localId, String syncID,
3838                ContentProvider diffs, Cursor diffsCursor) {
3839            updateOrResolveRow(localId, syncID, diffs, diffsCursor, true);
3840        }
3841
3842        protected void updateOrResolveRow(long localRowId, String syncID,
3843                ContentProvider diffs, Cursor cursor, boolean conflicts) {
3844            final SQLiteDatabase db = getDatabase();
3845
3846            String oldName = DatabaseUtils.stringForQuery(db,
3847                    "select name from groups where _id=" + localRowId, null);
3848            String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME));
3849            String account = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
3850            String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
3851            // this can come before or after the delete
3852            fixupPeopleStarredOnGroupRename(oldName, newName, account, syncId);
3853
3854            mValues.clear();
3855            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ID, mValues);
3856            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues);
3857            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues);
3858            DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues);
3859            DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues);
3860            DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues);
3861            DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues);
3862            mValues.put(Groups._SYNC_DIRTY, 0);
3863            db.update(mTable, mValues, Groups._ID + '=' + localRowId, null);
3864        }
3865
3866        @Override
3867        public void deleteRow(Cursor cursor) {
3868            // we have to read this row from the DB since the projection that is used
3869            // by cursor doesn't necessarily contain the columns we need
3870            Cursor c = getDatabase().query(sGroupsTable, null,
3871                    "_id=" + cursor.getLong(cursor.getColumnIndexOrThrow(Groups._ID)),
3872                    null, null, null, null);
3873            try {
3874                c.moveToNext();
3875                String oldName = c.getString(c.getColumnIndexOrThrow(Groups.NAME));
3876                String newName = null;
3877                String account = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
3878                String syncId = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ID));
3879                String systemId = c.getString(c.getColumnIndexOrThrow(Groups.SYSTEM_ID));
3880                if (!TextUtils.isEmpty(systemId)) {
3881                    // We don't support deleting of system groups, but due to a server bug they
3882                    // occasionally get sent. Ignore the delete.
3883                    Log.w(TAG, "ignoring a delete for a system group: " +
3884                            DatabaseUtils.dumpCurrentRowToString(c));
3885                    cursor.moveToNext();
3886                    return;
3887                }
3888
3889                // this must come before the delete, since the join won't work once this row is gone
3890                fixupPeopleStarredOnGroupRename(oldName, newName, account, syncId);
3891            } finally {
3892                c.close();
3893            }
3894
3895            cursor.deleteRow();
3896        }
3897    }
3898
3899    protected class PhotoMerger extends AbstractTableMerger {
3900        private ContentValues mValues = new ContentValues();
3901
3902        public PhotoMerger() {
3903            super(getDatabase(), sPhotosTable, sPhotosURL, null, null);
3904        }
3905
3906        @Override
3907        protected void notifyChanges() {
3908            // notify that a change has occurred.
3909            getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI,
3910                    null /* observer */, false /* do not sync to network */);
3911        }
3912
3913        @Override
3914        public void insertRow(ContentProvider diffs, Cursor cursor) {
3915            // This photo may correspond to a contact that is in the delete table. If so then
3916            // ignore this insert.
3917            String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Photos._SYNC_ID));
3918            boolean contactIsDeleted = DatabaseUtils.longForQuery(getDatabase(),
3919                    "select count(*) from _deleted_people where _sync_id=?",
3920                    new String[]{syncId}) > 0;
3921            if (contactIsDeleted) {
3922                return;
3923            }
3924
3925            throw new UnsupportedOperationException(
3926                    "the photo row is inserted by PersonMerger.insertRow");
3927        }
3928
3929        @Override
3930        public void updateRow(long localId, ContentProvider diffs, Cursor diffsCursor) {
3931            updateOrResolveRow(localId, null, diffs, diffsCursor, false);
3932        }
3933
3934        @Override
3935        public void resolveRow(long localId, String syncID,
3936                ContentProvider diffs, Cursor diffsCursor) {
3937            updateOrResolveRow(localId, syncID, diffs, diffsCursor, true);
3938        }
3939
3940        protected void updateOrResolveRow(long localRowId, String syncID,
3941                ContentProvider diffs, Cursor cursor, boolean conflicts) {
3942            if (Log.isLoggable(TAG, Log.VERBOSE)) {
3943                Log.v(TAG, "PhotoMerger.updateOrResolveRow: localRowId " + localRowId
3944                        + ", syncId " + syncID + ", conflicts " + conflicts
3945                        + ", server row " + DatabaseUtils.dumpCurrentRowToString(cursor));
3946            }
3947            mValues.clear();
3948            DatabaseUtils.cursorStringToContentValues(cursor, Photos._SYNC_TIME, mValues);
3949            DatabaseUtils.cursorStringToContentValues(cursor, Photos._SYNC_VERSION, mValues);
3950            DatabaseUtils.cursorStringToContentValues(cursor, Photos.EXISTS_ON_SERVER, mValues);
3951            // reset the error field to allow the phone to attempt to redownload the photo.
3952            mValues.put(Photos.SYNC_ERROR, (String)null);
3953
3954            // If the photo didn't change locally and the server doesn't have a photo for this
3955            // contact then delete the local photo.
3956            long syncDirty = DatabaseUtils.longForQuery(getDatabase(),
3957                    "SELECT _sync_dirty FROM photos WHERE _id=" + localRowId
3958                            + " UNION SELECT 0 AS _sync_dirty ORDER BY _sync_dirty DESC LIMIT 1",
3959                    null);
3960            if (syncDirty == 0) {
3961                if (mValues.getAsInteger(Photos.EXISTS_ON_SERVER) == 0) {
3962                    mValues.put(Photos.DATA, (String)null);
3963                    mValues.put(Photos.LOCAL_VERSION, mValues.getAsString(Photos.LOCAL_VERSION));
3964                }
3965                // if it does exist on the server then we will attempt to download it later
3966            }
3967            // if it does conflict then we will send the client version of the photo to
3968            // the server later. That will trigger a new sync of the photo data which will
3969            // cause this method to be called again, at which time the row will no longer
3970            // conflict. We will then download the photo we just sent to the server and
3971            // set the LOCAL_VERSION to match the data we just downloaded.
3972
3973            getDatabase().update(mTable, mValues, Photos._ID + '=' + localRowId, null);
3974        }
3975
3976        @Override
3977        public void deleteRow(Cursor cursor) {
3978            // this row is never deleted explicitly, instead it is deleted by a trigger on
3979            // the people table
3980            cursor.moveToNext();
3981        }
3982    }
3983
3984    private static final String TAG = "ContactsProvider";
3985
3986    /* package private */ static final String DATABASE_NAME = "contacts.db";
3987    /* package private */ static final int DATABASE_VERSION = 82;
3988
3989    protected static final String CONTACTS_AUTHORITY = "contacts";
3990    protected static final String CALL_LOG_AUTHORITY = "call_log";
3991
3992    private static final int PEOPLE_BASE = 0;
3993    private static final int PEOPLE = PEOPLE_BASE;
3994    private static final int PEOPLE_FILTER = PEOPLE_BASE + 1;
3995    private static final int PEOPLE_ID = PEOPLE_BASE + 2;
3996    private static final int PEOPLE_PHONES = PEOPLE_BASE + 3;
3997    private static final int PEOPLE_PHONES_ID = PEOPLE_BASE + 4;
3998    private static final int PEOPLE_CONTACTMETHODS = PEOPLE_BASE + 5;
3999    private static final int PEOPLE_CONTACTMETHODS_ID = PEOPLE_BASE + 6;
4000    private static final int PEOPLE_RAW = PEOPLE_BASE + 7;
4001    private static final int PEOPLE_WITH_PHONES_FILTER = PEOPLE_BASE + 8;
4002    private static final int PEOPLE_STREQUENT = PEOPLE_BASE + 9;
4003    private static final int PEOPLE_STREQUENT_FILTER = PEOPLE_BASE + 10;
4004    private static final int PEOPLE_ORGANIZATIONS = PEOPLE_BASE + 11;
4005    private static final int PEOPLE_ORGANIZATIONS_ID = PEOPLE_BASE + 12;
4006    private static final int PEOPLE_GROUPMEMBERSHIP = PEOPLE_BASE + 13;
4007    private static final int PEOPLE_GROUPMEMBERSHIP_ID = PEOPLE_BASE + 14;
4008    private static final int PEOPLE_PHOTO = PEOPLE_BASE + 15;
4009    private static final int PEOPLE_EXTENSIONS = PEOPLE_BASE + 16;
4010    private static final int PEOPLE_EXTENSIONS_ID = PEOPLE_BASE + 17;
4011    private static final int PEOPLE_CONTACTMETHODS_WITH_PRESENCE = PEOPLE_BASE + 18;
4012    private static final int PEOPLE_OWNER = PEOPLE_BASE + 19;
4013    private static final int PEOPLE_UPDATE_CONTACT_TIME = PEOPLE_BASE + 20;
4014    private static final int PEOPLE_PHONES_WITH_PRESENCE = PEOPLE_BASE + 21;
4015    private static final int PEOPLE_WITH_EMAIL_OR_IM_FILTER = PEOPLE_BASE + 22;
4016    private static final int PEOPLE_PHOTO_DATA = PEOPLE_BASE + 23;
4017
4018    private static final int DELETED_BASE = 1000;
4019    private static final int DELETED_PEOPLE = DELETED_BASE;
4020    private static final int DELETED_GROUPS = DELETED_BASE + 1;
4021
4022    private static final int PHONES_BASE = 2000;
4023    private static final int PHONES = PHONES_BASE;
4024    private static final int PHONES_ID = PHONES_BASE + 1;
4025    private static final int PHONES_FILTER = PHONES_BASE + 2;
4026    private static final int PHONES_FILTER_NAME = PHONES_BASE + 3;
4027    private static final int PHONES_MOBILE_FILTER_NAME = PHONES_BASE + 4;
4028    private static final int PHONES_WITH_PRESENCE = PHONES_BASE + 5;
4029
4030    private static final int CONTACTMETHODS_BASE = 3000;
4031    private static final int CONTACTMETHODS = CONTACTMETHODS_BASE;
4032    private static final int CONTACTMETHODS_ID = CONTACTMETHODS_BASE + 1;
4033    private static final int CONTACTMETHODS_EMAIL = CONTACTMETHODS_BASE + 2;
4034    private static final int CONTACTMETHODS_EMAIL_FILTER = CONTACTMETHODS_BASE + 3;
4035    private static final int CONTACTMETHODS_WITH_PRESENCE = CONTACTMETHODS_BASE + 4;
4036
4037    private static final int CALLS_BASE = 4000;
4038    private static final int CALLS = CALLS_BASE;
4039    private static final int CALLS_ID = CALLS_BASE + 1;
4040    private static final int CALLS_FILTER = CALLS_BASE + 2;
4041
4042    private static final int PRESENCE_BASE = 5000;
4043    private static final int PRESENCE = PRESENCE_BASE;
4044    private static final int PRESENCE_ID = PRESENCE_BASE + 1;
4045
4046    private static final int ORGANIZATIONS_BASE = 6000;
4047    private static final int ORGANIZATIONS = ORGANIZATIONS_BASE;
4048    private static final int ORGANIZATIONS_ID = ORGANIZATIONS_BASE + 1;
4049
4050    private static final int VOICE_DIALER_TIMESTAMP = 7000;
4051    private static final int SEARCH_SUGGESTIONS = 7001;
4052    private static final int SEARCH_SHORTCUT = 7002;
4053
4054    private static final int GROUPS_BASE = 8000;
4055    private static final int GROUPS = GROUPS_BASE;
4056    private static final int GROUPS_ID = GROUPS_BASE + 2;
4057    private static final int GROUP_NAME_MEMBERS = GROUPS_BASE + 3;
4058    private static final int GROUP_NAME_MEMBERS_FILTER = GROUPS_BASE + 4;
4059    private static final int GROUP_SYSTEM_ID_MEMBERS = GROUPS_BASE + 5;
4060    private static final int GROUP_SYSTEM_ID_MEMBERS_FILTER = GROUPS_BASE + 6;
4061
4062    private static final int GROUPMEMBERSHIP_BASE = 9000;
4063    private static final int GROUPMEMBERSHIP = GROUPMEMBERSHIP_BASE;
4064    private static final int GROUPMEMBERSHIP_ID = GROUPMEMBERSHIP_BASE + 2;
4065    private static final int GROUPMEMBERSHIP_RAW = GROUPMEMBERSHIP_BASE + 3;
4066
4067    private static final int PHOTOS_BASE = 10000;
4068    private static final int PHOTOS = PHOTOS_BASE;
4069    private static final int PHOTOS_ID = PHOTOS_BASE + 1;
4070
4071    private static final int EXTENSIONS_BASE = 11000;
4072    private static final int EXTENSIONS = EXTENSIONS_BASE;
4073    private static final int EXTENSIONS_ID = EXTENSIONS_BASE + 2;
4074
4075    private static final int SETTINGS = 12000;
4076
4077    private static final int LIVE_FOLDERS_BASE = 13000;
4078    private static final int LIVE_FOLDERS_PEOPLE = LIVE_FOLDERS_BASE + 1;
4079    private static final int LIVE_FOLDERS_PEOPLE_GROUP_NAME = LIVE_FOLDERS_BASE + 2;
4080    private static final int LIVE_FOLDERS_PEOPLE_WITH_PHONES = LIVE_FOLDERS_BASE + 3;
4081    private static final int LIVE_FOLDERS_PEOPLE_FAVORITES = LIVE_FOLDERS_BASE + 4;
4082
4083    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
4084
4085    private static final HashMap<String, String> sGroupsProjectionMap;
4086    private static final HashMap<String, String> sPeopleProjectionMap;
4087    private static final HashMap<String, String> sPeopleWithPhotoProjectionMap;
4088    private static final HashMap<String, String> sPeopleWithEmailOrImProjectionMap;
4089    /** Used to force items to the top of a times_contacted list */
4090    private static final HashMap<String, String> sStrequentStarredProjectionMap;
4091    private static final HashMap<String, String> sCallsProjectionMap;
4092    private static final HashMap<String, String> sPhonesProjectionMap;
4093    private static final HashMap<String, String> sPhonesWithPresenceProjectionMap;
4094    private static final HashMap<String, String> sContactMethodsProjectionMap;
4095    private static final HashMap<String, String> sContactMethodsWithPresenceProjectionMap;
4096    private static final HashMap<String, String> sPresenceProjectionMap;
4097    private static final HashMap<String, String> sEmailSearchProjectionMap;
4098    private static final HashMap<String, String> sOrganizationsProjectionMap;
4099    private static final HashMap<String, String> sGroupMembershipProjectionMap;
4100    private static final HashMap<String, String> sPhotosProjectionMap;
4101    private static final HashMap<String, String> sExtensionsProjectionMap;
4102    private static final HashMap<String, String> sLiveFoldersProjectionMap;
4103
4104    private static final String sPhonesKeyOrderBy;
4105    private static final String sContactMethodsKeyOrderBy;
4106    private static final String sOrganizationsKeyOrderBy;
4107    private static final String sGroupmembershipKeyOrderBy;
4108
4109    private static final String DISPLAY_NAME_SQL
4110            = "(CASE WHEN (name IS NOT NULL AND name != '') "
4111                + "THEN name "
4112            + "ELSE "
4113                + "(CASE WHEN primary_organization is NOT NULL THEN "
4114                    + "(SELECT company FROM organizations WHERE "
4115                        + "organizations._id = primary_organization) "
4116                + "ELSE "
4117                    + "(CASE WHEN primary_phone IS NOT NULL THEN "
4118                        +"(SELECT number FROM phones WHERE phones._id = primary_phone) "
4119                    + "ELSE "
4120                        + "(CASE WHEN primary_email IS NOT NULL THEN "
4121                            + "(SELECT data FROM contact_methods WHERE "
4122                                + "contact_methods._id = primary_email) "
4123                        + "ELSE "
4124                            + "null "
4125                        + "END) "
4126                    + "END) "
4127                + "END) "
4128            + "END) ";
4129
4130    private static final String PHONETICALLY_SORTABLE_STRING_SQL =
4131        "GET_PHONETICALLY_SORTABLE_STRING("
4132            + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') "
4133                + "THEN phonetic_name "
4134            + "ELSE "
4135                + "(CASE WHEN (name is NOT NULL AND name != '') "
4136                    + "THEN name "
4137                + "ELSE "
4138                    + "(CASE WHEN primary_email IS NOT NULL THEN "
4139                        + "(SELECT data FROM contact_methods WHERE "
4140                            + "contact_methods._id = primary_email) "
4141                    + "ELSE "
4142                        + "(CASE WHEN primary_phone IS NOT NULL THEN "
4143                            + "(SELECT number FROM phones WHERE phones._id = primary_phone) "
4144                        + "ELSE "
4145                            + "null "
4146                        + "END) "
4147                    + "END) "
4148                + "END) "
4149            + "END"
4150        + ")";
4151
4152    private static final String NAME_WHEN_SQL
4153            = " WHEN name is NOT NULL ANd name != '' THEN name";
4154
4155    private static final String PRIMARY_ORGANIZATION_WHEN_SQL
4156            = " WHEN primary_organization is NOT NULL THEN "
4157            + "(SELECT company FROM organizations WHERE organizations._id = primary_organization)";
4158
4159    private static final String PRIMARY_PHONE_WHEN_SQL
4160            = " WHEN primary_phone IS NOT NULL THEN "
4161            + "(SELECT number FROM phones WHERE phones._id = primary_phone)";
4162
4163    private static final String PRIMARY_EMAIL_WHEN_SQL
4164            = " WHEN primary_email IS NOT NULL THEN "
4165            + "(SELECT data FROM contact_methods WHERE contact_methods._id = primary_email)";
4166
4167    // The outer CASE is for figuring out what info DISPLAY_NAME_SQL returned.
4168    // We then pick the next piece of info, to avoid the two lines in the search
4169    // suggestion being identical.
4170    private static final String SUGGEST_DESCRIPTION_SQL
4171            = "(CASE"
4172                // DISPLAY_NAME_SQL returns name, try org, phone, email
4173                + " WHEN (name IS NOT NULL AND name != '') THEN "
4174                    + "(CASE"
4175                        + PRIMARY_ORGANIZATION_WHEN_SQL
4176                        + PRIMARY_PHONE_WHEN_SQL
4177                        + PRIMARY_EMAIL_WHEN_SQL
4178                        + " ELSE null END)"
4179                // DISPLAY_NAME_SQL returns org, try phone, email
4180                + " WHEN primary_organization is NOT NULL THEN "
4181                    + "(CASE"
4182                        + PRIMARY_PHONE_WHEN_SQL
4183                        + PRIMARY_EMAIL_WHEN_SQL
4184                        + " ELSE null END)"
4185                // DISPLAY_NAME_SQL returns phone, try email
4186                + " WHEN primary_phone IS NOT NULL THEN "
4187                    + "(CASE"
4188                        + PRIMARY_EMAIL_WHEN_SQL
4189                        + " ELSE null END)"
4190                // DISPLAY_NAME_SQL returns email or NULL, return NULL
4191                + " ELSE null END)";
4192
4193    private static final String PRESENCE_ICON_SQL
4194            = "(CASE"
4195                + buildPresenceStatusWhen(People.OFFLINE)
4196                + buildPresenceStatusWhen(People.INVISIBLE)
4197                + buildPresenceStatusWhen(People.AWAY)
4198                + buildPresenceStatusWhen(People.IDLE)
4199                + buildPresenceStatusWhen(People.DO_NOT_DISTURB)
4200                + buildPresenceStatusWhen(People.AVAILABLE)
4201                + " ELSE null END)";
4202
4203    private static String buildPresenceStatusWhen(int status) {
4204        return " WHEN " + Presence.PRESENCE_STATUS + " = " + status
4205            + " THEN " + Presence.getPresenceIconResourceId(status);
4206    }
4207
4208
4209    // This is similar to DISPLAY_NAME_SQL. Only difference is that this prioritize
4210    // phonetic_name.
4211    private static final String PHONETIC_LOOKUP_STRING_SQL =
4212        "GET_NORMALIZED_STRING("
4213            + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') "
4214                + "THEN phonetic_name "
4215            + "ELSE "
4216                + "(CASE WHEN (name is NOT NULL AND name != '') "
4217                    + "THEN name "
4218                + "ELSE "
4219                    + "(CASE WHEN primary_organization is NOT NULL THEN "
4220                        + "(SELECT company FROM organizations WHERE "
4221                            + "organizations._id = primary_organization) "
4222                    + "ELSE "
4223                        + "(CASE WHEN primary_phone IS NOT NULL THEN "
4224                            +"(SELECT number FROM phones WHERE phones._id = primary_phone) "
4225                        + "ELSE "
4226                            + "(CASE WHEN primary_email IS NOT NULL THEN "
4227                                + "(SELECT data FROM contact_methods WHERE "
4228                                    + "contact_methods._id = primary_email) "
4229                            + "ELSE "
4230                                + "null "
4231                            + "END) "
4232                        + "END) "
4233                    + "END) "
4234                + "END) "
4235            + "END)";
4236
4237    private static final String PHONETIC_SUGGEST_DESCRIPTION_SQL =
4238        "(CASE"
4239            + " WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') THEN "
4240            // PHONETIC_LOOKUP_STRING_SQL returns phonetic_name. try name, org, phone, email
4241                + "(CASE"
4242                    + NAME_WHEN_SQL
4243                    + PRIMARY_ORGANIZATION_WHEN_SQL
4244                    + PRIMARY_PHONE_WHEN_SQL
4245                    + PRIMARY_EMAIL_WHEN_SQL
4246                    + " ELSE null END)"
4247            // PHONETIC_LOOKUP_STRING_SQL returns name, try org, phone, email
4248            + " WHEN (name IS NOT NULL AND name != '') THEN "
4249                + "(CASE"
4250                    + PRIMARY_ORGANIZATION_WHEN_SQL
4251                    + PRIMARY_PHONE_WHEN_SQL
4252                    + PRIMARY_EMAIL_WHEN_SQL
4253                    + " ELSE null END)"
4254            // PHONETIC_LOOKUP_STRING_SQL returns org, try phone, email
4255            + " WHEN primary_organization is NOT NULL THEN "
4256                + "(CASE"
4257                    + PRIMARY_PHONE_WHEN_SQL
4258                    + PRIMARY_EMAIL_WHEN_SQL
4259                    + " ELSE null END)"
4260            // PHONETIC_LOOKUP_STRING_SQL returns phone, try email
4261            + " WHEN primary_phone IS NOT NULL THEN "
4262                + "(CASE"
4263                    + PRIMARY_EMAIL_WHEN_SQL
4264                    + " ELSE null END)"
4265            // PHONETIC_LOOKUP_STRING_SQL returns email or NULL, return NULL
4266        + " ELSE null END)";
4267
4268    // "primary_organization" etc. are not considered here, since peopleLookup does not
4269    // consider them either.
4270    private static final String PHONETIC_LOOKUP_SQL_SIMPLE =
4271        "GET_NORMALIZED_STRING("
4272            + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') "
4273                + "THEN phonetic_name "
4274            + "ELSE "
4275                + "(CASE WHEN (name is NOT NULL AND name != '') "
4276                    + "THEN name "
4277                + "ELSE "
4278                    + "'' "
4279                + "END) "
4280            + "END)";
4281
4282    private static final String PHONETIC_LOOKUP_SQL_SIMPLE_WITH_NEW =
4283        "GET_NORMALIZED_STRING("
4284            + "CASE WHEN (new.phonetic_name IS NOT NULL AND new.phonetic_name != '') "
4285                + "THEN new.phonetic_name "
4286            + "ELSE "
4287                + "(CASE WHEN (new.name is NOT NULL AND new.name != '') "
4288                    + "THEN new.name "
4289                + "ELSE "
4290                    + "'' "
4291                + "END) "
4292            + "END)";
4293
4294    private static final String[] sPhonesKeyColumns;
4295    private static final String[] sContactMethodsKeyColumns;
4296    private static final String[] sOrganizationsKeyColumns;
4297    private static final String[] sGroupmembershipKeyColumns;
4298    private static final String[] sExtensionsKeyColumns;
4299
4300    static private String buildOrderBy(String table, String... columns) {
4301        StringBuilder sb = null;
4302        for (String column : columns) {
4303            if (sb == null) {
4304                sb = new StringBuilder();
4305            } else {
4306                sb.append(", ");
4307            }
4308            sb.append(table);
4309            sb.append('.');
4310            sb.append(column);
4311        }
4312        return (sb == null) ? "" : sb.toString();
4313    }
4314
4315    /**
4316     * @return true when phonetic_name should be considered when looking up people's names.
4317     */
4318    private synchronized boolean usePhoneticNameForPeopleLookup() {
4319        return mSearchSuggestionLanguage.equals(Locale.JAPAN.getLanguage());
4320    }
4321
4322    private void updateSuggestColumnTexts() {
4323        if (usePhoneticNameForPeopleLookup()) {
4324            mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
4325                    PHONETIC_LOOKUP_STRING_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
4326            mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
4327                    PHONETIC_SUGGEST_DESCRIPTION_SQL + " AS " +
4328                    SearchManager.SUGGEST_COLUMN_TEXT_2);
4329        } else {
4330            mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
4331                    DISPLAY_NAME_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
4332            mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
4333                    SUGGEST_DESCRIPTION_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2);
4334        }
4335    }
4336
4337    static {
4338        // Contacts URI matching table
4339        UriMatcher matcher = sURIMatcher;
4340        matcher.addURI(CONTACTS_AUTHORITY, "extensions", EXTENSIONS);
4341        matcher.addURI(CONTACTS_AUTHORITY, "extensions/#", EXTENSIONS_ID);
4342        matcher.addURI(CONTACTS_AUTHORITY, "groups", GROUPS);
4343        matcher.addURI(CONTACTS_AUTHORITY, "groups/#", GROUPS_ID);
4344        matcher.addURI(CONTACTS_AUTHORITY, "groups/name/*/members", GROUP_NAME_MEMBERS);
4345        matcher.addURI(CONTACTS_AUTHORITY, "groups/name/*/members/filter/*",
4346                GROUP_NAME_MEMBERS_FILTER);
4347        matcher.addURI(CONTACTS_AUTHORITY, "groups/system_id/*/members", GROUP_SYSTEM_ID_MEMBERS);
4348        matcher.addURI(CONTACTS_AUTHORITY, "groups/system_id/*/members/filter/*",
4349                GROUP_SYSTEM_ID_MEMBERS_FILTER);
4350        matcher.addURI(CONTACTS_AUTHORITY, "groupmembership", GROUPMEMBERSHIP);
4351        matcher.addURI(CONTACTS_AUTHORITY, "groupmembership/#", GROUPMEMBERSHIP_ID);
4352        matcher.addURI(CONTACTS_AUTHORITY, "groupmembershipraw", GROUPMEMBERSHIP_RAW);
4353        matcher.addURI(CONTACTS_AUTHORITY, "people", PEOPLE);
4354        matcher.addURI(CONTACTS_AUTHORITY, "people/strequent", PEOPLE_STREQUENT);
4355        matcher.addURI(CONTACTS_AUTHORITY, "people/strequent/filter/*", PEOPLE_STREQUENT_FILTER);
4356        matcher.addURI(CONTACTS_AUTHORITY, "people/filter/*", PEOPLE_FILTER);
4357        matcher.addURI(CONTACTS_AUTHORITY, "people/with_phones_filter/*",
4358                PEOPLE_WITH_PHONES_FILTER);
4359        matcher.addURI(CONTACTS_AUTHORITY, "people/with_email_or_im_filter/*",
4360                PEOPLE_WITH_EMAIL_OR_IM_FILTER);
4361        matcher.addURI(CONTACTS_AUTHORITY, "people/#", PEOPLE_ID);
4362        matcher.addURI(CONTACTS_AUTHORITY, "people/#/extensions", PEOPLE_EXTENSIONS);
4363        matcher.addURI(CONTACTS_AUTHORITY, "people/#/extensions/#", PEOPLE_EXTENSIONS_ID);
4364        matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones", PEOPLE_PHONES);
4365        matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones_with_presence",
4366                PEOPLE_PHONES_WITH_PRESENCE);
4367        matcher.addURI(CONTACTS_AUTHORITY, "people/#/photo", PEOPLE_PHOTO);
4368        matcher.addURI(CONTACTS_AUTHORITY, "people/#/photo/data", PEOPLE_PHOTO_DATA);
4369        matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones/#", PEOPLE_PHONES_ID);
4370        matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods", PEOPLE_CONTACTMETHODS);
4371        matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods_with_presence",
4372                PEOPLE_CONTACTMETHODS_WITH_PRESENCE);
4373        matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods/#", PEOPLE_CONTACTMETHODS_ID);
4374        matcher.addURI(CONTACTS_AUTHORITY, "people/#/organizations", PEOPLE_ORGANIZATIONS);
4375        matcher.addURI(CONTACTS_AUTHORITY, "people/#/organizations/#", PEOPLE_ORGANIZATIONS_ID);
4376        matcher.addURI(CONTACTS_AUTHORITY, "people/#/groupmembership", PEOPLE_GROUPMEMBERSHIP);
4377        matcher.addURI(CONTACTS_AUTHORITY, "people/#/groupmembership/#", PEOPLE_GROUPMEMBERSHIP_ID);
4378        matcher.addURI(CONTACTS_AUTHORITY, "people/raw", PEOPLE_RAW);
4379        matcher.addURI(CONTACTS_AUTHORITY, "people/owner", PEOPLE_OWNER);
4380        matcher.addURI(CONTACTS_AUTHORITY, "people/#/update_contact_time",
4381                PEOPLE_UPDATE_CONTACT_TIME);
4382        matcher.addURI(CONTACTS_AUTHORITY, "deleted_people", DELETED_PEOPLE);
4383        matcher.addURI(CONTACTS_AUTHORITY, "deleted_groups", DELETED_GROUPS);
4384        matcher.addURI(CONTACTS_AUTHORITY, "phones", PHONES);
4385        matcher.addURI(CONTACTS_AUTHORITY, "phones_with_presence", PHONES_WITH_PRESENCE);
4386        matcher.addURI(CONTACTS_AUTHORITY, "phones/filter/*", PHONES_FILTER);
4387        matcher.addURI(CONTACTS_AUTHORITY, "phones/filter_name/*", PHONES_FILTER_NAME);
4388        matcher.addURI(CONTACTS_AUTHORITY, "phones/mobile_filter_name/*",
4389                PHONES_MOBILE_FILTER_NAME);
4390        matcher.addURI(CONTACTS_AUTHORITY, "phones/#", PHONES_ID);
4391        matcher.addURI(CONTACTS_AUTHORITY, "photos", PHOTOS);
4392        matcher.addURI(CONTACTS_AUTHORITY, "photos/#", PHOTOS_ID);
4393        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods", CONTACTMETHODS);
4394        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/email", CONTACTMETHODS_EMAIL);
4395        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/email/*", CONTACTMETHODS_EMAIL_FILTER);
4396        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/#", CONTACTMETHODS_ID);
4397        matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/with_presence",
4398                CONTACTMETHODS_WITH_PRESENCE);
4399        matcher.addURI(CONTACTS_AUTHORITY, "presence", PRESENCE);
4400        matcher.addURI(CONTACTS_AUTHORITY, "presence/#", PRESENCE_ID);
4401        matcher.addURI(CONTACTS_AUTHORITY, "organizations", ORGANIZATIONS);
4402        matcher.addURI(CONTACTS_AUTHORITY, "organizations/#", ORGANIZATIONS_ID);
4403        matcher.addURI(CONTACTS_AUTHORITY, "voice_dialer_timestamp", VOICE_DIALER_TIMESTAMP);
4404        matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
4405                SEARCH_SUGGESTIONS);
4406        matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
4407                SEARCH_SUGGESTIONS);
4408        matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
4409                SEARCH_SHORTCUT);
4410        matcher.addURI(CONTACTS_AUTHORITY, "settings", SETTINGS);
4411
4412        matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people", LIVE_FOLDERS_PEOPLE);
4413        matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people/*",
4414                LIVE_FOLDERS_PEOPLE_GROUP_NAME);
4415        matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people_with_phones",
4416                LIVE_FOLDERS_PEOPLE_WITH_PHONES);
4417        matcher.addURI(CONTACTS_AUTHORITY, "live_folders/favorites",
4418                LIVE_FOLDERS_PEOPLE_FAVORITES);
4419
4420        // Call log URI matching table
4421        matcher.addURI(CALL_LOG_AUTHORITY, "calls", CALLS);
4422        matcher.addURI(CALL_LOG_AUTHORITY, "calls/filter/*", CALLS_FILTER);
4423        matcher.addURI(CALL_LOG_AUTHORITY, "calls/#", CALLS_ID);
4424
4425        HashMap<String, String> map;
4426
4427        // Create the common people columns
4428        HashMap<String, String> peopleColumns = new HashMap<String, String>();
4429        peopleColumns.put(PeopleColumns.NAME, People.NAME);
4430        peopleColumns.put(PeopleColumns.NOTES, People.NOTES);
4431        peopleColumns.put(PeopleColumns.TIMES_CONTACTED, People.TIMES_CONTACTED);
4432        peopleColumns.put(PeopleColumns.LAST_TIME_CONTACTED, People.LAST_TIME_CONTACTED);
4433        peopleColumns.put(PeopleColumns.STARRED, People.STARRED);
4434        peopleColumns.put(PeopleColumns.CUSTOM_RINGTONE, People.CUSTOM_RINGTONE);
4435        peopleColumns.put(PeopleColumns.SEND_TO_VOICEMAIL, People.SEND_TO_VOICEMAIL);
4436        peopleColumns.put(PeopleColumns.PHONETIC_NAME, People.PHONETIC_NAME);
4437        peopleColumns.put(PeopleColumns.DISPLAY_NAME,
4438                DISPLAY_NAME_SQL + " AS " + People.DISPLAY_NAME);
4439        peopleColumns.put(PeopleColumns.SORT_STRING,
4440                PHONETICALLY_SORTABLE_STRING_SQL + " AS " + People.SORT_STRING);
4441
4442        // Create the common groups columns
4443        HashMap<String, String> groupsColumns = new HashMap<String, String>();
4444        groupsColumns.put(GroupsColumns.NAME, Groups.NAME);
4445        groupsColumns.put(GroupsColumns.NOTES, Groups.NOTES);
4446        groupsColumns.put(GroupsColumns.SYSTEM_ID, Groups.SYSTEM_ID);
4447        groupsColumns.put(GroupsColumns.SHOULD_SYNC, Groups.SHOULD_SYNC);
4448
4449        // Create the common presence columns
4450        HashMap<String, String> presenceColumns = new HashMap<String, String>();
4451        presenceColumns.put(PresenceColumns.IM_PROTOCOL, PresenceColumns.IM_PROTOCOL);
4452        presenceColumns.put(PresenceColumns.IM_HANDLE, PresenceColumns.IM_HANDLE);
4453        presenceColumns.put(PresenceColumns.IM_ACCOUNT, PresenceColumns.IM_ACCOUNT);
4454        presenceColumns.put(PresenceColumns.PRESENCE_STATUS, PresenceColumns.PRESENCE_STATUS);
4455        presenceColumns.put(PresenceColumns.PRESENCE_CUSTOM_STATUS,
4456                PresenceColumns.PRESENCE_CUSTOM_STATUS);
4457
4458        // Create the common sync columns
4459        HashMap<String, String> syncColumns = new HashMap<String, String>();
4460        syncColumns.put(SyncConstValue._SYNC_ID, SyncConstValue._SYNC_ID);
4461        syncColumns.put(SyncConstValue._SYNC_TIME, SyncConstValue._SYNC_TIME);
4462        syncColumns.put(SyncConstValue._SYNC_VERSION, SyncConstValue._SYNC_VERSION);
4463        syncColumns.put(SyncConstValue._SYNC_LOCAL_ID, SyncConstValue._SYNC_LOCAL_ID);
4464        syncColumns.put(SyncConstValue._SYNC_DIRTY, SyncConstValue._SYNC_DIRTY);
4465        syncColumns.put(SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT);
4466
4467        // Phones columns
4468        HashMap<String, String> phonesColumns = new HashMap<String, String>();
4469        phonesColumns.put(Phones.NUMBER, Phones.NUMBER);
4470        phonesColumns.put(Phones.NUMBER_KEY, Phones.NUMBER_KEY);
4471        phonesColumns.put(Phones.TYPE, Phones.TYPE);
4472        phonesColumns.put(Phones.LABEL, Phones.LABEL);
4473
4474        // People projection map
4475        map = new HashMap<String, String>();
4476        map.put(People._ID, "people._id AS " + People._ID);
4477        peopleColumns.put(People.PRIMARY_PHONE_ID, People.PRIMARY_PHONE_ID);
4478        peopleColumns.put(People.PRIMARY_EMAIL_ID, People.PRIMARY_EMAIL_ID);
4479        peopleColumns.put(People.PRIMARY_ORGANIZATION_ID, People.PRIMARY_ORGANIZATION_ID);
4480        map.putAll(peopleColumns);
4481        map.putAll(phonesColumns);
4482        map.putAll(syncColumns);
4483        map.putAll(presenceColumns);
4484        sPeopleProjectionMap = map;
4485
4486        // People with photo projection map
4487        map = new HashMap<String, String>(sPeopleProjectionMap);
4488        map.put("photo_data", "photos.data AS photo_data");
4489        sPeopleWithPhotoProjectionMap = map;
4490
4491        // People with E-mail or IM projection map
4492        map = new HashMap<String, String>();
4493        map.put(People._ID, "people._id AS " + People._ID);
4494        map.put(ContactMethods.DATA, "contact_methods." + ContactMethods.DATA + " AS " + ContactMethods.DATA);
4495        map.put(ContactMethods.KIND, "contact_methods." + ContactMethods.KIND + " AS " + ContactMethods.KIND);
4496        map.putAll(peopleColumns);
4497        sPeopleWithEmailOrImProjectionMap = map;
4498
4499        // Groups projection map
4500        map = new HashMap<String, String>();
4501        map.put(Groups._ID, Groups._ID);
4502        map.putAll(groupsColumns);
4503        map.putAll(syncColumns);
4504        sGroupsProjectionMap = map;
4505
4506        // Group Membership projection map
4507        map = new HashMap<String, String>();
4508        map.put(GroupMembership._ID, "groupmembership._id AS " + GroupMembership._ID);
4509        map.put(GroupMembership.PERSON_ID, GroupMembership.PERSON_ID);
4510        map.put(GroupMembership.GROUP_ID, "groups._id AS " + GroupMembership.GROUP_ID);
4511        map.put(GroupMembership.GROUP_SYNC_ACCOUNT, GroupMembership.GROUP_SYNC_ACCOUNT);
4512        map.put(GroupMembership.GROUP_SYNC_ID, GroupMembership.GROUP_SYNC_ID);
4513        map.putAll(groupsColumns);
4514        sGroupMembershipProjectionMap = map;
4515
4516        // Use this when you need to force items to the top of a times_contacted list
4517        map = new HashMap<String, String>(sPeopleProjectionMap);
4518        map.put(People.TIMES_CONTACTED, Long.MAX_VALUE + " AS " + People.TIMES_CONTACTED);
4519        map.put("photo_data", "photos.data AS photo_data");
4520        sStrequentStarredProjectionMap = map;
4521
4522        // Calls projection map
4523        map = new HashMap<String, String>();
4524        map.put(Calls._ID, Calls._ID);
4525        map.put(Calls.NUMBER, Calls.NUMBER);
4526        map.put(Calls.DATE, Calls.DATE);
4527        map.put(Calls.DURATION, Calls.DURATION);
4528        map.put(Calls.TYPE, Calls.TYPE);
4529        map.put(Calls.NEW, Calls.NEW);
4530        map.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
4531        map.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
4532        map.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
4533        sCallsProjectionMap = map;
4534
4535        // Phones projection map
4536        map = new HashMap<String, String>();
4537        map.put(Phones._ID, "phones._id AS " + Phones._ID);
4538        map.putAll(phonesColumns);
4539        map.put(Phones.PERSON_ID, "phones.person AS " + Phones.PERSON_ID);
4540        map.put(Phones.ISPRIMARY, Phones.ISPRIMARY);
4541        map.putAll(peopleColumns);
4542        sPhonesProjectionMap = map;
4543
4544        // Phones with presence projection map
4545        map = new HashMap<String, String>(sPhonesProjectionMap);
4546        map.putAll(presenceColumns);
4547        sPhonesWithPresenceProjectionMap = map;
4548
4549        // Organizations projection map
4550        map = new HashMap<String, String>();
4551        map.put(Organizations._ID, "organizations._id AS " + Organizations._ID);
4552        map.put(Organizations.LABEL, Organizations.LABEL);
4553        map.put(Organizations.TYPE, Organizations.TYPE);
4554        map.put(Organizations.PERSON_ID, Organizations.PERSON_ID);
4555        map.put(Organizations.COMPANY, Organizations.COMPANY);
4556        map.put(Organizations.TITLE, Organizations.TITLE);
4557        map.put(Organizations.ISPRIMARY, Organizations.ISPRIMARY);
4558        sOrganizationsProjectionMap = map;
4559
4560        // Extensions projection map
4561        map = new HashMap<String, String>();
4562        map.put(Extensions._ID, Extensions._ID);
4563        map.put(Extensions.NAME, Extensions.NAME);
4564        map.put(Extensions.VALUE, Extensions.VALUE);
4565        map.put(Extensions.PERSON_ID, Extensions.PERSON_ID);
4566        sExtensionsProjectionMap = map;
4567
4568        // Contact methods projection map
4569        map = new HashMap<String, String>();
4570        map.put(ContactMethods._ID, "contact_methods._id AS " + ContactMethods._ID);
4571        map.put(ContactMethods.KIND, ContactMethods.KIND);
4572        map.put(ContactMethods.TYPE, ContactMethods.TYPE);
4573        map.put(ContactMethods.LABEL, ContactMethods.LABEL);
4574        map.put(ContactMethods.DATA, ContactMethods.DATA);
4575        map.put(ContactMethods.AUX_DATA, ContactMethods.AUX_DATA);
4576        map.put(ContactMethods.PERSON_ID, "contact_methods.person AS " + ContactMethods.PERSON_ID);
4577        map.put(ContactMethods.ISPRIMARY, ContactMethods.ISPRIMARY);
4578        map.putAll(peopleColumns);
4579        sContactMethodsProjectionMap = map;
4580
4581        // Contact methods with presence projection map
4582        map = new HashMap<String, String>(sContactMethodsProjectionMap);
4583        map.putAll(presenceColumns);
4584        sContactMethodsWithPresenceProjectionMap = map;
4585
4586        // Email search projection map
4587        map = new HashMap<String, String>();
4588        map.put(ContactMethods.NAME, ContactMethods.NAME);
4589        map.put(ContactMethods.DATA, ContactMethods.DATA);
4590        map.put(ContactMethods._ID, "contact_methods._id AS " + ContactMethods._ID);
4591        sEmailSearchProjectionMap = map;
4592
4593        // Presence projection map
4594        map = new HashMap<String, String>();
4595        map.put(Presence._ID, "presence._id AS " + Presence._ID);
4596        map.putAll(presenceColumns);
4597        map.putAll(peopleColumns);
4598        sPresenceProjectionMap = map;
4599
4600        // Photos projection map
4601        map = new HashMap<String, String>();
4602        map.put(Photos._ID, Photos._ID);
4603        map.put(Photos.LOCAL_VERSION, Photos.LOCAL_VERSION);
4604        map.put(Photos.EXISTS_ON_SERVER, Photos.EXISTS_ON_SERVER);
4605        map.put(Photos.SYNC_ERROR, Photos.SYNC_ERROR);
4606        map.put(Photos.PERSON_ID, Photos.PERSON_ID);
4607        map.put(Photos.DATA, Photos.DATA);
4608        map.put(Photos.DOWNLOAD_REQUIRED, ""
4609                + "(exists_on_server!=0 "
4610                + " AND sync_error IS NULL "
4611                + " AND (local_version IS NULL OR _sync_version != local_version)) "
4612                + "AS " + Photos.DOWNLOAD_REQUIRED);
4613        map.putAll(syncColumns);
4614        sPhotosProjectionMap = map;
4615
4616        // Live folder projection
4617        map = new HashMap<String, String>();
4618        map.put(LiveFolders._ID, "people._id AS " + LiveFolders._ID);
4619        map.put(LiveFolders.NAME, DISPLAY_NAME_SQL + " AS " + LiveFolders.NAME);
4620        // TODO: Put contact photo back when we have a way to display a default icon
4621        // for contacts without a photo
4622        // map.put(LiveFolders.ICON_BITMAP, Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
4623        sLiveFoldersProjectionMap = map;
4624
4625        // Order by statements
4626        sPhonesKeyOrderBy = buildOrderBy(sPhonesTable, Phones.NUMBER);
4627        sContactMethodsKeyOrderBy = buildOrderBy(sContactMethodsTable,
4628                ContactMethods.DATA, ContactMethods.KIND);
4629        sOrganizationsKeyOrderBy = buildOrderBy(sOrganizationsTable, Organizations.COMPANY);
4630        sGroupmembershipKeyOrderBy =
4631                buildOrderBy(sGroupmembershipTable, GroupMembership.GROUP_SYNC_ACCOUNT);
4632
4633        sPhonesKeyColumns = new String[]{Phones.NUMBER};
4634        sContactMethodsKeyColumns = new String[]{ContactMethods.DATA, ContactMethods.KIND};
4635        sOrganizationsKeyColumns = new String[]{Organizations.COMPANY};
4636        sGroupmembershipKeyColumns = new String[]{GroupMembership.GROUP_SYNC_ACCOUNT};
4637        sExtensionsKeyColumns = new String[]{Extensions.NAME};
4638
4639        String groupJoinByLocalId = "groups._id=groupmembership.group_id";
4640        String groupJoinByServerId = "("
4641                + "groups._sync_account=groupmembership.group_sync_account"
4642                + " AND "
4643                + "groups._sync_id=groupmembership.group_sync_id"
4644                + ")";
4645        sGroupsJoinString = "(" + groupJoinByLocalId + " OR " + groupJoinByServerId + ")";
4646    }
4647}
4648