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