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