ContactsProvider2.java revision e3eb7ef438010c893c429f3031dcc7298171865d
1/*
2 * Copyright (C) 2009 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 com.android.internal.content.SyncStateContentProviderHelper;
20import com.android.providers.contacts.OpenHelper.AggregationExceptionColumns;
21import com.android.providers.contacts.OpenHelper.Clauses;
22import com.android.providers.contacts.OpenHelper.ContactsColumns;
23import com.android.providers.contacts.OpenHelper.DataColumns;
24import com.android.providers.contacts.OpenHelper.GroupsColumns;
25import com.android.providers.contacts.OpenHelper.MimetypesColumns;
26import com.android.providers.contacts.OpenHelper.NameLookupColumns;
27import com.android.providers.contacts.OpenHelper.PackagesColumns;
28import com.android.providers.contacts.OpenHelper.PhoneColumns;
29import com.android.providers.contacts.OpenHelper.PhoneLookupColumns;
30import com.android.providers.contacts.OpenHelper.RawContactsColumns;
31import com.android.providers.contacts.OpenHelper.Tables;
32import com.google.android.collect.Lists;
33
34import android.accounts.Account;
35import android.accounts.AccountManager;
36import android.app.SearchManager;
37import android.content.ContentProviderOperation;
38import android.content.ContentProviderResult;
39import android.content.ContentUris;
40import android.content.ContentValues;
41import android.content.Context;
42import android.content.Entity;
43import android.content.EntityIterator;
44import android.content.OperationApplicationException;
45import android.content.SharedPreferences;
46import android.content.UriMatcher;
47import android.content.SharedPreferences.Editor;
48import android.database.Cursor;
49import android.database.DatabaseUtils;
50import android.database.sqlite.SQLiteCursor;
51import android.database.sqlite.SQLiteDatabase;
52import android.database.sqlite.SQLiteException;
53import android.database.sqlite.SQLiteQueryBuilder;
54import android.database.sqlite.SQLiteStatement;
55import android.net.Uri;
56import android.os.RemoteException;
57import android.preference.PreferenceManager;
58import android.provider.BaseColumns;
59import android.provider.ContactsContract;
60import android.provider.Contacts.People;
61import android.provider.ContactsContract.AggregationExceptions;
62import android.provider.ContactsContract.CommonDataKinds;
63import android.provider.ContactsContract.Contacts;
64import android.provider.ContactsContract.Data;
65import android.provider.ContactsContract.Groups;
66import android.provider.ContactsContract.PhoneLookup;
67import android.provider.ContactsContract.Presence;
68import android.provider.ContactsContract.RawContacts;
69import android.provider.ContactsContract.Settings;
70import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
71import android.provider.ContactsContract.CommonDataKinds.Email;
72import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
73import android.provider.ContactsContract.CommonDataKinds.Im;
74import android.provider.ContactsContract.CommonDataKinds.Nickname;
75import android.provider.ContactsContract.CommonDataKinds.Organization;
76import android.provider.ContactsContract.CommonDataKinds.Phone;
77import android.provider.ContactsContract.CommonDataKinds.StructuredName;
78import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
79import android.telephony.PhoneNumberUtils;
80import android.text.TextUtils;
81import android.util.Log;
82
83import java.util.ArrayList;
84import java.util.HashMap;
85import java.util.concurrent.CountDownLatch;
86
87/**
88 * Contacts content provider. The contract between this provider and applications
89 * is defined in {@link ContactsContract}.
90 */
91public class ContactsProvider2 extends SQLiteContentProvider {
92
93    // TODO: clean up debug tag and rename this class
94    private static final String TAG = "ContactsProvider ~~~~";
95
96    // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
97    // TODO: check for restricted flag during insert(), update(), and delete() calls
98
99    /** Default for the maximum number of returned aggregation suggestions. */
100    private static final int DEFAULT_MAX_SUGGESTIONS = 5;
101
102    /**
103     * Shared preference key for the legacy contact import version. The need for a version
104     * as opposed to a boolean flag is that if we discover bugs in the contact import process,
105     * we can trigger re-import by incrementing the import version.
106     */
107    private static final String PREF_CONTACTS_IMPORTED = "contacts_imported_v1";
108    private static final int PREF_CONTACTS_IMPORT_VERSION = 1;
109
110    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
111
112    private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
113            + Contacts.TIMES_CONTACTED + " DESC, "
114            + Contacts.DISPLAY_NAME + " ASC";
115    private static final String STREQUENT_LIMIT =
116            "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
117            + Contacts.STARRED + "=1) + 25";
118
119    private static final int CONTACTS = 1000;
120    private static final int CONTACTS_ID = 1001;
121    private static final int CONTACTS_DATA = 1002;
122    private static final int CONTACTS_SUMMARY = 1003;
123    private static final int CONTACTS_SUMMARY_ID = 1005;
124    private static final int CONTACTS_SUMMARY_FILTER = 1006;
125    private static final int CONTACTS_SUMMARY_STREQUENT = 1007;
126    private static final int CONTACTS_SUMMARY_STREQUENT_FILTER = 1008;
127    private static final int CONTACTS_SUMMARY_GROUP = 1009;
128
129    private static final int RAW_CONTACTS = 2002;
130    private static final int RAW_CONTACTS_ID = 2003;
131    private static final int RAW_CONTACTS_DATA = 2004;
132
133    private static final int DATA = 3000;
134    private static final int DATA_ID = 3001;
135    private static final int PHONES = 3002;
136    private static final int PHONES_FILTER = 3003;
137    private static final int EMAILS = 3004;
138    private static final int EMAILS_FILTER = 3005;
139    private static final int POSTALS = 3006;
140
141    private static final int PHONE_LOOKUP = 4000;
142
143    private static final int AGGREGATION_EXCEPTIONS = 6000;
144    private static final int AGGREGATION_EXCEPTION_ID = 6001;
145
146    private static final int PRESENCE = 7000;
147    private static final int PRESENCE_ID = 7001;
148
149    private static final int AGGREGATION_SUGGESTIONS = 8000;
150
151    private static final int SETTINGS = 9000;
152
153    private static final int GROUPS = 10000;
154    private static final int GROUPS_ID = 10001;
155    private static final int GROUPS_SUMMARY = 10003;
156
157    private static final int SYNCSTATE = 11000;
158
159    private static final int SEARCH_SUGGESTIONS = 12001;
160    private static final int SEARCH_SHORTCUT = 12002;
161
162    private interface ContactsQuery {
163        public static final String TABLE = Tables.RAW_CONTACTS;
164
165        public static final String[] PROJECTION = new String[] {
166            RawContactsColumns.CONCRETE_ID,
167            RawContacts.ACCOUNT_NAME,
168            RawContacts.ACCOUNT_TYPE,
169        };
170
171        public static final int RAW_CONTACT_ID = 0;
172        public static final int ACCOUNT_NAME = 1;
173        public static final int ACCOUNT_TYPE = 2;
174    }
175
176    private interface DataRawContactsQuery {
177        public static final String TABLE = Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS;
178
179        public static final String[] PROJECTION = new String[] {
180            RawContactsColumns.CONCRETE_ID,
181            DataColumns.CONCRETE_ID,
182            RawContacts.CONTACT_ID,
183            RawContacts.IS_RESTRICTED,
184            Data.MIMETYPE,
185        };
186
187        public static final int RAW_CONTACT_ID = 0;
188        public static final int DATA_ID = 1;
189        public static final int CONTACT_ID = 2;
190        public static final int IS_RESTRICTED = 3;
191        public static final int MIMETYPE = 4;
192    }
193
194    private interface DataContactsQuery {
195        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS;
196
197        public static final String[] PROJECTION = new String[] {
198            RawContactsColumns.CONCRETE_ID,
199            DataColumns.CONCRETE_ID,
200            ContactsColumns.CONCRETE_ID,
201            MimetypesColumns.CONCRETE_ID,
202        };
203
204        public static final int RAW_CONTACT_ID = 0;
205        public static final int DATA_ID = 1;
206        public static final int CONTACT_ID = 2;
207        public static final int MIMETYPE_ID = 3;
208    }
209
210    private interface DisplayNameQuery {
211        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
212
213        public static final String[] COLUMNS = new String[] {
214            MimetypesColumns.MIMETYPE,
215            Data.IS_PRIMARY,
216            Data.DATA2,
217            StructuredName.DISPLAY_NAME,
218        };
219
220        public static final int MIMETYPE = 0;
221        public static final int IS_PRIMARY = 1;
222        public static final int DATA2 = 2;
223        public static final int DISPLAY_NAME = 3;
224    }
225
226    private interface DataQuery {
227        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
228
229        public static final String[] CONCRETE_COLUMNS = new String[] {
230            DataColumns.CONCRETE_ID,
231            MimetypesColumns.MIMETYPE,
232            Data.RAW_CONTACT_ID,
233            Data.IS_PRIMARY,
234            Data.DATA2,
235        };
236
237        public static final String[] COLUMNS = new String[] {
238            Data._ID,
239            MimetypesColumns.MIMETYPE,
240            Data.RAW_CONTACT_ID,
241            Data.IS_PRIMARY,
242            Data.DATA2,
243        };
244
245        public static final int ID = 0;
246        public static final int MIMETYPE = 1;
247        public static final int RAW_CONTACT_ID = 2;
248        public static final int IS_PRIMARY = 3;
249        public static final int DATA2 = 4;
250    }
251
252    private interface DataIdQuery {
253        String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };
254
255        int _ID = 0;
256        int RAW_CONTACT_ID = 1;
257        int MIMETYPE = 2;
258    }
259
260    // Higher number represents higher priority in choosing what data to use for the display name
261    private static final int DISPLAY_NAME_PRIORITY_EMAIL = 1;
262    private static final int DISPLAY_NAME_PRIORITY_PHONE = 2;
263    private static final int DISPLAY_NAME_PRIORITY_ORGANIZATION = 3;
264    private static final int DISPLAY_NAME_PRIORITY_STRUCTURED_NAME = 4;
265
266    private static final HashMap<String, Integer> sDisplayNamePriorities;
267    static {
268        sDisplayNamePriorities = new HashMap<String, Integer>();
269        sDisplayNamePriorities.put(StructuredName.CONTENT_ITEM_TYPE,
270                DISPLAY_NAME_PRIORITY_STRUCTURED_NAME);
271        sDisplayNamePriorities.put(Organization.CONTENT_ITEM_TYPE,
272                DISPLAY_NAME_PRIORITY_ORGANIZATION);
273        sDisplayNamePriorities.put(Phone.CONTENT_ITEM_TYPE,
274                DISPLAY_NAME_PRIORITY_PHONE);
275        sDisplayNamePriorities.put(Email.CONTENT_ITEM_TYPE,
276                DISPLAY_NAME_PRIORITY_EMAIL);
277    }
278
279    public static final String DEFAULT_ACCOUNT_TYPE = "com.google.GAIA";
280    public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google";
281
282    /** Contains just the contacts columns */
283    private static final HashMap<String, String> sContactsProjectionMap;
284    /** Contains the contact columns along with primary phone */
285    private static final HashMap<String, String> sContactsSummaryProjectionMap;
286    /** Contains just the contacts columns */
287    private static final HashMap<String, String> sRawContactsProjectionMap;
288    /** Contains columns from the data view */
289    private static final HashMap<String, String> sDataProjectionMap;
290    /** Contains the data and contacts columns, for joined tables */
291    private static final HashMap<String, String> sPhoneLookupProjectionMap;
292    /** Contains the just the {@link Groups} columns */
293    private static final HashMap<String, String> sGroupsProjectionMap;
294    /** Contains {@link Groups} columns along with summary details */
295    private static final HashMap<String, String> sGroupsSummaryProjectionMap;
296    /** Contains the agg_exceptions columns */
297    private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
298    /** Contains the agg_exceptions columns */
299    private static final HashMap<String, String> sSettingsProjectionMap;
300    /** Contains Presence columns */
301    private static final HashMap<String, String> sPresenceProjectionMap;
302
303    /** Sql select statement that returns the contact id associated with a data record. */
304    private static final String sNestedRawContactIdSelect;
305    /** Sql select statement that returns the mimetype id associated with a data record. */
306    private static final String sNestedMimetypeSelect;
307    /** Sql select statement that returns the contact id associated with a contact record. */
308    private static final String sNestedContactIdSelect;
309    /** Sql select statement that returns a list of contact ids associated with an contact record. */
310    private static final String sNestedContactIdListSelect;
311    /** Sql where statement used to match all the data records that need to be updated when a new
312     * "primary" is selected.*/
313    private static final String sSetPrimaryWhere;
314    /** Sql where statement used to match all the data records that need to be updated when a new
315     * "super primary" is selected.*/
316    private static final String sSetSuperPrimaryWhere;
317    /** Sql where statement for filtering on groups. */
318    private static final String sContactsInGroupSelect;
319    /** Precompiled sql statement for setting a data record to the primary. */
320    private SQLiteStatement mSetPrimaryStatement;
321    /** Precompiled sql statement for setting a data record to the super primary. */
322    private SQLiteStatement mSetSuperPrimaryStatement;
323    /** Precompiled sql statement for incrementing times contacted for an contact */
324    private SQLiteStatement mLastTimeContactedUpdate;
325    /** Precompiled sql statement for updating a contact display name */
326    private SQLiteStatement mContactDisplayNameUpdate;
327    /** Precompiled sql statement for marking a raw contact as dirty */
328    private SQLiteStatement mRawContactDirtyUpdate;
329
330    static {
331        // Contacts URI matching table
332        final UriMatcher matcher = sUriMatcher;
333        matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
334        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
335        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
336        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary", CONTACTS_SUMMARY);
337        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/#", CONTACTS_SUMMARY_ID);
338        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/filter/*",
339                CONTACTS_SUMMARY_FILTER);
340        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/strequent/",
341                CONTACTS_SUMMARY_STREQUENT);
342        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/strequent/filter/*",
343                CONTACTS_SUMMARY_STREQUENT_FILTER);
344        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/group/*",
345                CONTACTS_SUMMARY_GROUP);
346        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
347                AGGREGATION_SUGGESTIONS);
348        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
349        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
350        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
351
352        matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
353        matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
354        matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
355        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
356        matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
357        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
358        matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
359
360        matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
361        matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
362        matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
363
364        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
365
366        matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
367        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
368                AGGREGATION_EXCEPTIONS);
369        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
370                AGGREGATION_EXCEPTION_ID);
371
372        matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);
373
374        matcher.addURI(ContactsContract.AUTHORITY, "presence", PRESENCE);
375        matcher.addURI(ContactsContract.AUTHORITY, "presence/#", PRESENCE_ID);
376
377        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
378                SEARCH_SUGGESTIONS);
379        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
380                SEARCH_SUGGESTIONS);
381        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
382                SEARCH_SHORTCUT);
383
384        sContactsProjectionMap = new HashMap<String, String>();
385        sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
386        sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
387        sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
388        sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
389        sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
390        sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
391        sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
392        sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
393        sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER);
394        sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
395
396        sContactsSummaryProjectionMap = new HashMap<String, String>();
397        sContactsSummaryProjectionMap.putAll(sContactsProjectionMap);
398        sContactsSummaryProjectionMap.put(Contacts.PRESENCE_STATUS,
399                "MAX(" + Presence.PRESENCE_STATUS + ") AS " + Contacts.PRESENCE_STATUS);
400
401        sRawContactsProjectionMap = new HashMap<String, String>();
402        sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
403        sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
404        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
405        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
406        sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
407        sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
408        sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
409        sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
410        sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
411        sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
412                RawContacts.LAST_TIME_CONTACTED);
413        sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE);
414        sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL);
415        sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED);
416        sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE);
417        sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1);
418        sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2);
419        sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3);
420        sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4);
421
422        sDataProjectionMap = new HashMap<String, String>();
423        sDataProjectionMap.put(Data._ID, Data._ID);
424        sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
425        sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
426        sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
427        sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
428        sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
429        sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
430        sDataProjectionMap.put(Data.DATA1, Data.DATA1);
431        sDataProjectionMap.put(Data.DATA2, Data.DATA2);
432        sDataProjectionMap.put(Data.DATA3, Data.DATA3);
433        sDataProjectionMap.put(Data.DATA4, Data.DATA4);
434        sDataProjectionMap.put(Data.DATA5, Data.DATA5);
435        sDataProjectionMap.put(Data.DATA6, Data.DATA6);
436        sDataProjectionMap.put(Data.DATA7, Data.DATA7);
437        sDataProjectionMap.put(Data.DATA8, Data.DATA8);
438        sDataProjectionMap.put(Data.DATA9, Data.DATA9);
439        sDataProjectionMap.put(Data.DATA10, Data.DATA10);
440        sDataProjectionMap.put(Data.DATA11, Data.DATA11);
441        sDataProjectionMap.put(Data.DATA12, Data.DATA12);
442        sDataProjectionMap.put(Data.DATA13, Data.DATA13);
443        sDataProjectionMap.put(Data.DATA14, Data.DATA14);
444        sDataProjectionMap.put(Data.DATA15, Data.DATA15);
445        sDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
446        sDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
447        sDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
448        sDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
449        sDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
450        sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
451        sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
452        sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
453        sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
454        sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
455        sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
456        sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
457        sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
458        sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
459        sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
460        sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
461        sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
462        sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
463
464        sPhoneLookupProjectionMap = new HashMap<String, String>();
465        sPhoneLookupProjectionMap.put(PhoneLookup._ID,
466                ContactsColumns.CONCRETE_ID + " AS " + PhoneLookup._ID);
467        sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
468                ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME);
469        sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
470                ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
471                        + " AS " + PhoneLookup.LAST_TIME_CONTACTED);
472        sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
473                ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED);
474        sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
475                ContactsColumns.CONCRETE_STARRED + " AS " + PhoneLookup.STARRED);
476        sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
477                Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
478        sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
479                Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID);
480        sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
481                ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE);
482        sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
483                Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
484        sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
485                ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
486                        + " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
487        sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
488                Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
489        sPhoneLookupProjectionMap.put(PhoneLookup.TYPE,
490                Phone.TYPE + " AS " + PhoneLookup.TYPE);
491        sPhoneLookupProjectionMap.put(PhoneLookup.LABEL,
492                Phone.LABEL + " AS " + PhoneLookup.LABEL);
493
494        HashMap<String, String> columns;
495
496        // Groups projection map
497        columns = new HashMap<String, String>();
498        columns.put(Groups._ID, "groups._id AS _id");
499        columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
500        columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
501        columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
502        columns.put(Groups.DIRTY, Groups.DIRTY);
503        columns.put(Groups.VERSION, Groups.VERSION);
504        columns.put(Groups.RES_PACKAGE, PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE);
505        columns.put(Groups.TITLE, Groups.TITLE);
506        columns.put(Groups.TITLE_RES, Groups.TITLE_RES);
507        columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
508        columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID);
509        columns.put(Groups.DELETED, Groups.DELETED);
510        columns.put(Groups.NOTES, Groups.NOTES);
511        columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC);
512        columns.put(Groups.SYNC1, Tables.GROUPS + "." + Groups.SYNC1 + " AS " + Groups.SYNC1);
513        columns.put(Groups.SYNC2, Tables.GROUPS + "." + Groups.SYNC2 + " AS " + Groups.SYNC2);
514        columns.put(Groups.SYNC3, Tables.GROUPS + "." + Groups.SYNC3 + " AS " + Groups.SYNC3);
515        columns.put(Groups.SYNC4, Tables.GROUPS + "." + Groups.SYNC4 + " AS " + Groups.SYNC4);
516        sGroupsProjectionMap = columns;
517
518        // RawContacts and groups projection map
519        columns = new HashMap<String, String>();
520        columns.putAll(sGroupsProjectionMap);
521
522        columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
523                + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
524                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
525                + ") AS " + Groups.SUMMARY_COUNT);
526
527        columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
528                + ContactsColumns.CONCRETE_ID + ") FROM "
529                + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
530                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
531                + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES);
532
533        sGroupsSummaryProjectionMap = columns;
534
535        // Aggregate exception projection map
536        columns = new HashMap<String, String>();
537        columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
538        columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
539        columns.put(AggregationExceptions.CONTACT_ID,
540                "raw_contacts1." + RawContacts.CONTACT_ID
541                + " AS " + AggregationExceptions.CONTACT_ID);
542        columns.put(AggregationExceptions.RAW_CONTACT_ID, AggregationExceptionColumns.RAW_CONTACT_ID2);
543        sAggregationExceptionsProjectionMap = columns;
544
545        // Settings projection map
546        columns = new HashMap<String, String>();
547        columns.put(Settings._ID, Settings._ID);
548        columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME);
549        columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE);
550        columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE);
551        columns.put(Settings.SHOULD_SYNC_MODE, Settings.SHOULD_SYNC_MODE);
552        columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC);
553        sSettingsProjectionMap = columns;
554
555
556        columns = new HashMap<String, String>();
557        columns.put(Presence._ID, Presence._ID);
558        columns.put(Presence.RAW_CONTACT_ID, Presence.RAW_CONTACT_ID);
559        columns.put(Presence.DATA_ID, Presence.DATA_ID);
560        columns.put(Presence.IM_ACCOUNT, Presence.IM_ACCOUNT);
561        columns.put(Presence.IM_HANDLE, Presence.IM_HANDLE);
562        columns.put(Presence.IM_PROTOCOL, Presence.IM_PROTOCOL);
563        columns.put(Presence.PRESENCE_STATUS, Presence.PRESENCE_STATUS);
564        columns.put(Presence.PRESENCE_CUSTOM_STATUS, Presence.PRESENCE_CUSTOM_STATUS);
565        sPresenceProjectionMap = columns;
566
567        sNestedRawContactIdSelect = "SELECT " + Data.RAW_CONTACT_ID + " FROM " + Tables.DATA + " WHERE "
568                + Data._ID + "=?";
569        sNestedMimetypeSelect = "SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA
570                + " WHERE " + Data._ID + "=?";
571        sNestedContactIdSelect = "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS
572                + " WHERE " + RawContacts._ID + "=(" + sNestedRawContactIdSelect + ")";
573        sNestedContactIdListSelect = "SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS
574                + " WHERE " + RawContacts.CONTACT_ID + "=(" + sNestedContactIdSelect + ")";
575        sSetPrimaryWhere = Data.RAW_CONTACT_ID + "=(" + sNestedRawContactIdSelect + ") AND "
576                + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
577        sSetSuperPrimaryWhere = Data.RAW_CONTACT_ID + " IN (" + sNestedContactIdListSelect + ") AND "
578                + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
579        sContactsInGroupSelect = Contacts._ID + " IN "
580                + "(SELECT " + RawContacts.CONTACT_ID
581                + " FROM " + Tables.RAW_CONTACTS
582                + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
583                        + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
584                        + " FROM " + Tables.DATA_JOIN_MIMETYPES
585                        + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
586                                + "' AND " + GroupMembership.GROUP_ROW_ID + "="
587                                + "(SELECT " + Tables.GROUPS + "." + Groups._ID
588                                + " FROM " + Tables.GROUPS
589                                + " WHERE " + Groups.TITLE + "=?)))";
590    }
591
592    /**
593     * Handles inserts and update for a specific Data type.
594     */
595    private abstract class DataRowHandler {
596
597        protected final String mMimetype;
598
599        public DataRowHandler(String mimetype) {
600            mMimetype = mimetype;
601        }
602
603        /**
604         * Inserts a row into the {@link Data} table.
605         */
606        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
607            final long dataId = db.insert(Tables.DATA, null, values);
608
609            Integer primary = values.getAsInteger(Data.IS_PRIMARY);
610            if (primary != null && primary != 0) {
611                setIsPrimary(dataId);
612            }
613
614            fixContactDisplayName(db, rawContactId);
615            return dataId;
616        }
617
618        /**
619         * Validates data and updates a {@link Data} row using the cursor, which contains
620         * the current data.
621         */
622        public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
623            throw new UnsupportedOperationException();
624        }
625
626        public int delete(SQLiteDatabase db, Cursor c) {
627            long dataId = c.getLong(DataQuery.ID);
628            long rawContactId = c.getLong(DataQuery.RAW_CONTACT_ID);
629            boolean primary = c.getInt(DataQuery.IS_PRIMARY) != 0;
630            int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
631            if (count != 0 && primary) {
632                fixPrimary(db, rawContactId);
633                fixContactDisplayName(db, rawContactId);
634            }
635            return count;
636        }
637
638        private void fixPrimary(SQLiteDatabase db, long rawContactId) {
639            long newPrimaryId = findNewPrimaryDataId(db, rawContactId);
640            if (newPrimaryId != -1) {
641                ContactsProvider2.this.setIsPrimary(newPrimaryId);
642            }
643        }
644
645        protected long findNewPrimaryDataId(SQLiteDatabase db, long rawContactId) {
646            long primaryId = -1;
647            int primaryType = -1;
648            Cursor c = queryData(db, rawContactId);
649            try {
650                while (c.moveToNext()) {
651                    long dataId = c.getLong(DataQuery.ID);
652                    int type = c.getInt(DataQuery.DATA2);
653                    if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
654                        primaryId = dataId;
655                        primaryType = type;
656                    }
657                }
658            } finally {
659                c.close();
660            }
661            return primaryId;
662        }
663
664        /**
665         * Returns the rank of a specific record type to be used in determining the primary
666         * row. Lower number represents higher priority.
667         */
668        protected int getTypeRank(int type) {
669            return 0;
670        }
671
672        protected Cursor queryData(SQLiteDatabase db, long rawContactId) {
673            return db.query(DataQuery.TABLE, DataQuery.CONCRETE_COLUMNS, Data.RAW_CONTACT_ID + "="
674                    + rawContactId + " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'",
675                    null, null, null, null);
676        }
677
678        protected void fixContactDisplayName(SQLiteDatabase db, long rawContactId) {
679            if (!sDisplayNamePriorities.containsKey(mMimetype)) {
680                return;
681            }
682
683            String bestDisplayName = null;
684            Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS,
685                    Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null);
686            try {
687                int maxPriority = -1;
688                while (c.moveToNext()) {
689                    String mimeType = c.getString(DisplayNameQuery.MIMETYPE);
690                    boolean primary;
691                    String name;
692
693                    if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
694                        name = c.getString(DisplayNameQuery.DISPLAY_NAME);
695                        primary = true;
696                    } else {
697                        name = c.getString(DisplayNameQuery.DATA2);
698                        primary = (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0);
699                    }
700
701                    if (primary && name != null) {
702                        Integer priority = sDisplayNamePriorities.get(mimeType);
703                        if (priority != null && priority > maxPriority) {
704                            maxPriority = priority;
705                            bestDisplayName = name;
706                        }
707                    }
708                }
709
710            } finally {
711                c.close();
712            }
713
714            ContactsProvider2.this.setDisplayName(rawContactId, bestDisplayName);
715        }
716    }
717
718    public class CustomDataRowHandler extends DataRowHandler {
719
720        public CustomDataRowHandler(String mimetype) {
721            super(mimetype);
722        }
723    }
724
725    public class StructuredNameRowHandler extends DataRowHandler {
726
727        private final NameSplitter mNameSplitter;
728
729        public StructuredNameRowHandler(NameSplitter nameSplitter) {
730            super(StructuredName.CONTENT_ITEM_TYPE);
731            mNameSplitter = nameSplitter;
732        }
733
734        @Override
735        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
736            fixStructuredNameComponents(values);
737            return super.insert(db, rawContactId, values);
738        }
739
740        @Override
741        public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
742            // TODO Parse the full name if it has changed and replace pre-existing piece parts.
743        }
744
745        /**
746         * Parses the supplied display name, but only if the incoming values do not already contain
747         * structured name parts.  Also, if the display name is not provided, generate one by
748         * concatenating first name and last name
749         *
750         * TODO see if the order of first and last names needs to be conditionally reversed for
751         * some locales, e.g. China.
752         */
753        private void fixStructuredNameComponents(ContentValues values) {
754            String fullName = values.getAsString(StructuredName.DISPLAY_NAME);
755            if (!TextUtils.isEmpty(fullName)
756                    && TextUtils.isEmpty(values.getAsString(StructuredName.PREFIX))
757                    && TextUtils.isEmpty(values.getAsString(StructuredName.GIVEN_NAME))
758                    && TextUtils.isEmpty(values.getAsString(StructuredName.MIDDLE_NAME))
759                    && TextUtils.isEmpty(values.getAsString(StructuredName.FAMILY_NAME))
760                    && TextUtils.isEmpty(values.getAsString(StructuredName.SUFFIX))) {
761                NameSplitter.Name name = new NameSplitter.Name();
762                mNameSplitter.split(name, fullName);
763
764                values.put(StructuredName.PREFIX, name.getPrefix());
765                values.put(StructuredName.GIVEN_NAME, name.getGivenNames());
766                values.put(StructuredName.MIDDLE_NAME, name.getMiddleName());
767                values.put(StructuredName.FAMILY_NAME, name.getFamilyName());
768                values.put(StructuredName.SUFFIX, name.getSuffix());
769            }
770
771            if (TextUtils.isEmpty(fullName)) {
772                String givenName = values.getAsString(StructuredName.GIVEN_NAME);
773                String familyName = values.getAsString(StructuredName.FAMILY_NAME);
774                if (TextUtils.isEmpty(givenName)) {
775                    fullName = familyName;
776                } else if (TextUtils.isEmpty(familyName)) {
777                    fullName = givenName;
778                } else {
779                    fullName = givenName + " " + familyName;
780                }
781
782                if (!TextUtils.isEmpty(fullName)) {
783                    values.put(StructuredName.DISPLAY_NAME, fullName);
784                }
785            }
786        }
787    }
788
789    public class CommonDataRowHandler extends DataRowHandler {
790
791        private final String mTypeColumn;
792        private final String mLabelColumn;
793
794        public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) {
795            super(mimetype);
796            mTypeColumn = typeColumn;
797            mLabelColumn = labelColumn;
798        }
799
800        @Override
801        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
802            int type;
803            String label;
804            if (values.containsKey(mTypeColumn)) {
805                type = values.getAsInteger(mTypeColumn);
806            } else {
807                type = BaseTypes.TYPE_CUSTOM;
808            }
809            if (values.containsKey(mLabelColumn)) {
810                label = values.getAsString(mLabelColumn);
811            } else {
812                label = null;
813            }
814
815            if (type != BaseTypes.TYPE_CUSTOM && label != null) {
816                throw new IllegalArgumentException(mLabelColumn + " value can only be specified with "
817                        + mTypeColumn + "=" + BaseTypes.TYPE_CUSTOM + "(custom)");
818            }
819
820            if (type == BaseTypes.TYPE_CUSTOM && label == null) {
821                throw new IllegalArgumentException(mLabelColumn + " value must be specified when "
822                        + mTypeColumn + "=" + BaseTypes.TYPE_CUSTOM + "(custom)");
823            }
824
825            return super.insert(db, rawContactId, values);
826        }
827
828        @Override
829        public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
830            // TODO read the data and check the constraint
831        }
832    }
833
834    public class OrganizationDataRowHandler extends CommonDataRowHandler {
835
836        public OrganizationDataRowHandler() {
837            super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
838        }
839
840        @Override
841        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
842            long id = super.insert(db, rawContactId, values);
843            fixContactDisplayName(db, rawContactId);
844            return id;
845        }
846
847        @Override
848        protected int getTypeRank(int type) {
849            switch (type) {
850                case Organization.TYPE_WORK: return 0;
851                case Organization.TYPE_CUSTOM: return 1;
852                case Organization.TYPE_OTHER: return 2;
853                default: return 1000;
854            }
855        }
856    }
857
858    public class EmailDataRowHandler extends CommonDataRowHandler {
859
860        public EmailDataRowHandler() {
861            super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
862        }
863
864        @Override
865        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
866            long id = super.insert(db, rawContactId, values);
867            fixContactDisplayName(db, rawContactId);
868            return id;
869        }
870
871        @Override
872        protected int getTypeRank(int type) {
873            switch (type) {
874                case Email.TYPE_HOME: return 0;
875                case Email.TYPE_WORK: return 1;
876                case Email.TYPE_CUSTOM: return 2;
877                case Email.TYPE_OTHER: return 3;
878                default: return 1000;
879            }
880        }
881    }
882
883    public class PhoneDataRowHandler extends CommonDataRowHandler {
884
885        public PhoneDataRowHandler() {
886            super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
887        }
888
889        @Override
890        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
891            ContentValues phoneValues = new ContentValues();
892            String number = values.getAsString(Phone.NUMBER);
893            String normalizedNumber = null;
894            if (number != null) {
895                normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
896                values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
897            }
898
899            long id = super.insert(db, rawContactId, values);
900
901            if (number != null) {
902                phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
903                phoneValues.put(PhoneLookupColumns.DATA_ID, id);
904                phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
905                db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
906            }
907
908            return id;
909        }
910
911        @Override
912        protected int getTypeRank(int type) {
913            switch (type) {
914                case Phone.TYPE_MOBILE: return 0;
915                case Phone.TYPE_WORK: return 1;
916                case Phone.TYPE_HOME: return 2;
917                case Phone.TYPE_PAGER: return 3;
918                case Phone.TYPE_CUSTOM: return 4;
919                case Phone.TYPE_OTHER: return 5;
920                case Phone.TYPE_FAX_WORK: return 6;
921                case Phone.TYPE_FAX_HOME: return 7;
922                default: return 1000;
923            }
924        }
925    }
926
927    private HashMap<String, DataRowHandler> mDataRowHandlers;
928    private final ContactAggregationScheduler mAggregationScheduler;
929    private OpenHelper mOpenHelper;
930
931    private ContactAggregator mContactAggregator;
932    private NameSplitter mNameSplitter;
933    private LegacyApiSupport mLegacyApiSupport;
934    private GlobalSearchSupport mGlobalSearchSupport;
935
936    private ContentValues mValues = new ContentValues();
937
938    private volatile CountDownLatch mAccessLatch;
939    private boolean mImportMode;
940
941    private boolean mScheduleAggregation;
942
943    public ContactsProvider2() {
944        this(new ContactAggregationScheduler());
945    }
946
947    /**
948     * Constructor for testing.
949     */
950    /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
951        mAggregationScheduler = scheduler;
952    }
953
954    @Override
955    public boolean onCreate() {
956        super.onCreate();
957
958        final Context context = getContext();
959        mOpenHelper = (OpenHelper)getOpenHelper();
960        mGlobalSearchSupport = new GlobalSearchSupport(this);
961        mLegacyApiSupport = new LegacyApiSupport(context, mOpenHelper, this, mGlobalSearchSupport);
962        mContactAggregator = new ContactAggregator(context, mOpenHelper, mAggregationScheduler);
963
964        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
965        mSetPrimaryStatement = db.compileStatement(
966                "UPDATE " + Tables.DATA + " SET " + Data.IS_PRIMARY
967                + "=(_id=?) WHERE " + sSetPrimaryWhere);
968        mSetSuperPrimaryStatement = db.compileStatement(
969                "UPDATE " + Tables.DATA + " SET " + Data.IS_SUPER_PRIMARY
970                + "=(_id=?) WHERE " + sSetSuperPrimaryWhere);
971        mLastTimeContactedUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
972                + RawContacts.TIMES_CONTACTED + "=" + RawContacts.TIMES_CONTACTED + "+1,"
973                + RawContacts.LAST_TIME_CONTACTED + "=? WHERE " + RawContacts.CONTACT_ID + "=?");
974
975        mContactDisplayNameUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
976                + RawContactsColumns.DISPLAY_NAME + "=? WHERE " + RawContacts._ID + "=?");
977
978        mRawContactDirtyUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
979                + RawContacts.DIRTY + "=1 WHERE " + RawContacts._ID + "=?");
980
981        mNameSplitter = new NameSplitter(
982                context.getString(com.android.internal.R.string.common_name_prefixes),
983                context.getString(com.android.internal.R.string.common_last_name_prefixes),
984                context.getString(com.android.internal.R.string.common_name_suffixes),
985                context.getString(com.android.internal.R.string.common_name_conjunctions));
986
987        mDataRowHandlers = new HashMap<String, DataRowHandler>();
988
989        mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
990        mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
991                new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
992        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
993                StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
994        mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
995        mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
996        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
997                Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL));
998        mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
999                new StructuredNameRowHandler(mNameSplitter));
1000
1001        if (isLegacyContactImportNeeded()) {
1002            importLegacyContactsAsync();
1003        }
1004
1005        return (db != null);
1006    }
1007
1008    /* Visible for testing */
1009    @Override
1010    protected OpenHelper getOpenHelper(final Context context) {
1011        return OpenHelper.getInstance(context);
1012    }
1013
1014    /* package */ NameSplitter getNameSplitter() {
1015        return mNameSplitter;
1016    }
1017
1018    protected boolean isLegacyContactImportNeeded() {
1019        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1020        return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION;
1021    }
1022
1023    protected LegacyContactImporter getLegacyContactImporter() {
1024        return new LegacyContactImporter(getContext(), this);
1025    }
1026
1027    /**
1028     * Imports legacy contacts in a separate thread.  As long as the import process is running
1029     * all other access to the contacts is blocked.
1030     */
1031    private void importLegacyContactsAsync() {
1032        mAccessLatch = new CountDownLatch(1);
1033
1034        Thread importThread = new Thread("LegacyContactImport") {
1035            @Override
1036            public void run() {
1037                if (importLegacyContacts()) {
1038
1039                    /*
1040                     * When the import process is done, we can unlock the provider and
1041                     * start aggregating the imported contacts asynchronously.
1042                     */
1043                    mAccessLatch.countDown();
1044                    mAccessLatch = null;
1045                    scheduleContactAggregation();
1046                }
1047            }
1048        };
1049
1050        importThread.start();
1051    }
1052
1053    private boolean importLegacyContacts() {
1054        LegacyContactImporter importer = getLegacyContactImporter();
1055        if (importLegacyContacts(importer)) {
1056            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1057            Editor editor = prefs.edit();
1058            editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION);
1059            editor.commit();
1060            return true;
1061        } else {
1062            return false;
1063        }
1064    }
1065
1066    /* Visible for testing */
1067    /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
1068        mContactAggregator.setEnabled(false);
1069        mImportMode = true;
1070        try {
1071            importer.importContacts();
1072            mContactAggregator.setEnabled(true);
1073            return true;
1074        } catch (Throwable e) {
1075           Log.e(TAG, "Legacy contact import failed", e);
1076           return false;
1077        } finally {
1078            mImportMode = false;
1079        }
1080    }
1081
1082    @Override
1083    protected void finalize() throws Throwable {
1084        if (mContactAggregator != null) {
1085            mContactAggregator.quit();
1086        }
1087
1088        super.finalize();
1089    }
1090
1091    /**
1092     * Wipes all data from the contacts database.
1093     */
1094    /* package */ void wipeData() {
1095        mOpenHelper.wipeData();
1096    }
1097
1098    /**
1099     * While importing and aggregating contacts, this content provider will
1100     * block all attempts to change contacts data. In particular, it will hold
1101     * up all contact syncs. As soon as the import process is complete, all
1102     * processes waiting to write to the provider are unblocked and can proceed
1103     * to compete for the database transaction monitor.
1104     */
1105    private void waitForAccess() {
1106        CountDownLatch latch = mAccessLatch;
1107        if (latch != null) {
1108            while (true) {
1109                try {
1110                    latch.await();
1111                    mAccessLatch = null;
1112                    return;
1113                } catch (InterruptedException e) {
1114                }
1115            }
1116        }
1117    }
1118
1119    @Override
1120    public Uri insert(Uri uri, ContentValues values) {
1121        waitForAccess();
1122        return super.insert(uri, values);
1123    }
1124
1125    @Override
1126    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1127        waitForAccess();
1128        return super.update(uri, values, selection, selectionArgs);
1129    }
1130
1131    @Override
1132    public int delete(Uri uri, String selection, String[] selectionArgs) {
1133        waitForAccess();
1134        return super.delete(uri, selection, selectionArgs);
1135    }
1136
1137    @Override
1138    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
1139            throws OperationApplicationException {
1140        waitForAccess();
1141        return super.applyBatch(operations);
1142    }
1143
1144    @Override
1145    protected void onTransactionComplete() {
1146        if (mScheduleAggregation) {
1147            mScheduleAggregation = false;
1148            scheduleContactAggregation();
1149        }
1150        super.onTransactionComplete();
1151    }
1152
1153
1154    protected void scheduleContactAggregation() {
1155        mContactAggregator.schedule();
1156    }
1157
1158    private DataRowHandler getDataRowHandler(final String mimeType) {
1159        DataRowHandler handler = mDataRowHandlers.get(mimeType);
1160        if (handler == null) {
1161            handler = new CustomDataRowHandler(mimeType);
1162            mDataRowHandlers.put(mimeType, handler);
1163        }
1164        return handler;
1165    }
1166
1167    @Override
1168    protected Uri insertInTransaction(Uri uri, ContentValues values) {
1169        final int match = sUriMatcher.match(uri);
1170        long id = 0;
1171
1172        switch (match) {
1173            case SYNCSTATE:
1174                id = mOpenHelper.getSyncState().insert(mDb, values);
1175                break;
1176
1177            case CONTACTS: {
1178                insertContact(values);
1179                break;
1180            }
1181
1182            case RAW_CONTACTS: {
1183                final Account account = readAccountFromQueryParams(uri);
1184                id = insertRawContact(values, account);
1185                break;
1186            }
1187
1188            case RAW_CONTACTS_DATA: {
1189                values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
1190                id = insertData(values, shouldMarkRawContactAsDirty(uri));
1191                break;
1192            }
1193
1194            case DATA: {
1195                id = insertData(values, shouldMarkRawContactAsDirty(uri));
1196                break;
1197            }
1198
1199            case GROUPS: {
1200                final Account account = readAccountFromQueryParams(uri);
1201                id = insertGroup(values, account, shouldMarkGroupAsDirty(uri));
1202                break;
1203            }
1204
1205            case SETTINGS: {
1206                id = mDb.insert(Tables.SETTINGS, null, values);
1207                break;
1208            }
1209
1210            case PRESENCE: {
1211                id = insertPresence(values);
1212                break;
1213            }
1214
1215            default:
1216                return mLegacyApiSupport.insert(uri, values);
1217        }
1218
1219        if (id < 0) {
1220            return null;
1221        }
1222
1223        return ContentUris.withAppendedId(uri, id);
1224    }
1225
1226    /**
1227     * If account is non-null then store it in the values. If the account is already
1228     * specified in the values then it must be consistent with the account, if it is non-null.
1229     * @param values the ContentValues to read from and update
1230     * @param account the explicitly provided Account
1231     * @return false if the accounts are inconsistent
1232     */
1233    private boolean resolveAccount(ContentValues values, Account account) {
1234        // If either is specified then both must be specified.
1235        final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
1236        final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
1237        if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
1238            final Account valuesAccount = new Account(accountName, accountType);
1239            if (account != null && !valuesAccount.equals(account)) {
1240                return false;
1241            }
1242            account = valuesAccount;
1243        }
1244        if (account != null) {
1245            values.put(RawContacts.ACCOUNT_NAME, account.name);
1246            values.put(RawContacts.ACCOUNT_TYPE, account.type);
1247        }
1248        return true;
1249    }
1250
1251    /**
1252     * Inserts an item in the contacts table
1253     *
1254     * @param values the values for the new row
1255     * @return the row ID of the newly created row
1256     */
1257    private long insertContact(ContentValues values) {
1258        throw new UnsupportedOperationException("Aggregate contacts are created automatically");
1259    }
1260
1261    /**
1262     * Inserts an item in the contacts table
1263     *
1264     * @param values the values for the new row
1265     * @param account the account this contact should be associated with. may be null.
1266     * @return the row ID of the newly created row
1267     */
1268    private long insertRawContact(ContentValues values, Account account) {
1269        /*
1270         * The contact record is inserted in the contacts table, but it needs to
1271         * be processed by the aggregator before it will be returned by the
1272         * "aggregates" queries.
1273         */
1274        ContentValues overriddenValues = new ContentValues(values);
1275        overriddenValues.putNull(RawContacts.CONTACT_ID);
1276        if (!resolveAccount(overriddenValues, account)) {
1277            return -1;
1278        }
1279
1280        if (values.containsKey(RawContacts.DELETED)
1281                && values.getAsInteger(RawContacts.DELETED) != 0) {
1282            overriddenValues.put(RawContacts.AGGREGATION_MODE,
1283                    RawContacts.AGGREGATION_MODE_DISABLED);
1284        }
1285
1286        return mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
1287    }
1288
1289    /**
1290     * Inserts an item in the data table
1291     *
1292     * @param values the values for the new row
1293     * @return the row ID of the newly created row
1294     */
1295    private long insertData(ContentValues values, boolean markRawContactAsDirty) {
1296        int aggregationMode = RawContacts.AGGREGATION_MODE_DISABLED;
1297        long id = 0;
1298        mValues.clear();
1299        mValues.putAll(values);
1300
1301        long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
1302
1303        // Replace package with internal mapping
1304        final String packageName = mValues.getAsString(Data.RES_PACKAGE);
1305        if (packageName != null) {
1306            mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1307        }
1308        mValues.remove(Data.RES_PACKAGE);
1309
1310        // Replace mimetype with internal mapping
1311        final String mimeType = mValues.getAsString(Data.MIMETYPE);
1312        if (TextUtils.isEmpty(mimeType)) {
1313            throw new IllegalArgumentException(Data.MIMETYPE + " is required");
1314        }
1315
1316        mValues.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
1317        mValues.remove(Data.MIMETYPE);
1318
1319        // TODO create GroupMembershipRowHandler and move this code there
1320        resolveGroupSourceIdInValues(rawContactId, mimeType, mDb, mValues, true /* isInsert */);
1321
1322        id = getDataRowHandler(mimeType).insert(mDb, rawContactId, mValues);
1323        if (markRawContactAsDirty) {
1324            setRawContactDirty(rawContactId);
1325        }
1326
1327        aggregationMode = mContactAggregator.markContactForAggregation(mDb, rawContactId);
1328
1329        triggerAggregation(id, aggregationMode);
1330        return id;
1331    }
1332
1333    private void triggerAggregation(long rawContactId, int aggregationMode) {
1334        switch (aggregationMode) {
1335            case RawContacts.AGGREGATION_MODE_DEFAULT:
1336                mScheduleAggregation = true;
1337                break;
1338
1339            case RawContacts.AGGREGATION_MODE_IMMEDITATE:
1340                mContactAggregator.aggregateContact(mDb, rawContactId);
1341                break;
1342
1343            case RawContacts.AGGREGATION_MODE_DISABLED:
1344                // Do nothing
1345                break;
1346        }
1347    }
1348
1349    /**
1350     * Returns the group id of the group with sourceId and the same account as rawContactId.
1351     * If the group doesn't already exist then it is first created,
1352     * @param db SQLiteDatabase to use for this operation
1353     * @param rawContactId the contact this group is associated with
1354     * @param sourceId the sourceIf of the group to query or create
1355     * @return the group id of the existing or created group
1356     * @throws IllegalArgumentException if the contact is not associated with an account
1357     * @throws IllegalStateException if a group needs to be created but the creation failed
1358     */
1359    private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) {
1360        Account account = null;
1361        Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "="
1362                + rawContactId, null, null, null, null);
1363        try {
1364            if (c.moveToNext()) {
1365                final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
1366                final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
1367                if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
1368                    account = new Account(accountName, accountType);
1369                }
1370            }
1371        } finally {
1372            c.close();
1373        }
1374        if (account == null) {
1375            throw new IllegalArgumentException("if the groupmembership only "
1376                    + "has a sourceid the the contact must be associate with "
1377                    + "an account");
1378        }
1379
1380        // look up the group that contains this sourceId and has the same account name and type
1381        // as the contact refered to by rawContactId
1382        c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
1383                Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
1384                new String[]{sourceId, account.name, account.type}, null, null, null);
1385        try {
1386            if (c.moveToNext()) {
1387                return c.getLong(0);
1388            } else {
1389                ContentValues groupValues = new ContentValues();
1390                groupValues.put(Groups.ACCOUNT_NAME, account.name);
1391                groupValues.put(Groups.ACCOUNT_TYPE, account.type);
1392                groupValues.put(Groups.SOURCE_ID, sourceId);
1393                long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
1394                if (groupId < 0) {
1395                    throw new IllegalStateException("unable to create a new group with "
1396                            + "this sourceid: " + groupValues);
1397                }
1398                return groupId;
1399            }
1400        } finally {
1401            c.close();
1402        }
1403    }
1404
1405    /**
1406     * Delete data row by row so that fixing of primaries etc work correctly.
1407     */
1408    private int deleteData(String selection, String[] selectionArgs,
1409            boolean markRawContactAsDirty) {
1410        int count = 0;
1411
1412        // Note that the query will return data according to the access restrictions,
1413        // so we don't need to worry about deleting data we don't have permission to read.
1414        Cursor c = query(Data.CONTENT_URI, DataQuery.COLUMNS, selection, selectionArgs, null);
1415        try {
1416            while(c.moveToNext()) {
1417                long rawContactId = c.getLong(DataQuery.RAW_CONTACT_ID);
1418                String mimeType = c.getString(DataQuery.MIMETYPE);
1419                count += getDataRowHandler(mimeType).delete(mDb, c);
1420                if (markRawContactAsDirty) {
1421                    setRawContactDirty(rawContactId);
1422                }
1423            }
1424        } finally {
1425            c.close();
1426        }
1427
1428        return count;
1429    }
1430
1431    /**
1432     * Delete a data row provided that it is one of the allowed mime types.
1433     */
1434    public int deleteData(long dataId, String[] allowedMimeTypes) {
1435
1436        // Note that the query will return data according to the access restrictions,
1437        // so we don't need to worry about deleting data we don't have permission to read.
1438        Cursor c = query(Data.CONTENT_URI, DataQuery.COLUMNS, Data._ID + "=" + dataId, null, null);
1439
1440        try {
1441            if (!c.moveToFirst()) {
1442                return 0;
1443            }
1444
1445            String mimeType = c.getString(DataQuery.MIMETYPE);
1446            boolean valid = false;
1447            for (int i = 0; i < allowedMimeTypes.length; i++) {
1448                if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
1449                    valid = true;
1450                    break;
1451                }
1452            }
1453
1454            if (!valid) {
1455                throw new IllegalArgumentException("Data type mismatch: expected "
1456                        + Lists.newArrayList(allowedMimeTypes));
1457            }
1458
1459            return getDataRowHandler(mimeType).delete(mDb, c);
1460        } finally {
1461            c.close();
1462        }
1463    }
1464
1465    /**
1466     * Inserts an item in the groups table
1467     */
1468    private long insertGroup(ContentValues values, Account account, boolean markAsDirty) {
1469        ContentValues overriddenValues = new ContentValues(values);
1470        if (!resolveAccount(overriddenValues, account)) {
1471            return -1;
1472        }
1473
1474        // Replace package with internal mapping
1475        final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
1476        if (packageName != null) {
1477            overriddenValues.put(GroupsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1478        }
1479        overriddenValues.remove(Groups.RES_PACKAGE);
1480
1481        if (markAsDirty) {
1482            overriddenValues.put(Groups.DIRTY, 1);
1483        }
1484
1485        return mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
1486    }
1487
1488    /**
1489     * Inserts a presence update.
1490     */
1491    public long insertPresence(ContentValues values) {
1492        final String handle = values.getAsString(Presence.IM_HANDLE);
1493        final String protocol = values.getAsString(Presence.IM_PROTOCOL);
1494        if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
1495            throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required");
1496        }
1497
1498        // TODO: generalize to allow other providers to match against email
1499        boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == Integer.parseInt(protocol);
1500
1501        StringBuilder selection = new StringBuilder();
1502        String[] selectionArgs;
1503        if (matchEmail) {
1504            selection.append("(" + Clauses.WHERE_IM_MATCHES + ") OR ("
1505                    + Clauses.WHERE_EMAIL_MATCHES + ")");
1506            selectionArgs = new String[] { protocol, handle, handle };
1507        } else {
1508            selection.append(Clauses.WHERE_IM_MATCHES);
1509            selectionArgs = new String[] { protocol, handle };
1510        }
1511
1512        if (values.containsKey(Presence.DATA_ID)) {
1513            selection.append(" AND " + DataColumns.CONCRETE_ID + "=")
1514                    .append(values.getAsLong(Presence.DATA_ID));
1515        }
1516
1517        if (values.containsKey(Presence.RAW_CONTACT_ID)) {
1518            selection.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + "=")
1519                    .append(values.getAsLong(Presence.RAW_CONTACT_ID));
1520        }
1521
1522        selection.append(" AND ").append(getContactsRestrictions());
1523
1524        long dataId = -1;
1525        long rawContactId = -1;
1526
1527        Cursor cursor = null;
1528        try {
1529            cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
1530                    selection.toString(), selectionArgs, null, null, null);
1531            if (cursor.moveToFirst()) {
1532                dataId = cursor.getLong(DataContactsQuery.DATA_ID);
1533                rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
1534            } else {
1535                // No contact found, return a null URI
1536                return -1;
1537            }
1538        } finally {
1539            if (cursor != null) {
1540                cursor.close();
1541            }
1542        }
1543
1544        values.put(Presence.DATA_ID, dataId);
1545        values.put(Presence.RAW_CONTACT_ID, rawContactId);
1546
1547        // Insert the presence update
1548        long presenceId = mDb.replace(Tables.PRESENCE, null, values);
1549        return presenceId;
1550    }
1551
1552    @Override
1553    protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
1554        final int match = sUriMatcher.match(uri);
1555        switch (match) {
1556            case SYNCSTATE:
1557                return mOpenHelper.getSyncState().delete(mDb, selection, selectionArgs);
1558
1559            case CONTACTS_ID: {
1560                long contactId = ContentUris.parseId(uri);
1561
1562                // Remove references to the contact first
1563                ContentValues values = new ContentValues();
1564                values.putNull(RawContacts.CONTACT_ID);
1565                mDb.update(Tables.RAW_CONTACTS, values,
1566                        RawContacts.CONTACT_ID + "=" + contactId, null);
1567
1568                return mDb.delete(Tables.CONTACTS, BaseColumns._ID + "=" + contactId, null);
1569            }
1570
1571            case RAW_CONTACTS: {
1572                final boolean permanently =
1573                        readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false);
1574                int numDeletes = 0;
1575                Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
1576                        selection, selectionArgs, null, null, null);
1577                try {
1578                    while (c.moveToNext()) {
1579                        final long rawContactId = c.getLong(0);
1580                        numDeletes += deleteRawContact(rawContactId, permanently);
1581                    }
1582                } finally {
1583                    c.close();
1584                }
1585                return numDeletes;
1586            }
1587
1588            case RAW_CONTACTS_ID: {
1589                final boolean permanently =
1590                        readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false);
1591                final long rawContactId = ContentUris.parseId(uri);
1592                return deleteRawContact(rawContactId, permanently);
1593            }
1594
1595            case DATA: {
1596                return deleteData(selection, selectionArgs, shouldMarkRawContactAsDirty(uri));
1597            }
1598
1599            case DATA_ID: {
1600                long dataId = ContentUris.parseId(uri);
1601                return deleteData(Data._ID + "=" + dataId, null, shouldMarkRawContactAsDirty(uri));
1602            }
1603
1604            case GROUPS_ID: {
1605                boolean markAsDirty = shouldMarkGroupAsDirty(uri);
1606                final boolean deletePermanently =
1607                        readBooleanQueryParameter(uri, Groups.DELETE_PERMANENTLY, false);
1608                return deleteGroup(ContentUris.parseId(uri), markAsDirty, deletePermanently);
1609            }
1610
1611            case GROUPS: {
1612                boolean markAsDirty = shouldMarkGroupAsDirty(uri);
1613                final boolean permanently =
1614                        readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false);
1615                int numDeletes = 0;
1616                Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
1617                        selection, selectionArgs, null, null, null);
1618                try {
1619                    while (c.moveToNext()) {
1620                        numDeletes += deleteGroup(c.getLong(0), markAsDirty, permanently);
1621                    }
1622                } finally {
1623                    c.close();
1624                }
1625                return numDeletes;
1626            }
1627
1628            case SETTINGS: {
1629                return mDb.delete(Tables.SETTINGS, selection, selectionArgs);
1630            }
1631
1632            case PRESENCE: {
1633                return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
1634            }
1635
1636            default:
1637                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
1638        }
1639    }
1640
1641    private boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) {
1642        final String flag = uri.getQueryParameter(name);
1643        return flag == null
1644                ? defaultValue
1645                : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
1646    }
1647
1648    private int deleteGroup(long groupId, boolean markAsDirty, boolean permanently) {
1649        final long groupMembershipMimetypeId = mOpenHelper
1650                .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
1651        mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
1652                + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
1653                + groupId, null);
1654
1655        try {
1656            if (permanently) {
1657                return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
1658            } else {
1659                mValues.clear();
1660                mValues.put(Groups.DELETED, 1);
1661                if (markAsDirty) {
1662                    mValues.put(Groups.DIRTY, 1);
1663                }
1664                return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
1665            }
1666        } finally {
1667            mOpenHelper.updateAllVisible();
1668        }
1669    }
1670
1671    public int deleteRawContact(long rawContactId, boolean permanently) {
1672        // TODO delete aggregation exceptions
1673        mOpenHelper.removeContactIfSingleton(rawContactId);
1674        if (permanently) {
1675            mDb.delete(Tables.PRESENCE, Presence.RAW_CONTACT_ID + "=" + rawContactId, null);
1676            return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
1677        } else {
1678
1679            // Clear out data used for aggregation - this deleted contact should not be aggregated
1680            mDb.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
1681                    + NameLookupColumns.RAW_CONTACT_ID + "=" + rawContactId);
1682
1683            mValues.clear();
1684            mValues.put(RawContacts.DELETED, 1);
1685            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
1686            mValues.putNull(RawContacts.CONTACT_ID);
1687            mValues.put(RawContacts.DIRTY, 1);
1688            return updateRawContact(rawContactId, mValues, null, null);
1689        }
1690    }
1691
1692    private static Account readAccountFromQueryParams(Uri uri) {
1693        final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
1694        final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
1695        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
1696            return null;
1697        }
1698        return new Account(name, type);
1699    }
1700
1701    @Override
1702    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
1703            String[] selectionArgs) {
1704        int count = 0;
1705
1706        final int match = sUriMatcher.match(uri);
1707        switch(match) {
1708            case SYNCSTATE:
1709                return mOpenHelper.getSyncState().update(mDb, values, selection, selectionArgs);
1710
1711            // TODO(emillar): We will want to disallow editing the contacts table at some point.
1712            case CONTACTS: {
1713                count = mDb.update(Tables.CONTACTS, values, selection, selectionArgs);
1714                break;
1715            }
1716
1717            case CONTACTS_ID: {
1718                count = updateContactData(ContentUris.parseId(uri), values);
1719                break;
1720            }
1721
1722            case DATA: {
1723                count = updateData(uri, values, selection, selectionArgs,
1724                        shouldMarkRawContactAsDirty(uri));
1725                break;
1726            }
1727
1728            case DATA_ID: {
1729                count = updateData(uri, values, selection, selectionArgs,
1730                        shouldMarkRawContactAsDirty(uri));
1731                break;
1732            }
1733
1734            case RAW_CONTACTS: {
1735
1736                // TODO: security checks
1737                count = mDb.update(Tables.RAW_CONTACTS, values, selection, selectionArgs);
1738                break;
1739            }
1740
1741            case RAW_CONTACTS_ID: {
1742                long rawContactId = ContentUris.parseId(uri);
1743                count = updateRawContact(rawContactId, values, selection, selectionArgs);
1744                break;
1745            }
1746
1747            case GROUPS: {
1748                count = updateGroups(values, selection, selectionArgs,
1749                        shouldMarkGroupAsDirty(uri));
1750                break;
1751            }
1752
1753            case GROUPS_ID: {
1754                long groupId = ContentUris.parseId(uri);
1755                String selectionWithId = (Groups._ID + "=" + groupId + " ")
1756                        + (selection == null ? "" : " AND " + selection);
1757                count = updateGroups(values, selectionWithId, selectionArgs,
1758                        shouldMarkGroupAsDirty(uri));
1759                break;
1760            }
1761
1762            case AGGREGATION_EXCEPTIONS: {
1763                count = updateAggregationException(mDb, values);
1764                break;
1765            }
1766
1767            case SETTINGS: {
1768                count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
1769                break;
1770            }
1771
1772            default:
1773                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
1774        }
1775
1776        return count;
1777    }
1778
1779    private int updateGroups(ContentValues values, String selectionWithId,
1780            String[] selectionArgs, boolean markAsDirty) {
1781
1782        ContentValues updatedValues;
1783        if (markAsDirty) {
1784            updatedValues = mValues;
1785            updatedValues.clear();
1786            updatedValues.putAll(values);
1787            updatedValues.put(Groups.DIRTY, 1);
1788        } else {
1789            updatedValues = values;
1790        }
1791
1792        int count = mDb.update(Tables.GROUPS, values, selectionWithId, selectionArgs);
1793
1794        // If changing visibility, then update contacts
1795        if (values.containsKey(Groups.GROUP_VISIBLE)) {
1796            mOpenHelper.updateAllVisible();
1797        }
1798        return count;
1799    }
1800
1801    private int updateRawContact(long rawContactId, ContentValues values, String selection,
1802            String[] selectionArgs) {
1803
1804        // TODO: security checks
1805        String selectionWithId = (RawContacts._ID + " = " + rawContactId + " ")
1806                + (selection == null ? "" : " AND " + selection);
1807        return mDb.update(Tables.RAW_CONTACTS, values, selectionWithId, selectionArgs);
1808    }
1809
1810    private int updateData(Uri uri, ContentValues values, String selection,
1811            String[] selectionArgs, boolean markRawContactAsDirty) {
1812        int count = 0;
1813
1814        // Note that the query will return data according to the access restrictions,
1815        // so we don't need to worry about updating data we don't have permission to read.
1816        Cursor c = query(uri, DataIdQuery.COLUMNS, selection, selectionArgs, null);
1817        try {
1818            while(c.moveToNext()) {
1819                final long dataId = c.getLong(DataIdQuery._ID);
1820                final long rawContactId = c.getLong(DataIdQuery.RAW_CONTACT_ID);
1821                final String mimetype = c.getString(DataIdQuery.MIMETYPE);
1822                count += updateData(dataId, rawContactId, mimetype, values,
1823                        markRawContactAsDirty);
1824            }
1825        } finally {
1826            c.close();
1827        }
1828
1829        return count;
1830    }
1831
1832    private int updateData(long dataId, long rawContactId, String mimeType, ContentValues values,
1833            boolean markRawContactAsDirty) {
1834        mValues.clear();
1835        mValues.putAll(values);
1836        mValues.remove(Data._ID);
1837        mValues.remove(Data.RAW_CONTACT_ID);
1838        mValues.remove(Data.MIMETYPE);
1839
1840        String packageName = values.getAsString(Data.RES_PACKAGE);
1841        if (packageName != null) {
1842            mValues.remove(Data.RES_PACKAGE);
1843            mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1844        }
1845
1846        boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
1847        boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
1848
1849        // Remove primary or super primary values being set to 0. This is disallowed by the
1850        // content provider.
1851        if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
1852            containsIsSuperPrimary = false;
1853            mValues.remove(Data.IS_SUPER_PRIMARY);
1854        }
1855        if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
1856            containsIsPrimary = false;
1857            mValues.remove(Data.IS_PRIMARY);
1858        }
1859
1860        if (containsIsSuperPrimary) {
1861            setIsSuperPrimary(dataId);
1862            setIsPrimary(dataId);
1863
1864            // Now that we've taken care of setting these, remove them from "values".
1865            mValues.remove(Data.IS_SUPER_PRIMARY);
1866            if (containsIsPrimary) {
1867                mValues.remove(Data.IS_PRIMARY);
1868            }
1869        } else if (containsIsPrimary) {
1870            setIsPrimary(dataId);
1871
1872            // Now that we've taken care of setting this, remove it from "values".
1873            mValues.remove(Data.IS_PRIMARY);
1874        }
1875
1876        // TODO create GroupMembershipRowHandler and move this code there
1877        resolveGroupSourceIdInValues(rawContactId, mimeType, mDb, mValues, false /* isInsert */);
1878
1879        if (mValues.size() > 0) {
1880            mDb.update(Tables.DATA, mValues, Data._ID + " = " + dataId, null);
1881            if (markRawContactAsDirty) {
1882                setRawContactDirty(rawContactId);
1883            }
1884
1885            return 1;
1886        }
1887        return 0;
1888    }
1889
1890    private void resolveGroupSourceIdInValues(long rawContactId, String mimeType, SQLiteDatabase db,
1891            ContentValues values, boolean isInsert) {
1892        if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
1893            boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
1894            boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
1895            if (containsGroupSourceId && containsGroupId) {
1896                throw new IllegalArgumentException(
1897                        "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
1898                                + "and GroupMembership.GROUP_ROW_ID");
1899            }
1900
1901            if (!containsGroupSourceId && !containsGroupId) {
1902                if (isInsert) {
1903                    throw new IllegalArgumentException(
1904                            "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
1905                                    + "and GroupMembership.GROUP_ROW_ID");
1906                } else {
1907                    return;
1908                }
1909            }
1910
1911            if (containsGroupSourceId) {
1912                final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
1913                final long groupId = getOrMakeGroup(db, rawContactId, sourceId);
1914                values.remove(GroupMembership.GROUP_SOURCE_ID);
1915                values.put(GroupMembership.GROUP_ROW_ID, groupId);
1916            }
1917        }
1918    }
1919
1920    private int updateContactData(long contactId, ContentValues values) {
1921
1922        // First update all constituent contacts
1923        ContentValues optionValues = new ContentValues(5);
1924        OpenHelper.copyStringValue(optionValues, RawContacts.CUSTOM_RINGTONE,
1925                values, Contacts.CUSTOM_RINGTONE);
1926        OpenHelper.copyLongValue(optionValues, RawContacts.SEND_TO_VOICEMAIL,
1927                values, Contacts.SEND_TO_VOICEMAIL);
1928        OpenHelper.copyLongValue(optionValues, RawContacts.LAST_TIME_CONTACTED,
1929                values, Contacts.LAST_TIME_CONTACTED);
1930        OpenHelper.copyLongValue(optionValues, RawContacts.TIMES_CONTACTED,
1931                values, Contacts.TIMES_CONTACTED);
1932        OpenHelper.copyLongValue(optionValues, RawContacts.STARRED,
1933                values, Contacts.STARRED);
1934
1935        // Nothing to update - just return
1936        if (optionValues.size() == 0) {
1937            return 0;
1938        }
1939
1940        mDb.update(Tables.RAW_CONTACTS, optionValues,
1941                RawContacts.CONTACT_ID + "=" + contactId, null);
1942        return mDb.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
1943    }
1944
1945    public void updateContactTime(long contactId, long lastTimeContacted) {
1946        mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
1947        mLastTimeContactedUpdate.bindLong(2, contactId);
1948        mLastTimeContactedUpdate.execute();
1949    }
1950
1951    private static class RawContactPair {
1952        final long rawContactId1;
1953        final long rawContactId2;
1954
1955        /**
1956         * Constructor that ensures that this.rawContactId1 &lt; this.rawContactId2
1957         */
1958        public RawContactPair(long rawContactId1, long rawContactId2) {
1959            if (rawContactId1 < rawContactId2) {
1960                this.rawContactId1 = rawContactId1;
1961                this.rawContactId2 = rawContactId2;
1962            } else {
1963                this.rawContactId2 = rawContactId1;
1964                this.rawContactId1 = rawContactId2;
1965            }
1966        }
1967    }
1968
1969    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
1970        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
1971        long contactId = values.getAsInteger(AggregationExceptions.CONTACT_ID);
1972        long rawContactId = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID);
1973
1974        // First, we build a list of rawContactID-rawContactID pairs for the given contact.
1975        ArrayList<RawContactPair> pairs = new ArrayList<RawContactPair>();
1976        Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts.CONTACT_ID
1977                + "=" + contactId, null, null, null, null);
1978        try {
1979            while (c.moveToNext()) {
1980                long aggregatedContactId = c.getLong(ContactsQuery.RAW_CONTACT_ID);
1981                if (aggregatedContactId != rawContactId) {
1982                    pairs.add(new RawContactPair(aggregatedContactId, rawContactId));
1983                }
1984            }
1985        } finally {
1986            c.close();
1987        }
1988
1989        // Now we iterate through all contact pairs to see if we need to insert/delete/update
1990        // the corresponding exception
1991        ContentValues exceptionValues = new ContentValues(3);
1992        exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
1993        for (RawContactPair pair : pairs) {
1994            final String whereClause =
1995                    AggregationExceptionColumns.RAW_CONTACT_ID1 + "=" + pair.rawContactId1 + " AND "
1996                    + AggregationExceptionColumns.RAW_CONTACT_ID2 + "=" + pair.rawContactId2;
1997            if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
1998                db.delete(Tables.AGGREGATION_EXCEPTIONS, whereClause, null);
1999            } else {
2000                exceptionValues.put(AggregationExceptionColumns.RAW_CONTACT_ID1, pair.rawContactId1);
2001                exceptionValues.put(AggregationExceptionColumns.RAW_CONTACT_ID2, pair.rawContactId2);
2002                db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
2003                        exceptionValues);
2004            }
2005        }
2006
2007        int aggregationMode = mContactAggregator.markContactForAggregation(mDb, rawContactId);
2008        if (aggregationMode != RawContacts.AGGREGATION_MODE_DISABLED) {
2009            mContactAggregator.aggregateContact(db, rawContactId);
2010            if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC
2011                    || exceptionType == AggregationExceptions.TYPE_KEEP_OUT) {
2012                mContactAggregator.updateAggregateData(contactId);
2013            }
2014        }
2015
2016        // The return value is fake - we just confirm that we made a change, not count actual
2017        // rows changed.
2018        return 1;
2019    }
2020
2021    /**
2022     * Test if a {@link String} value appears in the given list.
2023     */
2024    private boolean isContained(String[] array, String value) {
2025        if (array != null) {
2026            for (String test : array) {
2027                if (value.equals(test)) {
2028                    return true;
2029                }
2030            }
2031        }
2032        return false;
2033    }
2034
2035    /**
2036     * Test if a {@link String} value appears in the given list, and add to the
2037     * array if the value doesn't already appear.
2038     */
2039    private String[] assertContained(String[] array, String value) {
2040        if (array != null && !isContained(array, value)) {
2041            String[] newArray = new String[array.length + 1];
2042            System.arraycopy(array, 0, newArray, 0, array.length);
2043            newArray[array.length] = value;
2044            array = newArray;
2045        }
2046        return array;
2047    }
2048
2049    @Override
2050    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
2051            String sortOrder) {
2052        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2053
2054        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2055        String groupBy = null;
2056        String limit = getLimit(uri);
2057
2058        // TODO: Consider writing a test case for RestrictionExceptions when you
2059        // write a new query() block to make sure it protects restricted data.
2060        final int match = sUriMatcher.match(uri);
2061        switch (match) {
2062            case SYNCSTATE:
2063                return mOpenHelper.getSyncState().query(db, projection, selection,  selectionArgs,
2064                        sortOrder);
2065
2066            case CONTACTS: {
2067                qb.setTables(mOpenHelper.getContactView());
2068                qb.setProjectionMap(sContactsProjectionMap);
2069                break;
2070            }
2071
2072            case CONTACTS_ID: {
2073                long contactId = ContentUris.parseId(uri);
2074                qb.setTables(mOpenHelper.getContactView());
2075                qb.setProjectionMap(sContactsProjectionMap);
2076                qb.appendWhere(Contacts._ID + "=" + contactId);
2077                break;
2078            }
2079
2080            case CONTACTS_SUMMARY: {
2081                // TODO: join into social status tables
2082                qb.setTables(mOpenHelper.getContactSummaryView());
2083                qb.setProjectionMap(sContactsSummaryProjectionMap);
2084                groupBy = Contacts._ID;
2085                break;
2086            }
2087
2088            case CONTACTS_SUMMARY_ID: {
2089                // TODO: join into social status tables
2090                long contactId = ContentUris.parseId(uri);
2091                qb.setTables(mOpenHelper.getContactSummaryView());
2092                qb.setProjectionMap(sContactsSummaryProjectionMap);
2093                groupBy = Contacts._ID;
2094                qb.appendWhere(Contacts._ID + "=" + contactId);
2095                break;
2096            }
2097
2098            case CONTACTS_SUMMARY_FILTER: {
2099                qb.setTables(mOpenHelper.getContactSummaryView());
2100                qb.setProjectionMap(sContactsSummaryProjectionMap);
2101                groupBy = Contacts._ID;
2102
2103                if (uri.getPathSegments().size() > 2) {
2104                    String filterParam = uri.getLastPathSegment();
2105                    StringBuilder sb = new StringBuilder();
2106                    sb.append("raw_contact_id IN ");
2107                    appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
2108                    qb.appendWhere(sb.toString());
2109                }
2110                break;
2111            }
2112
2113            case CONTACTS_SUMMARY_STREQUENT_FILTER:
2114            case CONTACTS_SUMMARY_STREQUENT: {
2115                String filterSql = null;
2116                if (match == CONTACTS_SUMMARY_STREQUENT_FILTER
2117                        && uri.getPathSegments().size() > 3) {
2118                    String filterParam = uri.getLastPathSegment();
2119                    StringBuilder sb = new StringBuilder();
2120                    sb.append("raw_contact_id IN ");
2121                    appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
2122                    filterSql = sb.toString();
2123                }
2124
2125                // Build the first query for starred
2126                qb.setTables(mOpenHelper.getContactSummaryView());
2127                qb.setProjectionMap(sContactsSummaryProjectionMap);
2128                if (filterSql != null) {
2129                    qb.appendWhere(filterSql);
2130                }
2131                final String starredQuery = qb.buildQuery(projection, Contacts.STARRED + "=1",
2132                        null, Contacts._ID, null, null, null);
2133
2134                // Build the second query for frequent
2135                qb = new SQLiteQueryBuilder();
2136                qb.setTables(mOpenHelper.getContactSummaryView());
2137                qb.setProjectionMap(sContactsSummaryProjectionMap);
2138                if (filterSql != null) {
2139                    qb.appendWhere(filterSql);
2140                }
2141                final String frequentQuery = qb.buildQuery(projection,
2142                        Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
2143                        + " = 0 OR " + Contacts.STARRED + " IS NULL)",
2144                        null, Contacts._ID, null, null, null);
2145
2146                // Put them together
2147                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
2148                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
2149                Cursor c = db.rawQuery(query, null);
2150                if (c != null) {
2151                    c.setNotificationUri(getContext().getContentResolver(),
2152                            ContactsContract.AUTHORITY_URI);
2153                }
2154                return c;
2155            }
2156
2157            case CONTACTS_SUMMARY_GROUP: {
2158                qb.setTables(mOpenHelper.getContactSummaryView());
2159                qb.setProjectionMap(sContactsSummaryProjectionMap);
2160                if (uri.getPathSegments().size() > 2) {
2161                    qb.appendWhere(sContactsInGroupSelect);
2162                    selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
2163                }
2164                groupBy = Contacts._ID;
2165                break;
2166            }
2167
2168            case CONTACTS_DATA: {
2169                long contactId = Long.parseLong(uri.getPathSegments().get(1));
2170
2171                qb.setTables(mOpenHelper.getDataView());
2172                qb.setProjectionMap(sDataProjectionMap);
2173                appendAccountFromParameter(qb, uri);
2174                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
2175                break;
2176            }
2177
2178            case PHONES: {
2179                qb.setTables(mOpenHelper.getDataView());
2180                qb.setProjectionMap(sDataProjectionMap);
2181                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
2182                break;
2183            }
2184
2185            case PHONES_FILTER: {
2186                qb.setTables(mOpenHelper.getDataView());
2187                qb.setProjectionMap(sDataProjectionMap);
2188                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
2189                if (uri.getPathSegments().size() > 2) {
2190                    String filterParam = uri.getLastPathSegment();
2191                    StringBuilder sb = new StringBuilder();
2192                    sb.append(Data.RAW_CONTACT_ID + " IN ");
2193                    appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
2194                    qb.appendWhere(" AND " + sb);
2195                }
2196                break;
2197            }
2198
2199            case EMAILS: {
2200                qb.setTables(mOpenHelper.getDataView());
2201                qb.setProjectionMap(sDataProjectionMap);
2202                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
2203                break;
2204            }
2205
2206            case EMAILS_FILTER: {
2207                qb.setTables(mOpenHelper.getDataView());
2208                qb.setProjectionMap(sDataProjectionMap);
2209                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
2210                if (uri.getPathSegments().size() > 2) {
2211                    qb.appendWhere(" AND " + CommonDataKinds.Email.DATA + "=");
2212                    qb.appendWhereEscapeString(uri.getLastPathSegment());
2213                }
2214                break;
2215            }
2216
2217            case POSTALS: {
2218                qb.setTables(mOpenHelper.getDataView());
2219                qb.setProjectionMap(sDataProjectionMap);
2220                qb.appendWhere(Data.MIMETYPE + " = '" + StructuredPostal.CONTENT_ITEM_TYPE + "'");
2221                break;
2222            }
2223
2224            case RAW_CONTACTS: {
2225                qb.setTables(mOpenHelper.getRawContactView());
2226                qb.setProjectionMap(sRawContactsProjectionMap);
2227                break;
2228            }
2229
2230            case RAW_CONTACTS_ID: {
2231                long rawContactId = ContentUris.parseId(uri);
2232                qb.setTables(mOpenHelper.getRawContactView());
2233                qb.setProjectionMap(sRawContactsProjectionMap);
2234                qb.appendWhere(RawContacts._ID + "=" + rawContactId);
2235                break;
2236            }
2237
2238            case RAW_CONTACTS_DATA: {
2239                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
2240                qb.setTables(mOpenHelper.getDataView());
2241                qb.setProjectionMap(sDataProjectionMap);
2242                qb.appendWhere(Data.RAW_CONTACT_ID + "=" + rawContactId);
2243                break;
2244            }
2245
2246            case DATA: {
2247                qb.setTables(mOpenHelper.getDataView());
2248                qb.setProjectionMap(sDataProjectionMap);
2249                appendAccountFromParameter(qb, uri);
2250                break;
2251            }
2252
2253            case DATA_ID: {
2254                qb.setTables(mOpenHelper.getDataView());
2255                qb.setProjectionMap(sDataProjectionMap);
2256                qb.appendWhere(Data._ID + "=" + ContentUris.parseId(uri));
2257                break;
2258            }
2259
2260            case PHONE_LOOKUP: {
2261
2262                if (TextUtils.isEmpty(sortOrder)) {
2263                    // Default the sort order to something reasonable so we get consistent
2264                    // results when callers don't request an ordering
2265                    sortOrder = RawContactsColumns.CONCRETE_ID;
2266                }
2267
2268                String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
2269                mOpenHelper.buildPhoneLookupAndContactQuery(qb, number);
2270                qb.setProjectionMap(sPhoneLookupProjectionMap);
2271
2272                // Phone lookup cannot be combined with a selection
2273                selection = null;
2274                selectionArgs = null;
2275                break;
2276            }
2277
2278            case GROUPS: {
2279                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2280                qb.setProjectionMap(sGroupsProjectionMap);
2281                break;
2282            }
2283
2284            case GROUPS_ID: {
2285                long groupId = ContentUris.parseId(uri);
2286                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2287                qb.setProjectionMap(sGroupsProjectionMap);
2288                qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId);
2289                break;
2290            }
2291
2292            case GROUPS_SUMMARY: {
2293                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2294                qb.setProjectionMap(sGroupsSummaryProjectionMap);
2295                groupBy = GroupsColumns.CONCRETE_ID;
2296                break;
2297            }
2298
2299            case AGGREGATION_EXCEPTIONS: {
2300                qb.setTables(Tables.AGGREGATION_EXCEPTIONS_JOIN_RAW_CONTACTS);
2301                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
2302                break;
2303            }
2304
2305            case AGGREGATION_SUGGESTIONS: {
2306                long contactId = Long.parseLong(uri.getPathSegments().get(1));
2307                final int maxSuggestions;
2308                if (limit != null) {
2309                    maxSuggestions = Integer.parseInt(limit);
2310                } else {
2311                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
2312                }
2313
2314                return mContactAggregator.queryAggregationSuggestions(contactId, projection,
2315                        sContactsProjectionMap, maxSuggestions);
2316            }
2317
2318            case SETTINGS: {
2319                qb.setTables(Tables.SETTINGS);
2320                qb.setProjectionMap(sSettingsProjectionMap);
2321                break;
2322            }
2323
2324            case PRESENCE: {
2325                qb.setTables(Tables.PRESENCE);
2326                qb.setProjectionMap(sPresenceProjectionMap);
2327                break;
2328            }
2329
2330            case PRESENCE_ID: {
2331                qb.setTables(Tables.PRESENCE);
2332                qb.setProjectionMap(sPresenceProjectionMap);
2333                qb.appendWhere(Presence._ID + "=" + ContentUris.parseId(uri));
2334                break;
2335            }
2336
2337            case SEARCH_SUGGESTIONS: {
2338                return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
2339            }
2340
2341            case SEARCH_SHORTCUT: {
2342                // TODO
2343                break;
2344            }
2345
2346            default:
2347                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
2348                        sortOrder, limit);
2349        }
2350
2351        // Perform the query and set the notification uri
2352        final Cursor c = qb.query(db, projection, selection, selectionArgs,
2353                groupBy, null, sortOrder, limit);
2354        if (c != null) {
2355            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
2356        }
2357        return c;
2358    }
2359
2360    private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
2361        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
2362        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
2363        if (!TextUtils.isEmpty(accountName)) {
2364            qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
2365                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2366                    + RawContacts.ACCOUNT_TYPE + "="
2367                    + DatabaseUtils.sqlEscapeString(accountType));
2368        } else {
2369            qb.appendWhere("1");
2370        }
2371    }
2372
2373    /**
2374     * Gets the value of the "limit" URI query parameter.
2375     *
2376     * @return A string containing a non-negative integer, or <code>null</code> if
2377     *         the parameter is not set, or is set to an invalid value.
2378     */
2379    private String getLimit(Uri url) {
2380        String limitParam = url.getQueryParameter("limit");
2381        if (limitParam == null) {
2382            return null;
2383        }
2384        // make sure that the limit is a non-negative integer
2385        try {
2386            int l = Integer.parseInt(limitParam);
2387            if (l < 0) {
2388                Log.w(TAG, "Invalid limit parameter: " + limitParam);
2389                return null;
2390            }
2391            return String.valueOf(l);
2392        } catch (NumberFormatException ex) {
2393            Log.w(TAG, "Invalid limit parameter: " + limitParam);
2394            return null;
2395        }
2396    }
2397
2398    String getContactsRestrictions() {
2399        if (mOpenHelper.hasRestrictedAccess()) {
2400            return "1";
2401        } else {
2402            return RawContacts.IS_RESTRICTED + "=0";
2403        }
2404    }
2405
2406    public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
2407        if (mOpenHelper.hasRestrictedAccess()) {
2408            return "1";
2409        } else {
2410            return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
2411                    + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
2412        }
2413    }
2414
2415    /**
2416     * An implementation of EntityIterator that joins the contacts and data tables
2417     * and consumes all the data rows for a contact in order to build the Entity for a contact.
2418     */
2419    private static class ContactsEntityIterator implements EntityIterator {
2420        private final Cursor mEntityCursor;
2421        private volatile boolean mIsClosed;
2422
2423        private static final String[] DATA_KEYS = new String[]{
2424                Data.DATA1,
2425                Data.DATA2,
2426                Data.DATA3,
2427                Data.DATA4,
2428                Data.DATA5,
2429                Data.DATA6,
2430                Data.DATA7,
2431                Data.DATA8,
2432                Data.DATA9,
2433                Data.DATA10,
2434                Data.DATA11,
2435                Data.DATA12,
2436                Data.DATA13,
2437                Data.DATA14,
2438                Data.DATA15,
2439                Data.SYNC1,
2440                Data.SYNC2,
2441                Data.SYNC3,
2442                Data.SYNC4};
2443
2444        private static final String[] PROJECTION = new String[]{
2445                RawContacts.ACCOUNT_NAME,
2446                RawContacts.ACCOUNT_TYPE,
2447                RawContacts.SOURCE_ID,
2448                RawContacts.VERSION,
2449                RawContacts.DIRTY,
2450                Data._ID,
2451                Data.RES_PACKAGE,
2452                Data.MIMETYPE,
2453                Data.DATA1,
2454                Data.DATA2,
2455                Data.DATA3,
2456                Data.DATA4,
2457                Data.DATA5,
2458                Data.DATA6,
2459                Data.DATA7,
2460                Data.DATA8,
2461                Data.DATA9,
2462                Data.DATA10,
2463                Data.DATA11,
2464                Data.DATA12,
2465                Data.DATA13,
2466                Data.DATA14,
2467                Data.DATA15,
2468                Data.SYNC1,
2469                Data.SYNC2,
2470                Data.SYNC3,
2471                Data.SYNC4,
2472                Data.RAW_CONTACT_ID,
2473                Data.IS_PRIMARY,
2474                Data.DATA_VERSION,
2475                GroupMembership.GROUP_SOURCE_ID,
2476                RawContacts.SYNC1,
2477                RawContacts.SYNC2,
2478                RawContacts.SYNC3,
2479                RawContacts.SYNC4,
2480                RawContacts.DELETED,
2481                RawContacts.CONTACT_ID};
2482
2483        private static final int COLUMN_ACCOUNT_NAME = 0;
2484        private static final int COLUMN_ACCOUNT_TYPE = 1;
2485        private static final int COLUMN_SOURCE_ID = 2;
2486        private static final int COLUMN_VERSION = 3;
2487        private static final int COLUMN_DIRTY = 4;
2488        private static final int COLUMN_DATA_ID = 5;
2489        private static final int COLUMN_RES_PACKAGE = 6;
2490        private static final int COLUMN_MIMETYPE = 7;
2491        private static final int COLUMN_DATA1 = 8;
2492        private static final int COLUMN_RAW_CONTACT_ID = 27;
2493        private static final int COLUMN_IS_PRIMARY = 28;
2494        private static final int COLUMN_DATA_VERSION = 29;
2495        private static final int COLUMN_GROUP_SOURCE_ID = 30;
2496        private static final int COLUMN_SYNC1 = 31;
2497        private static final int COLUMN_SYNC2 = 32;
2498        private static final int COLUMN_SYNC3 = 33;
2499        private static final int COLUMN_SYNC4 = 34;
2500        private static final int COLUMN_DELETED = 35;
2501        private static final int COLUMN_CONTACT_ID = 36;
2502
2503        public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri,
2504                String selection, String[] selectionArgs, String sortOrder) {
2505            mIsClosed = false;
2506
2507            final String updatedSortOrder = (sortOrder == null)
2508                    ? Data.RAW_CONTACT_ID
2509                    : (Data.RAW_CONTACT_ID + "," + sortOrder);
2510
2511            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
2512            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2513            qb.setTables(Tables.CONTACT_ENTITIES);
2514            if (contactsIdString != null) {
2515                qb.appendWhere(Data.RAW_CONTACT_ID + "=" + contactsIdString);
2516            }
2517            final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
2518            final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
2519            if (!TextUtils.isEmpty(accountName)) {
2520                qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
2521                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2522                        + RawContacts.ACCOUNT_TYPE + "="
2523                        + DatabaseUtils.sqlEscapeString(accountType));
2524            }
2525            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
2526                    null, null, updatedSortOrder);
2527            mEntityCursor.moveToFirst();
2528        }
2529
2530        public void close() {
2531            if (mIsClosed) {
2532                throw new IllegalStateException("closing when already closed");
2533            }
2534            mIsClosed = true;
2535            mEntityCursor.close();
2536        }
2537
2538        public boolean hasNext() throws RemoteException {
2539            if (mIsClosed) {
2540                throw new IllegalStateException("calling hasNext() when the iterator is closed");
2541            }
2542
2543            return !mEntityCursor.isAfterLast();
2544        }
2545
2546        public Entity next() throws RemoteException {
2547            if (mIsClosed) {
2548                throw new IllegalStateException("calling next() when the iterator is closed");
2549            }
2550            if (!hasNext()) {
2551                throw new IllegalStateException("you may only call next() if hasNext() is true");
2552            }
2553
2554            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
2555
2556            final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID);
2557
2558            // we expect the cursor is already at the row we need to read from
2559            ContentValues contactValues = new ContentValues();
2560            contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
2561            contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
2562            contactValues.put(RawContacts._ID, rawContactId);
2563            contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY));
2564            contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION));
2565            contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
2566            contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1));
2567            contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2));
2568            contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3));
2569            contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4));
2570            contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED));
2571            contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID));
2572            Entity contact = new Entity(contactValues);
2573
2574            // read data rows until the contact id changes
2575            do {
2576                if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) {
2577                    break;
2578                }
2579                // add the data to to the contact
2580                ContentValues dataValues = new ContentValues();
2581                dataValues.put(Data._ID, c.getString(COLUMN_DATA_ID));
2582                dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
2583                dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
2584                dataValues.put(Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY));
2585                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
2586                if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
2587                    dataValues.put(GroupMembership.GROUP_SOURCE_ID,
2588                            c.getString(COLUMN_GROUP_SOURCE_ID));
2589                }
2590                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
2591                for (int i = 0; i < DATA_KEYS.length; i++) {
2592                    final int columnIndex = i + COLUMN_DATA1;
2593                    String key = DATA_KEYS[i];
2594                    if (c.isNull(columnIndex)) {
2595                        // don't put anything
2596                    } else if (c.isLong(columnIndex)) {
2597                        dataValues.put(key, c.getLong(columnIndex));
2598                    } else if (c.isFloat(columnIndex)) {
2599                        dataValues.put(key, c.getFloat(columnIndex));
2600                    } else if (c.isString(columnIndex)) {
2601                        dataValues.put(key, c.getString(columnIndex));
2602                    } else if (c.isBlob(columnIndex)) {
2603                        dataValues.put(key, c.getBlob(columnIndex));
2604                    }
2605                }
2606                contact.addSubValue(Data.CONTENT_URI, dataValues);
2607            } while (mEntityCursor.moveToNext());
2608
2609            return contact;
2610        }
2611    }
2612
2613    /**
2614     * An implementation of EntityIterator that joins the contacts and data tables
2615     * and consumes all the data rows for a contact in order to build the Entity for a contact.
2616     */
2617    private static class GroupsEntityIterator implements EntityIterator {
2618        private final Cursor mEntityCursor;
2619        private volatile boolean mIsClosed;
2620
2621        private static final String[] PROJECTION = new String[]{
2622                Groups._ID,
2623                Groups.ACCOUNT_NAME,
2624                Groups.ACCOUNT_TYPE,
2625                Groups.SOURCE_ID,
2626                Groups.DIRTY,
2627                Groups.VERSION,
2628                Groups.RES_PACKAGE,
2629                Groups.TITLE,
2630                Groups.TITLE_RES,
2631                Groups.GROUP_VISIBLE,
2632                Groups.SYNC1,
2633                Groups.SYNC2,
2634                Groups.SYNC3,
2635                Groups.SYNC4,
2636                Groups.SYSTEM_ID,
2637                Groups.NOTES,
2638                Groups.DELETED};
2639
2640        private static final int COLUMN_ID = 0;
2641        private static final int COLUMN_ACCOUNT_NAME = 1;
2642        private static final int COLUMN_ACCOUNT_TYPE = 2;
2643        private static final int COLUMN_SOURCE_ID = 3;
2644        private static final int COLUMN_DIRTY = 4;
2645        private static final int COLUMN_VERSION = 5;
2646        private static final int COLUMN_RES_PACKAGE = 6;
2647        private static final int COLUMN_TITLE = 7;
2648        private static final int COLUMN_TITLE_RES = 8;
2649        private static final int COLUMN_GROUP_VISIBLE = 9;
2650        private static final int COLUMN_SYNC1 = 10;
2651        private static final int COLUMN_SYNC2 = 11;
2652        private static final int COLUMN_SYNC3 = 12;
2653        private static final int COLUMN_SYNC4 = 13;
2654        private static final int COLUMN_SYSTEM_ID = 14;
2655        private static final int COLUMN_NOTES = 15;
2656        private static final int COLUMN_DELETED = 16;
2657
2658        public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri,
2659                String selection, String[] selectionArgs, String sortOrder) {
2660            mIsClosed = false;
2661
2662            final String updatedSortOrder = (sortOrder == null)
2663                    ? Groups._ID
2664                    : (Groups._ID + "," + sortOrder);
2665
2666            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
2667            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2668            qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2669            qb.setProjectionMap(sGroupsProjectionMap);
2670            if (groupIdString != null) {
2671                qb.appendWhere(Groups._ID + "=" + groupIdString);
2672            }
2673            final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
2674            final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
2675            if (!TextUtils.isEmpty(accountName)) {
2676                qb.appendWhere(Groups.ACCOUNT_NAME + "="
2677                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2678                        + Groups.ACCOUNT_TYPE + "="
2679                        + DatabaseUtils.sqlEscapeString(accountType));
2680            }
2681            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
2682                    null, null, updatedSortOrder);
2683            mEntityCursor.moveToFirst();
2684        }
2685
2686        public void close() {
2687            if (mIsClosed) {
2688                throw new IllegalStateException("closing when already closed");
2689            }
2690            mIsClosed = true;
2691            mEntityCursor.close();
2692        }
2693
2694        public boolean hasNext() throws RemoteException {
2695            if (mIsClosed) {
2696                throw new IllegalStateException("calling hasNext() when the iterator is closed");
2697            }
2698
2699            return !mEntityCursor.isAfterLast();
2700        }
2701
2702        public Entity next() throws RemoteException {
2703            if (mIsClosed) {
2704                throw new IllegalStateException("calling next() when the iterator is closed");
2705            }
2706            if (!hasNext()) {
2707                throw new IllegalStateException("you may only call next() if hasNext() is true");
2708            }
2709
2710            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
2711
2712            final long groupId = c.getLong(COLUMN_ID);
2713
2714            // we expect the cursor is already at the row we need to read from
2715            ContentValues groupValues = new ContentValues();
2716            groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
2717            groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
2718            groupValues.put(Groups._ID, groupId);
2719            groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY));
2720            groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION));
2721            groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
2722            groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
2723            groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE));
2724            groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES));
2725            groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE));
2726            groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1));
2727            groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2));
2728            groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3));
2729            groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4));
2730            groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID));
2731            groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED));
2732            groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES));
2733            Entity group = new Entity(groupValues);
2734
2735            mEntityCursor.moveToNext();
2736
2737            return group;
2738        }
2739    }
2740
2741    @Override
2742    public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
2743            String sortOrder) {
2744        waitForAccess();
2745
2746        final int match = sUriMatcher.match(uri);
2747        switch (match) {
2748            case RAW_CONTACTS:
2749            case RAW_CONTACTS_ID:
2750                String contactsIdString = null;
2751                if (match == RAW_CONTACTS_ID) {
2752                    contactsIdString = uri.getPathSegments().get(1);
2753                }
2754
2755                return new ContactsEntityIterator(this, contactsIdString,
2756                        uri, selection, selectionArgs, sortOrder);
2757            case GROUPS:
2758            case GROUPS_ID:
2759                String idString = null;
2760                if (match == GROUPS_ID) {
2761                    idString = uri.getPathSegments().get(1);
2762                }
2763
2764                return new GroupsEntityIterator(this, idString,
2765                        uri, selection, selectionArgs, sortOrder);
2766            default:
2767                throw new UnsupportedOperationException("Unknown uri: " + uri);
2768        }
2769    }
2770
2771    @Override
2772    public String getType(Uri uri) {
2773        final int match = sUriMatcher.match(uri);
2774        switch (match) {
2775            case CONTACTS: return Contacts.CONTENT_TYPE;
2776            case CONTACTS_ID: return Contacts.CONTENT_ITEM_TYPE;
2777            case RAW_CONTACTS: return RawContacts.CONTENT_TYPE;
2778            case RAW_CONTACTS_ID: return RawContacts.CONTENT_ITEM_TYPE;
2779            case DATA_ID:
2780                final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2781                long dataId = ContentUris.parseId(uri);
2782                return mOpenHelper.getDataMimeType(dataId);
2783            case AGGREGATION_EXCEPTIONS: return AggregationExceptions.CONTENT_TYPE;
2784            case AGGREGATION_EXCEPTION_ID: return AggregationExceptions.CONTENT_ITEM_TYPE;
2785            case SETTINGS: return Settings.CONTENT_TYPE;
2786            case AGGREGATION_SUGGESTIONS: return Contacts.CONTENT_TYPE;
2787            case SEARCH_SUGGESTIONS:
2788                return SearchManager.SUGGEST_MIME_TYPE;
2789            case SEARCH_SHORTCUT:
2790                return SearchManager.SHORTCUT_MIME_TYPE;
2791        }
2792        throw new UnsupportedOperationException("Unknown uri: " + uri);
2793    }
2794
2795    private void setDisplayName(long rawContactId, String displayName) {
2796        if (displayName != null) {
2797            mContactDisplayNameUpdate.bindString(1, displayName);
2798        } else {
2799            mContactDisplayNameUpdate.bindNull(1);
2800        }
2801        mContactDisplayNameUpdate.bindLong(2, rawContactId);
2802        mContactDisplayNameUpdate.execute();
2803    }
2804
2805    /**
2806     * Checks the {@link Data#MARK_AS_DIRTY} query parameter.
2807     *
2808     * Returns true if the parameter is missing or is either "true" or "1".
2809     */
2810    private boolean shouldMarkRawContactAsDirty(Uri uri) {
2811        if (mImportMode) {
2812            return false;
2813        }
2814
2815        String param = uri.getQueryParameter(Data.MARK_AS_DIRTY);
2816        return param == null || (!param.equalsIgnoreCase("false") && !param.equals("0"));
2817    }
2818
2819    /**
2820     * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
2821     */
2822    private void setRawContactDirty(long rawContactId) {
2823        mRawContactDirtyUpdate.bindLong(1, rawContactId);
2824        mRawContactDirtyUpdate.execute();
2825    }
2826
2827    /**
2828     * Checks the {@link Groups#MARK_AS_DIRTY} query parameter.
2829     *
2830     * Returns true if the parameter is missing or is either "true" or "1".
2831     */
2832    private boolean shouldMarkGroupAsDirty(Uri uri) {
2833        if (mImportMode) {
2834            return false;
2835        }
2836
2837        return readBooleanQueryParameter(uri, Groups.MARK_AS_DIRTY, true);
2838    }
2839
2840    /*
2841     * Sets the given dataId record in the "data" table to primary, and resets all data records of
2842     * the same mimetype and under the same contact to not be primary.
2843     *
2844     * @param dataId the id of the data record to be set to primary.
2845     */
2846    private void setIsPrimary(long dataId) {
2847        mSetPrimaryStatement.bindLong(1, dataId);
2848        mSetPrimaryStatement.bindLong(2, dataId);
2849        mSetPrimaryStatement.bindLong(3, dataId);
2850        mSetPrimaryStatement.execute();
2851    }
2852
2853    /*
2854     * Sets the given dataId record in the "data" table to "super primary", and resets all data
2855     * records of the same mimetype and under the same aggregate to not be "super primary".
2856     *
2857     * @param dataId the id of the data record to be set to primary.
2858     */
2859    private void setIsSuperPrimary(long dataId) {
2860        mSetSuperPrimaryStatement.bindLong(1, dataId);
2861        mSetSuperPrimaryStatement.bindLong(2, dataId);
2862        mSetSuperPrimaryStatement.bindLong(3, dataId);
2863        mSetSuperPrimaryStatement.execute();
2864    }
2865
2866    public String getRawContactsByFilterAsNestedQuery(String filterParam) {
2867        StringBuilder sb = new StringBuilder();
2868        appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
2869        return sb.toString();
2870    }
2871
2872    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
2873            String limit) {
2874        sb.append("(SELECT DISTINCT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '");
2875        sb.append(NameNormalizer.normalize(filterParam));
2876        sb.append("*'");
2877        if (limit != null) {
2878            sb.append(" LIMIT ").append(limit);
2879        }
2880        sb.append(")");
2881    }
2882
2883    /**
2884     * Inserts an argument at the beginning of the selection arg list.
2885     */
2886    private String[] insertSelectionArg(String[] selectionArgs, String arg) {
2887        if (selectionArgs == null) {
2888            return new String[] {arg};
2889        } else {
2890            int newLength = selectionArgs.length + 1;
2891            String[] newSelectionArgs = new String[newLength];
2892            newSelectionArgs[0] = arg;
2893            System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
2894            return newSelectionArgs;
2895        }
2896    }
2897
2898    protected Account getDefaultAccount() {
2899        AccountManager accountManager = AccountManager.get(getContext());
2900        try {
2901            Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE,
2902                    new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult();
2903            if (accounts != null && accounts.length > 0) {
2904                return accounts[0];
2905            }
2906        } catch (Throwable e) {
2907            throw new RuntimeException("Cannot determine the default account "
2908                    + "for contacts compatibility", e);
2909        }
2910        throw new RuntimeException("Cannot determine the default account "
2911                + "for contacts compatibility");
2912    }
2913}
2914