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