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