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