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