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