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