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