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