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