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