ContactsProvider2.java revision 1443b159afd6149659383ff5fd7b7958d6c077cc
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 AS _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, PackagesColumns.PACKAGE + " AS " + 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, Tables.GROUPS + "." + Groups.SYNC1 + " AS " + Groups.SYNC1);
592        columns.put(Groups.SYNC2, Tables.GROUPS + "." + Groups.SYNC2 + " AS " + Groups.SYNC2);
593        columns.put(Groups.SYNC3, Tables.GROUPS + "." + Groups.SYNC3 + " AS " + Groups.SYNC3);
594        columns.put(Groups.SYNC4, Tables.GROUPS + "." + Groups.SYNC4 + " AS " + 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    }
1700
1701    private void flushTransactionalChanges() {
1702        if (VERBOSE_LOGGING) {
1703            Log.v(TAG, "flushTransactionChanges");
1704        }
1705        for (long rawContactId : mInsertedRawContacts) {
1706            mContactAggregator.insertContact(mDb, rawContactId);
1707        }
1708
1709        String ids;
1710        if (!mUpdatedRawContacts.isEmpty()) {
1711            ids = buildIdsString(mUpdatedRawContacts);
1712            mDb.execSQL("UPDATE raw_contacts SET version = version + 1 WHERE _id in " + ids,
1713                    new Object[]{});
1714        }
1715
1716        for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
1717            long id = entry.getKey();
1718            mOpenHelper.getSyncState().update(mDb, id, entry.getValue());
1719        }
1720
1721        clearTransactionalChanges();
1722    }
1723
1724    private String buildIdsString(HashSet<Long> ids) {
1725        StringBuilder idsBuilder = null;
1726        for (long id : ids) {
1727            if (idsBuilder == null) {
1728                idsBuilder = new StringBuilder();
1729                idsBuilder.append("(");
1730            } else {
1731                idsBuilder.append(",");
1732            }
1733            idsBuilder.append(id);
1734        }
1735        idsBuilder.append(")");
1736        return idsBuilder.toString();
1737    }
1738
1739    @Override
1740    protected void onEndTransaction() {
1741        if (mScheduleAggregation) {
1742            mScheduleAggregation = false;
1743            scheduleContactAggregation();
1744        }
1745        super.onEndTransaction();
1746    }
1747
1748    @Override
1749    protected void notifyChange() {
1750        notifyChange(mSyncToNetwork);
1751        mSyncToNetwork = false;
1752    }
1753
1754    protected void notifyChange(boolean syncToNetwork) {
1755        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
1756                syncToNetwork);
1757    }
1758
1759    protected void scheduleContactAggregation() {
1760        mContactAggregator.run();
1761    }
1762
1763    private boolean isNewRawContact(long rawContactId) {
1764        return mInsertedRawContacts.contains(rawContactId);
1765    }
1766
1767    private DataRowHandler getDataRowHandler(final String mimeType) {
1768        DataRowHandler handler = mDataRowHandlers.get(mimeType);
1769        if (handler == null) {
1770            handler = new CustomDataRowHandler(mimeType);
1771            mDataRowHandlers.put(mimeType, handler);
1772        }
1773        return handler;
1774    }
1775
1776    @Override
1777    protected Uri insertInTransaction(Uri uri, ContentValues values) {
1778        if (VERBOSE_LOGGING) {
1779            Log.v(TAG, "insertInTransaction: " + uri);
1780        }
1781
1782        final boolean callerIsSyncAdapter =
1783                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
1784
1785        final int match = sUriMatcher.match(uri);
1786        long id = 0;
1787
1788        switch (match) {
1789            case SYNCSTATE:
1790                id = mOpenHelper.getSyncState().insert(mDb, values);
1791                break;
1792
1793            case CONTACTS: {
1794                insertContact(values);
1795                break;
1796            }
1797
1798            case RAW_CONTACTS: {
1799                final Account account = readAccountFromQueryParams(uri);
1800                id = insertRawContact(values, account);
1801                mSyncToNetwork |= !callerIsSyncAdapter;
1802                break;
1803            }
1804
1805            case RAW_CONTACTS_DATA: {
1806                values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
1807                id = insertData(values, callerIsSyncAdapter);
1808                mSyncToNetwork |= !callerIsSyncAdapter;
1809                break;
1810            }
1811
1812            case DATA: {
1813                id = insertData(values, callerIsSyncAdapter);
1814                mSyncToNetwork |= !callerIsSyncAdapter;
1815                break;
1816            }
1817
1818            case GROUPS: {
1819                final Account account = readAccountFromQueryParams(uri);
1820                id = insertGroup(values, account, callerIsSyncAdapter);
1821                mSyncToNetwork |= !callerIsSyncAdapter;
1822                break;
1823            }
1824
1825            case SETTINGS: {
1826                id = insertSettings(values);
1827                break;
1828            }
1829
1830            case PRESENCE: {
1831                id = insertPresence(values);
1832                break;
1833            }
1834
1835            default:
1836                mSyncToNetwork = true;
1837                return mLegacyApiSupport.insert(uri, values);
1838        }
1839
1840        if (id < 0) {
1841            return null;
1842        }
1843
1844        return ContentUris.withAppendedId(uri, id);
1845    }
1846
1847    /**
1848     * If account is non-null then store it in the values. If the account is already
1849     * specified in the values then it must be consistent with the account, if it is non-null.
1850     * @param values the ContentValues to read from and update
1851     * @param account the explicitly provided Account
1852     * @return false if the accounts are inconsistent
1853     */
1854    private boolean resolveAccount(ContentValues values, Account account) {
1855        // If either is specified then both must be specified.
1856        final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
1857        final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
1858        if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
1859            final Account valuesAccount = new Account(accountName, accountType);
1860            if (account != null && !valuesAccount.equals(account)) {
1861                return false;
1862            }
1863            account = valuesAccount;
1864        }
1865        if (account != null) {
1866            values.put(RawContacts.ACCOUNT_NAME, account.name);
1867            values.put(RawContacts.ACCOUNT_TYPE, account.type);
1868        }
1869        return true;
1870    }
1871
1872    /**
1873     * Inserts an item in the contacts table
1874     *
1875     * @param values the values for the new row
1876     * @return the row ID of the newly created row
1877     */
1878    private long insertContact(ContentValues values) {
1879        throw new UnsupportedOperationException("Aggregate contacts are created automatically");
1880    }
1881
1882    /**
1883     * Inserts an item in the contacts table
1884     *
1885     * @param values the values for the new row
1886     * @param account the account this contact should be associated with. may be null.
1887     * @return the row ID of the newly created row
1888     */
1889    private long insertRawContact(ContentValues values, Account account) {
1890        ContentValues overriddenValues = new ContentValues(values);
1891        overriddenValues.putNull(RawContacts.CONTACT_ID);
1892        if (!resolveAccount(overriddenValues, account)) {
1893            return -1;
1894        }
1895
1896        if (values.containsKey(RawContacts.DELETED)
1897                && values.getAsInteger(RawContacts.DELETED) != 0) {
1898            overriddenValues.put(RawContacts.AGGREGATION_MODE,
1899                    RawContacts.AGGREGATION_MODE_DISABLED);
1900        }
1901
1902        long rawContactId =
1903                mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
1904        mContactAggregator.markNewForAggregation(rawContactId);
1905
1906        // Trigger creation of a Contact based on this RawContact at the end of transaction
1907        mInsertedRawContacts.add(rawContactId);
1908        return rawContactId;
1909    }
1910
1911    /**
1912     * Inserts an item in the data table
1913     *
1914     * @param values the values for the new row
1915     * @return the row ID of the newly created row
1916     */
1917    private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
1918        long id = 0;
1919        mValues.clear();
1920        mValues.putAll(values);
1921
1922        long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
1923
1924        // Replace package with internal mapping
1925        final String packageName = mValues.getAsString(Data.RES_PACKAGE);
1926        if (packageName != null) {
1927            mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1928        }
1929        mValues.remove(Data.RES_PACKAGE);
1930
1931        // Replace mimetype with internal mapping
1932        final String mimeType = mValues.getAsString(Data.MIMETYPE);
1933        if (TextUtils.isEmpty(mimeType)) {
1934            throw new IllegalArgumentException(Data.MIMETYPE + " is required");
1935        }
1936
1937        mValues.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
1938        mValues.remove(Data.MIMETYPE);
1939
1940        DataRowHandler rowHandler = getDataRowHandler(mimeType);
1941        id = rowHandler.insert(mDb, rawContactId, mValues);
1942        if (!callerIsSyncAdapter) {
1943            setRawContactDirty(rawContactId);
1944        }
1945        mUpdatedRawContacts.add(rawContactId);
1946
1947        if (rowHandler.isAggregationRequired()) {
1948            triggerAggregation(rawContactId);
1949        }
1950        return id;
1951    }
1952
1953    private void triggerAggregation(long rawContactId) {
1954        if (!mContactAggregator.isEnabled()) {
1955            return;
1956        }
1957
1958        int aggregationMode = mOpenHelper.getAggregationMode(rawContactId);
1959        switch (aggregationMode) {
1960            case RawContacts.AGGREGATION_MODE_DISABLED:
1961                break;
1962
1963            case RawContacts.AGGREGATION_MODE_DEFAULT: {
1964                mContactAggregator.markForAggregation(rawContactId);
1965                mScheduleAggregation = true;
1966                break;
1967            }
1968
1969            case RawContacts.AGGREGATION_MODE_SUSPENDED: {
1970                long contactId = mOpenHelper.getContactId(rawContactId);
1971
1972                if (contactId != 0) {
1973                    mContactAggregator.updateAggregateData(contactId);
1974                }
1975                break;
1976            }
1977
1978            case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
1979                long contactId = mOpenHelper.getContactId(rawContactId);
1980                mContactAggregator.aggregateContact(mDb, rawContactId, contactId);
1981                break;
1982            }
1983        }
1984    }
1985
1986    /**
1987     * Returns the group id of the group with sourceId and the same account as rawContactId.
1988     * If the group doesn't already exist then it is first created,
1989     * @param db SQLiteDatabase to use for this operation
1990     * @param rawContactId the contact this group is associated with
1991     * @param sourceId the sourceIf of the group to query or create
1992     * @return the group id of the existing or created group
1993     * @throws IllegalArgumentException if the contact is not associated with an account
1994     * @throws IllegalStateException if a group needs to be created but the creation failed
1995     */
1996    private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) {
1997        Account account = null;
1998        Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "="
1999                + rawContactId, null, null, null, null);
2000        try {
2001            if (c.moveToNext()) {
2002                final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
2003                final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
2004                if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
2005                    account = new Account(accountName, accountType);
2006                }
2007            }
2008        } finally {
2009            c.close();
2010        }
2011        if (account == null) {
2012            throw new IllegalArgumentException("if the groupmembership only "
2013                    + "has a sourceid the the contact must be associate with "
2014                    + "an account");
2015        }
2016
2017        // look up the group that contains this sourceId and has the same account name and type
2018        // as the contact refered to by rawContactId
2019        c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
2020                Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
2021                new String[]{sourceId, account.name, account.type}, null, null, null);
2022        try {
2023            if (c.moveToNext()) {
2024                return c.getLong(0);
2025            } else {
2026                ContentValues groupValues = new ContentValues();
2027                groupValues.put(Groups.ACCOUNT_NAME, account.name);
2028                groupValues.put(Groups.ACCOUNT_TYPE, account.type);
2029                groupValues.put(Groups.SOURCE_ID, sourceId);
2030                long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
2031                if (groupId < 0) {
2032                    throw new IllegalStateException("unable to create a new group with "
2033                            + "this sourceid: " + groupValues);
2034                }
2035                return groupId;
2036            }
2037        } finally {
2038            c.close();
2039        }
2040    }
2041
2042    /**
2043     * Delete data row by row so that fixing of primaries etc work correctly.
2044     */
2045    private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
2046        int count = 0;
2047
2048        // Note that the query will return data according to the access restrictions,
2049        // so we don't need to worry about deleting data we don't have permission to read.
2050        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null);
2051        try {
2052            while(c.moveToNext()) {
2053                long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
2054                String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
2055                DataRowHandler rowHandler = getDataRowHandler(mimeType);
2056                count += rowHandler.delete(mDb, c);
2057                if (!callerIsSyncAdapter) {
2058                    setRawContactDirty(rawContactId);
2059                    if (rowHandler.isAggregationRequired()) {
2060                        triggerAggregation(rawContactId);
2061                    }
2062                }
2063            }
2064        } finally {
2065            c.close();
2066        }
2067
2068        return count;
2069    }
2070
2071    /**
2072     * Delete a data row provided that it is one of the allowed mime types.
2073     */
2074    public int deleteData(long dataId, String[] allowedMimeTypes) {
2075
2076        // Note that the query will return data according to the access restrictions,
2077        // so we don't need to worry about deleting data we don't have permission to read.
2078        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=" + dataId, null,
2079                null);
2080
2081        try {
2082            if (!c.moveToFirst()) {
2083                return 0;
2084            }
2085
2086            String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
2087            boolean valid = false;
2088            for (int i = 0; i < allowedMimeTypes.length; i++) {
2089                if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
2090                    valid = true;
2091                    break;
2092                }
2093            }
2094
2095            if (!valid) {
2096                throw new IllegalArgumentException("Data type mismatch: expected "
2097                        + Lists.newArrayList(allowedMimeTypes));
2098            }
2099
2100            DataRowHandler rowHandler = getDataRowHandler(mimeType);
2101            int count = rowHandler.delete(mDb, c);
2102            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
2103            if (rowHandler.isAggregationRequired()) {
2104                triggerAggregation(rawContactId);
2105            }
2106            return count;
2107        } finally {
2108            c.close();
2109        }
2110    }
2111
2112    /**
2113     * Inserts an item in the groups table
2114     */
2115    private long insertGroup(ContentValues values, Account account, boolean callerIsSyncAdapter) {
2116        ContentValues overriddenValues = new ContentValues(values);
2117        if (!resolveAccount(overriddenValues, account)) {
2118            return -1;
2119        }
2120
2121        // Replace package with internal mapping
2122        final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
2123        if (packageName != null) {
2124            overriddenValues.put(GroupsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
2125        }
2126        overriddenValues.remove(Groups.RES_PACKAGE);
2127
2128        if (!callerIsSyncAdapter) {
2129            overriddenValues.put(Groups.DIRTY, 1);
2130        }
2131
2132        long result = mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
2133
2134        if (overriddenValues.containsKey(Groups.GROUP_VISIBLE)) {
2135            mOpenHelper.updateAllVisible();
2136        }
2137
2138        return result;
2139    }
2140
2141    private long insertSettings(ContentValues values) {
2142        final long id = mDb.insert(Tables.SETTINGS, null, values);
2143        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
2144            mOpenHelper.updateAllVisible();
2145        }
2146        return id;
2147    }
2148
2149    /**
2150     * Inserts a presence update.
2151     */
2152    public long insertPresence(ContentValues values) {
2153        final String handle = values.getAsString(Presence.IM_HANDLE);
2154        if (TextUtils.isEmpty(handle) || !values.containsKey(Presence.PROTOCOL)) {
2155            throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
2156        }
2157
2158        final long protocol = values.getAsLong(Presence.PROTOCOL);
2159        String customProtocol = null;
2160
2161        if (protocol == Im.PROTOCOL_CUSTOM) {
2162            customProtocol = values.getAsString(Presence.CUSTOM_PROTOCOL);
2163            if (TextUtils.isEmpty(customProtocol)) {
2164                throw new IllegalArgumentException(
2165                        "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
2166            }
2167        }
2168
2169        // TODO: generalize to allow other providers to match against email
2170        boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
2171
2172        StringBuilder selection = new StringBuilder();
2173        String[] selectionArgs;
2174        if (matchEmail) {
2175            selection.append(
2176                    "((" + MimetypesColumns.MIMETYPE + "='" + Im.CONTENT_ITEM_TYPE + "'"
2177                    + " AND " + Im.PROTOCOL + "=?"
2178                    + " AND " + Im.DATA + "=?");
2179            if (customProtocol != null) {
2180                selection.append(" AND " + Im.CUSTOM_PROTOCOL + "=");
2181                DatabaseUtils.appendEscapedSQLString(selection, customProtocol);
2182            }
2183            selection.append(") OR ("
2184                    + MimetypesColumns.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "'"
2185                    + " AND " + Email.DATA + "=?"
2186                    + "))");
2187            selectionArgs = new String[] { String.valueOf(protocol), handle, handle };
2188        } else {
2189            selection.append(
2190                    MimetypesColumns.MIMETYPE + "='" + Im.CONTENT_ITEM_TYPE + "'"
2191                    + " AND " + Im.PROTOCOL + "=?"
2192                    + " AND " + Im.DATA + "=?");
2193            if (customProtocol != null) {
2194                selection.append(" AND " + Im.CUSTOM_PROTOCOL + "=");
2195                DatabaseUtils.appendEscapedSQLString(selection, customProtocol);
2196            }
2197
2198            selectionArgs = new String[] { String.valueOf(protocol), handle };
2199        }
2200
2201        if (values.containsKey(Presence.DATA_ID)) {
2202            selection.append(" AND " + DataColumns.CONCRETE_ID + "=")
2203                    .append(values.getAsLong(Presence.DATA_ID));
2204        }
2205
2206        selection.append(" AND ").append(getContactsRestrictions());
2207
2208        long dataId = -1;
2209        long rawContactId = -1;
2210        long contactId = -1;
2211
2212        Cursor cursor = null;
2213        try {
2214            cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
2215                    selection.toString(), selectionArgs, null, null, null);
2216            if (cursor.moveToFirst()) {
2217                dataId = cursor.getLong(DataContactsQuery.DATA_ID);
2218                rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
2219                contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
2220            } else {
2221                // No contact found, return a null URI
2222                return -1;
2223            }
2224        } finally {
2225            if (cursor != null) {
2226                cursor.close();
2227            }
2228        }
2229
2230        values.put(Presence.DATA_ID, dataId);
2231        values.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
2232
2233        // Insert the presence update
2234        long presenceId = mDb.replace(Tables.PRESENCE, null, values);
2235
2236        if (contactId != -1) {
2237            if (values.containsKey(Presence.PRESENCE_STATUS)) {
2238                mAggregatedPresenceReplace.bindLong(1, contactId);
2239                mAggregatedPresenceReplace.bindLong(2, contactId);
2240                mAggregatedPresenceReplace.execute();
2241            }
2242            String status = values.getAsString(Presence.PRESENCE_CUSTOM_STATUS);
2243            if (status != null) {
2244                mAggregatedPresenceStatusUpdate.bindString(1, status);
2245                mAggregatedPresenceStatusUpdate.bindLong(2, contactId);
2246                mAggregatedPresenceStatusUpdate.execute();
2247            }
2248        }
2249        return presenceId;
2250    }
2251
2252    @Override
2253    protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
2254        if (VERBOSE_LOGGING) {
2255            Log.v(TAG, "deleteInTransaction: " + uri);
2256        }
2257        flushTransactionalChanges();
2258        final boolean callerIsSyncAdapter =
2259                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2260        final int match = sUriMatcher.match(uri);
2261        switch (match) {
2262            case SYNCSTATE:
2263                return mOpenHelper.getSyncState().delete(mDb, selection, selectionArgs);
2264
2265            case SYNCSTATE_ID:
2266                String selectionWithId =
2267                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
2268                        + (selection == null ? "" : " AND (" + selection + ")");
2269                return mOpenHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs);
2270
2271            case CONTACTS: {
2272                // TODO
2273                return 0;
2274            }
2275
2276            case CONTACTS_ID: {
2277                long contactId = ContentUris.parseId(uri);
2278                return deleteContact(contactId);
2279            }
2280
2281            case CONTACTS_LOOKUP:
2282            case CONTACTS_LOOKUP_ID: {
2283                final List<String> pathSegments = uri.getPathSegments();
2284                final int segmentCount = pathSegments.size();
2285                if (segmentCount < 3) {
2286                    throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
2287                }
2288                final String lookupKey = pathSegments.get(2);
2289                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
2290                return deleteContact(contactId);
2291            }
2292
2293            case RAW_CONTACTS: {
2294                int numDeletes = 0;
2295                Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
2296                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
2297                try {
2298                    while (c.moveToNext()) {
2299                        final long rawContactId = c.getLong(0);
2300                        numDeletes += deleteRawContact(rawContactId, callerIsSyncAdapter);
2301                    }
2302                } finally {
2303                    c.close();
2304                }
2305                return numDeletes;
2306            }
2307
2308            case RAW_CONTACTS_ID: {
2309                final long rawContactId = ContentUris.parseId(uri);
2310                return deleteRawContact(rawContactId, callerIsSyncAdapter);
2311            }
2312
2313            case DATA: {
2314                mSyncToNetwork |= !callerIsSyncAdapter;
2315                return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
2316                        callerIsSyncAdapter);
2317            }
2318
2319            case DATA_ID: {
2320                long dataId = ContentUris.parseId(uri);
2321                mSyncToNetwork |= !callerIsSyncAdapter;
2322                return deleteData(Data._ID + "=" + dataId, null, callerIsSyncAdapter);
2323            }
2324
2325            case GROUPS_ID: {
2326                mSyncToNetwork |= !callerIsSyncAdapter;
2327                return deleteGroup(ContentUris.parseId(uri), callerIsSyncAdapter);
2328            }
2329
2330            case GROUPS: {
2331                int numDeletes = 0;
2332                Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
2333                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
2334                try {
2335                    while (c.moveToNext()) {
2336                        numDeletes += deleteGroup(c.getLong(0), callerIsSyncAdapter);
2337                    }
2338                } finally {
2339                    c.close();
2340                }
2341                if (numDeletes > 0) {
2342                    mSyncToNetwork |= !callerIsSyncAdapter;
2343                }
2344                return numDeletes;
2345            }
2346
2347            case SETTINGS: {
2348                return deleteSettings(selection, selectionArgs);
2349            }
2350
2351            case PRESENCE: {
2352                return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
2353            }
2354
2355            default: {
2356                mSyncToNetwork = true;
2357                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
2358            }
2359        }
2360    }
2361
2362    private boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) {
2363        final String flag = uri.getQueryParameter(name);
2364        return flag == null
2365                ? defaultValue
2366                : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
2367    }
2368
2369    private int deleteGroup(long groupId, boolean callerIsSyncAdapter) {
2370        final long groupMembershipMimetypeId = mOpenHelper
2371                .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
2372        mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
2373                + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
2374                + groupId, null);
2375
2376        try {
2377            if (callerIsSyncAdapter) {
2378                return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
2379            } else {
2380                mValues.clear();
2381                mValues.put(Groups.DELETED, 1);
2382                mValues.put(Groups.DIRTY, 1);
2383                return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
2384            }
2385        } finally {
2386            mOpenHelper.updateAllVisible();
2387        }
2388    }
2389
2390    private int deleteSettings(String selection, String[] selectionArgs) {
2391        final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs);
2392        if (count > 0) {
2393            mOpenHelper.updateAllVisible();
2394        }
2395        return count;
2396    }
2397
2398    private int deleteContact(long contactId) {
2399        Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
2400                RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
2401        try {
2402            while (c.moveToNext()) {
2403                long rawContactId = c.getLong(0);
2404                markRawContactAsDeleted(rawContactId);
2405            }
2406        } finally {
2407            c.close();
2408        }
2409
2410        return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
2411    }
2412
2413    public int deleteRawContact(long rawContactId, boolean callerIsSyncAdapter) {
2414        if (callerIsSyncAdapter) {
2415            mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
2416            return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
2417        } else {
2418            mOpenHelper.removeContactIfSingleton(rawContactId);
2419            return markRawContactAsDeleted(rawContactId);
2420        }
2421    }
2422
2423    private int markRawContactAsDeleted(long rawContactId) {
2424        mSyncToNetwork = true;
2425
2426        mValues.clear();
2427        mValues.put(RawContacts.DELETED, 1);
2428        mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
2429        mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
2430        mValues.putNull(RawContacts.CONTACT_ID);
2431        mValues.put(RawContacts.DIRTY, 1);
2432        return updateRawContact(rawContactId, mValues);
2433    }
2434
2435    private static Account readAccountFromQueryParams(Uri uri) {
2436        final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
2437        final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
2438        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
2439            return null;
2440        }
2441        return new Account(name, type);
2442    }
2443
2444    @Override
2445    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
2446            String[] selectionArgs) {
2447        if (VERBOSE_LOGGING) {
2448            Log.v(TAG, "updateInTransaction: " + uri);
2449        }
2450
2451        int count = 0;
2452
2453        final int match = sUriMatcher.match(uri);
2454        if (match == SYNCSTATE_ID && selection == null) {
2455            long rowId = ContentUris.parseId(uri);
2456            Object data = values.get(ContactsContract.SyncStateColumns.DATA);
2457            mUpdatedSyncStates.put(rowId, data);
2458            return 1;
2459        }
2460        flushTransactionalChanges();
2461        final boolean callerIsSyncAdapter =
2462                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2463        switch(match) {
2464            case SYNCSTATE:
2465                return mOpenHelper.getSyncState().update(mDb, values,
2466                        appendAccountToSelection(uri, selection), selectionArgs);
2467
2468            case SYNCSTATE_ID: {
2469                selection = appendAccountToSelection(uri, selection);
2470                String selectionWithId =
2471                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
2472                        + (selection == null ? "" : " AND (" + selection + ")");
2473                return mOpenHelper.getSyncState().update(mDb, values,
2474                        selectionWithId, selectionArgs);
2475            }
2476
2477            case CONTACTS: {
2478                count = updateContactOptions(values, selection, selectionArgs);
2479                break;
2480            }
2481
2482            case CONTACTS_ID: {
2483                count = updateContactOptions(ContentUris.parseId(uri), values);
2484                break;
2485            }
2486
2487            case CONTACTS_LOOKUP:
2488            case CONTACTS_LOOKUP_ID: {
2489                final List<String> pathSegments = uri.getPathSegments();
2490                final int segmentCount = pathSegments.size();
2491                if (segmentCount < 3) {
2492                    throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
2493                }
2494                final String lookupKey = pathSegments.get(2);
2495                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
2496                count = updateContactOptions(contactId, values);
2497                break;
2498            }
2499
2500            case DATA: {
2501                count = updateData(uri, values, appendAccountToSelection(uri, selection),
2502                        selectionArgs, callerIsSyncAdapter);
2503                if (count > 0) {
2504                    mSyncToNetwork |= !callerIsSyncAdapter;
2505                }
2506                break;
2507            }
2508
2509            case DATA_ID: {
2510                count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
2511                if (count > 0) {
2512                    mSyncToNetwork |= !callerIsSyncAdapter;
2513                }
2514                break;
2515            }
2516
2517            case RAW_CONTACTS: {
2518                count = updateRawContacts(values, selection, selectionArgs);
2519                break;
2520            }
2521
2522            case RAW_CONTACTS_ID: {
2523                long rawContactId = ContentUris.parseId(uri);
2524                if (selection != null) {
2525                    count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId
2526                                    + " AND(" + selection + ")", selectionArgs);
2527                } else {
2528                    count = updateRawContact(rawContactId, values);
2529                }
2530                break;
2531            }
2532
2533            case GROUPS: {
2534                count = updateGroups(values, appendAccountToSelection(uri, selection),
2535                        selectionArgs, callerIsSyncAdapter);
2536                if (count > 0) {
2537                    mSyncToNetwork |= !callerIsSyncAdapter;
2538                }
2539                break;
2540            }
2541
2542            case GROUPS_ID: {
2543                long groupId = ContentUris.parseId(uri);
2544                String selectionWithId = (Groups._ID + "=" + groupId + " ")
2545                        + (selection == null ? "" : " AND " + selection);
2546                count = updateGroups(values, selectionWithId, selectionArgs, callerIsSyncAdapter);
2547                if (count > 0) {
2548                    mSyncToNetwork |= !callerIsSyncAdapter;
2549                }
2550                break;
2551            }
2552
2553            case AGGREGATION_EXCEPTIONS: {
2554                count = updateAggregationException(mDb, values);
2555                break;
2556            }
2557
2558            case SETTINGS: {
2559                count = updateSettings(values, selection, selectionArgs);
2560                break;
2561            }
2562
2563            default: {
2564                mSyncToNetwork = true;
2565                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
2566            }
2567        }
2568
2569        return count;
2570    }
2571
2572    private int updateGroups(ContentValues values, String selectionWithId,
2573            String[] selectionArgs, boolean callerIsSyncAdapter) {
2574
2575        ContentValues updatedValues;
2576        if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
2577            updatedValues = mValues;
2578            updatedValues.clear();
2579            updatedValues.putAll(values);
2580            updatedValues.put(Groups.DIRTY, 1);
2581        } else {
2582            updatedValues = values;
2583        }
2584
2585        int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs);
2586
2587        // If changing visibility, then update contacts
2588        if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
2589            mOpenHelper.updateAllVisible();
2590        }
2591        return count;
2592    }
2593
2594    private int updateSettings(ContentValues values, String selection, String[] selectionArgs) {
2595        final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
2596        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
2597            mOpenHelper.updateAllVisible();
2598        }
2599        return count;
2600    }
2601
2602    private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) {
2603        if (values.containsKey(RawContacts.CONTACT_ID)) {
2604            throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
2605                    "in content values. Contact IDs are assigned automatically");
2606        }
2607
2608        int count = 0;
2609        Cursor cursor = mDb.query(mOpenHelper.getRawContactView(),
2610                new String[] { RawContacts._ID }, selection,
2611                selectionArgs, null, null, null);
2612        try {
2613            while (cursor.moveToNext()) {
2614                long rawContactId = cursor.getLong(0);
2615                updateRawContact(rawContactId, values);
2616                count++;
2617            }
2618        } finally {
2619            cursor.close();
2620        }
2621
2622        return count;
2623    }
2624
2625    private int updateRawContact(long rawContactId, ContentValues values) {
2626        int count = mDb.update(Tables.RAW_CONTACTS, values, RawContacts._ID + " = " + rawContactId,
2627                null);
2628        if (count != 0) {
2629            if (values.containsKey(RawContacts.STARRED)) {
2630                mContactAggregator.updateStarred(rawContactId);
2631            }
2632            if (values.containsKey(RawContacts.SOURCE_ID)) {
2633                mContactAggregator.updateLookupKey(mDb, rawContactId);
2634            }
2635        }
2636        return count;
2637    }
2638
2639    private int updateData(Uri uri, ContentValues values, String selection,
2640            String[] selectionArgs, boolean callerIsSyncAdapter) {
2641        mValues.clear();
2642        mValues.putAll(values);
2643        mValues.remove(Data._ID);
2644        mValues.remove(Data.RAW_CONTACT_ID);
2645        mValues.remove(Data.MIMETYPE);
2646
2647        String packageName = values.getAsString(Data.RES_PACKAGE);
2648        if (packageName != null) {
2649            mValues.remove(Data.RES_PACKAGE);
2650            mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
2651        }
2652
2653        boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
2654        boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
2655
2656        // Remove primary or super primary values being set to 0. This is disallowed by the
2657        // content provider.
2658        if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
2659            containsIsSuperPrimary = false;
2660            mValues.remove(Data.IS_SUPER_PRIMARY);
2661        }
2662        if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
2663            containsIsPrimary = false;
2664            mValues.remove(Data.IS_PRIMARY);
2665        }
2666
2667        int count = 0;
2668
2669        // Note that the query will return data according to the access restrictions,
2670        // so we don't need to worry about updating data we don't have permission to read.
2671        Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null);
2672        try {
2673            while(c.moveToNext()) {
2674                count += updateData(mValues, c, callerIsSyncAdapter);
2675            }
2676        } finally {
2677            c.close();
2678        }
2679
2680        return count;
2681    }
2682
2683    private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
2684        if (values.size() == 0) {
2685            return 0;
2686        }
2687
2688        final String mimeType = c.getString(DataUpdateQuery.MIMETYPE);
2689        DataRowHandler rowHandler = getDataRowHandler(mimeType);
2690        rowHandler.update(mDb, values, c, callerIsSyncAdapter);
2691        long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
2692        if (rowHandler.isAggregationRequired()) {
2693            triggerAggregation(rawContactId);
2694        }
2695
2696        return 1;
2697    }
2698
2699    private int updateContactOptions(ContentValues values, String selection,
2700            String[] selectionArgs) {
2701        int count = 0;
2702        Cursor cursor = mDb.query(mOpenHelper.getContactView(),
2703                new String[] { Contacts._ID }, selection,
2704                selectionArgs, null, null, null);
2705        try {
2706            while (cursor.moveToNext()) {
2707                long contactId = cursor.getLong(0);
2708                updateContactOptions(contactId, values);
2709                count++;
2710            }
2711        } finally {
2712            cursor.close();
2713        }
2714
2715        return count;
2716    }
2717
2718    private int updateContactOptions(long contactId, ContentValues values) {
2719
2720        mValues.clear();
2721        OpenHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
2722                values, Contacts.CUSTOM_RINGTONE);
2723        OpenHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
2724                values, Contacts.SEND_TO_VOICEMAIL);
2725        OpenHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
2726                values, Contacts.LAST_TIME_CONTACTED);
2727        OpenHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
2728                values, Contacts.TIMES_CONTACTED);
2729        OpenHelper.copyLongValue(mValues, RawContacts.STARRED,
2730                values, Contacts.STARRED);
2731
2732        // Nothing to update - just return
2733        if (mValues.size() == 0) {
2734            return 0;
2735        }
2736
2737        if (mValues.containsKey(RawContacts.STARRED)) {
2738            // Mark dirty when changing starred to trigger sync
2739            mValues.put(RawContacts.DIRTY, 1);
2740        }
2741
2742        mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=" + contactId, null);
2743
2744        // Copy changeable values to prevent automatically managed fields from
2745        // being explicitly updated by clients.
2746        mValues.clear();
2747        OpenHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
2748                values, Contacts.CUSTOM_RINGTONE);
2749        OpenHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
2750                values, Contacts.SEND_TO_VOICEMAIL);
2751        OpenHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
2752                values, Contacts.LAST_TIME_CONTACTED);
2753        OpenHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
2754                values, Contacts.TIMES_CONTACTED);
2755        OpenHelper.copyLongValue(mValues, RawContacts.STARRED,
2756                values, Contacts.STARRED);
2757
2758        return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=" + contactId, null);
2759    }
2760
2761    public void updateContactTime(long contactId, long lastTimeContacted) {
2762        mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
2763        mLastTimeContactedUpdate.bindLong(2, contactId);
2764        mLastTimeContactedUpdate.execute();
2765    }
2766
2767    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
2768        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
2769        long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
2770        long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
2771
2772        long rawContactId1, rawContactId2;
2773        if (rcId1 < rcId2) {
2774            rawContactId1 = rcId1;
2775            rawContactId2 = rcId2;
2776        } else {
2777            rawContactId2 = rcId1;
2778            rawContactId1 = rcId2;
2779        }
2780
2781        if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
2782            db.delete(Tables.AGGREGATION_EXCEPTIONS,
2783                    AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND "
2784                    + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null);
2785        } else {
2786            ContentValues exceptionValues = new ContentValues(3);
2787            exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
2788            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
2789            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
2790            db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
2791                    exceptionValues);
2792        }
2793
2794        mContactAggregator.markForAggregation(rawContactId1);
2795        mContactAggregator.markForAggregation(rawContactId2);
2796
2797        long contactId1 = mOpenHelper.getContactId(rawContactId1);
2798        mContactAggregator.aggregateContact(db, rawContactId1, contactId1);
2799
2800        long contactId2 = mOpenHelper.getContactId(rawContactId2);
2801        mContactAggregator.aggregateContact(db, rawContactId2, contactId2);
2802
2803        // The return value is fake - we just confirm that we made a change, not count actual
2804        // rows changed.
2805        return 1;
2806    }
2807
2808    public void onAccountsUpdated(Account[] accounts) {
2809        mDb = mOpenHelper.getWritableDatabase();
2810        if (mDb == null) return;
2811
2812        Set<Account> validAccounts = Sets.newHashSet();
2813        for (Account account : accounts) {
2814            validAccounts.add(new Account(account.name, account.type));
2815        }
2816        ArrayList<Account> accountsToDelete = new ArrayList<Account>();
2817
2818        mDb.beginTransaction();
2819        try {
2820            // Find all the accounts the contacts DB knows about, mark the ones that aren't in the
2821            // valid set for deletion.
2822            Cursor c = mDb.rawQuery("SELECT DISTINCT account_name, account_type from "
2823                    + Tables.RAW_CONTACTS, null);
2824            while (c.moveToNext()) {
2825                if (c.getString(0) != null && c.getString(1) != null) {
2826                    Account currAccount = new Account(c.getString(0), c.getString(1));
2827                    if (!validAccounts.contains(currAccount)) {
2828                        accountsToDelete.add(currAccount);
2829                    }
2830                }
2831            }
2832            c.close();
2833
2834            for (Account account : accountsToDelete) {
2835                String[] params = new String[]{account.name, account.type};
2836                mDb.execSQL("DELETE FROM " + Tables.GROUPS
2837                        + " WHERE account_name = ? AND account_type = ?", params);
2838                mDb.execSQL("DELETE FROM " + Tables.PRESENCE
2839                        + " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (SELECT "
2840                        + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS
2841                        + " WHERE account_name = ? AND account_type = ?)", params);
2842                mDb.execSQL("DELETE FROM " + Tables.RAW_CONTACTS
2843                        + " WHERE account_name = ? AND account_type = ?", params);
2844            }
2845            mOpenHelper.getSyncState().onAccountsChanged(mDb, accounts);
2846            mDb.setTransactionSuccessful();
2847        } finally {
2848            mDb.endTransaction();
2849        }
2850    }
2851
2852    /**
2853     * Test all against {@link TextUtils#isEmpty(CharSequence)}.
2854     */
2855    private static boolean areAllEmpty(ContentValues values, String[] keys) {
2856        for (String key : keys) {
2857            if (!TextUtils.isEmpty(values.getAsString(key))) {
2858                return false;
2859            }
2860        }
2861        return true;
2862    }
2863
2864    @Override
2865    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
2866            String sortOrder) {
2867        if (VERBOSE_LOGGING) {
2868            Log.v(TAG, "query: " + uri);
2869        }
2870
2871        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2872
2873        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2874        String groupBy = null;
2875        String limit = getLimit(uri);
2876
2877        // TODO: Consider writing a test case for RestrictionExceptions when you
2878        // write a new query() block to make sure it protects restricted data.
2879        final int match = sUriMatcher.match(uri);
2880        switch (match) {
2881            case SYNCSTATE:
2882                return mOpenHelper.getSyncState().query(db, projection, selection,  selectionArgs,
2883                        sortOrder);
2884
2885            case CONTACTS: {
2886                setTablesAndProjectionMapForContacts(qb, projection);
2887                break;
2888            }
2889
2890            case CONTACTS_ID: {
2891                long contactId = ContentUris.parseId(uri);
2892                setTablesAndProjectionMapForContacts(qb, projection);
2893                qb.appendWhere(Contacts._ID + "=" + contactId);
2894                break;
2895            }
2896
2897            case CONTACTS_LOOKUP:
2898            case CONTACTS_LOOKUP_ID: {
2899                List<String> pathSegments = uri.getPathSegments();
2900                int segmentCount = pathSegments.size();
2901                if (segmentCount < 3) {
2902                    throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
2903                }
2904                String lookupKey = pathSegments.get(2);
2905                if (segmentCount == 4) {
2906                    long contactId = Long.parseLong(pathSegments.get(3));
2907                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
2908                    setTablesAndProjectionMapForContacts(lookupQb, projection);
2909                    lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " +
2910                            Contacts.LOOKUP_KEY + "=");
2911                    lookupQb.appendWhereEscapeString(lookupKey);
2912                    Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder,
2913                            groupBy, limit);
2914                    if (c.getCount() != 0) {
2915                        return c;
2916                    }
2917
2918                    c.close();
2919                }
2920
2921                setTablesAndProjectionMapForContacts(qb, projection);
2922                qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
2923                break;
2924            }
2925
2926            case CONTACTS_FILTER: {
2927                setTablesAndProjectionMapForContacts(qb, projection);
2928                if (uri.getPathSegments().size() > 2) {
2929                    String filterParam = uri.getLastPathSegment();
2930                    StringBuilder sb = new StringBuilder();
2931                    sb.append(Contacts._ID + " IN ");
2932                    appendContactFilterAsNestedQuery(sb, filterParam);
2933                    qb.appendWhere(sb.toString());
2934                }
2935                break;
2936            }
2937
2938            case CONTACTS_STREQUENT_FILTER:
2939            case CONTACTS_STREQUENT: {
2940                String filterSql = null;
2941                if (match == CONTACTS_STREQUENT_FILTER
2942                        && uri.getPathSegments().size() > 3) {
2943                    String filterParam = uri.getLastPathSegment();
2944                    StringBuilder sb = new StringBuilder();
2945                    sb.append(Contacts._ID + " IN ");
2946                    appendContactFilterAsNestedQuery(sb, filterParam);
2947                    filterSql = sb.toString();
2948                }
2949
2950                setTablesAndProjectionMapForContacts(qb, projection);
2951
2952                // Build the first query for starred
2953                if (filterSql != null) {
2954                    qb.appendWhere(filterSql);
2955                }
2956                final String starredQuery = qb.buildQuery(projection, Contacts.STARRED + "=1",
2957                        null, Contacts._ID, null, null, null);
2958
2959                // Build the second query for frequent
2960                qb = new SQLiteQueryBuilder();
2961                setTablesAndProjectionMapForContacts(qb, projection);
2962                if (filterSql != null) {
2963                    qb.appendWhere(filterSql);
2964                }
2965                final String frequentQuery = qb.buildQuery(projection,
2966                        Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
2967                        + " = 0 OR " + Contacts.STARRED + " IS NULL)",
2968                        null, Contacts._ID, null, null, null);
2969
2970                // Put them together
2971                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
2972                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
2973                Cursor c = db.rawQuery(query, null);
2974                if (c != null) {
2975                    c.setNotificationUri(getContext().getContentResolver(),
2976                            ContactsContract.AUTHORITY_URI);
2977                }
2978                return c;
2979            }
2980
2981            case CONTACTS_GROUP: {
2982                setTablesAndProjectionMapForContacts(qb, projection);
2983                if (uri.getPathSegments().size() > 2) {
2984                    qb.appendWhere(sContactsInGroupSelect);
2985                    selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
2986                }
2987                break;
2988            }
2989
2990            case CONTACTS_DATA: {
2991                long contactId = Long.parseLong(uri.getPathSegments().get(1));
2992
2993                qb.setTables(mOpenHelper.getDataView());
2994                qb.setProjectionMap(sDataProjectionMap);
2995                appendAccountFromParameter(qb, uri);
2996                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
2997                break;
2998            }
2999
3000            case CONTACTS_PHOTO: {
3001                long contactId = Long.parseLong(uri.getPathSegments().get(1));
3002
3003                qb.setTables(mOpenHelper.getDataView());
3004                qb.setProjectionMap(sDataProjectionMap);
3005                appendAccountFromParameter(qb, uri);
3006                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
3007                qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
3008                break;
3009            }
3010
3011            case PHONES: {
3012                qb.setTables(mOpenHelper.getDataView());
3013                qb.setProjectionMap(sDataProjectionMap);
3014                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
3015                break;
3016            }
3017
3018            case PHONES_FILTER: {
3019                qb.setTables(mOpenHelper.getDataView());
3020                qb.setProjectionMap(sDistinctDataProjectionMap);
3021                qb.appendWhere(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                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
3059                break;
3060            }
3061
3062            case EMAILS_LOOKUP: {
3063                qb.setTables(mOpenHelper.getDataView());
3064                qb.setProjectionMap(sDataProjectionMap);
3065                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
3066                if (uri.getPathSegments().size() > 2) {
3067                    qb.appendWhere(" AND " + Email.DATA + "=");
3068                    qb.appendWhereEscapeString(uri.getLastPathSegment());
3069                }
3070                break;
3071            }
3072
3073            case EMAILS_FILTER: {
3074                qb.setTables(mOpenHelper.getDataView());
3075                qb.setProjectionMap(sDistinctDataProjectionMap);
3076                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
3077                if (uri.getPathSegments().size() > 2) {
3078                    String filterParam = uri.getLastPathSegment();
3079                    StringBuilder sb = new StringBuilder();
3080                    sb.append("(");
3081
3082                    String normalizedName = NameNormalizer.normalize(filterParam);
3083                    if (normalizedName.length() > 0) {
3084                        sb.append(Data.RAW_CONTACT_ID + " IN ");
3085                        appendRawContactsByNormalizedNameFilter(sb, normalizedName, null);
3086                        sb.append(" OR ");
3087                    }
3088
3089                    sb.append(Email.DATA + " LIKE ");
3090                    sb.append(DatabaseUtils.sqlEscapeString(filterParam + '%'));
3091                    sb.append(")");
3092                    qb.appendWhere(" AND " + sb);
3093                }
3094                groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
3095                break;
3096            }
3097
3098            case POSTALS: {
3099                qb.setTables(mOpenHelper.getDataView());
3100                qb.setProjectionMap(sDataProjectionMap);
3101                qb.appendWhere(Data.MIMETYPE + " = '" + StructuredPostal.CONTENT_ITEM_TYPE + "'");
3102                break;
3103            }
3104
3105            case RAW_CONTACTS: {
3106                qb.setTables(mOpenHelper.getRawContactView());
3107                qb.setProjectionMap(sRawContactsProjectionMap);
3108                break;
3109            }
3110
3111            case RAW_CONTACTS_ID: {
3112                long rawContactId = ContentUris.parseId(uri);
3113                qb.setTables(mOpenHelper.getRawContactView());
3114                qb.setProjectionMap(sRawContactsProjectionMap);
3115                qb.appendWhere(RawContacts._ID + "=" + rawContactId);
3116                break;
3117            }
3118
3119            case RAW_CONTACTS_DATA: {
3120                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
3121                qb.setTables(mOpenHelper.getDataView());
3122                qb.setProjectionMap(sDataProjectionMap);
3123                qb.appendWhere(Data.RAW_CONTACT_ID + "=" + rawContactId);
3124                break;
3125            }
3126
3127            case DATA: {
3128                qb.setTables(mOpenHelper.getDataView());
3129                qb.setProjectionMap(sDataProjectionMap);
3130                appendAccountFromParameter(qb, uri);
3131                break;
3132            }
3133
3134            case DATA_ID: {
3135                qb.setTables(mOpenHelper.getDataView());
3136                qb.setProjectionMap(sDataProjectionMap);
3137                qb.appendWhere(Data._ID + "=" + ContentUris.parseId(uri));
3138                break;
3139            }
3140
3141            case DATA_WITH_PRESENCE: {
3142                qb.setTables(mOpenHelper.getDataView() + " data"
3143                        + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE
3144                        + " ON (" + AggregatedPresenceColumns.CONTACT_ID + "="
3145                                + RawContacts.CONTACT_ID + ")");
3146                qb.setProjectionMap(sDataWithPresenceProjectionMap);
3147                appendAccountFromParameter(qb, uri);
3148                break;
3149            }
3150
3151            case PHONE_LOOKUP: {
3152
3153                if (TextUtils.isEmpty(sortOrder)) {
3154                    // Default the sort order to something reasonable so we get consistent
3155                    // results when callers don't request an ordering
3156                    sortOrder = RawContactsColumns.CONCRETE_ID;
3157                }
3158
3159                String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
3160                mOpenHelper.buildPhoneLookupAndContactQuery(qb, number);
3161                qb.setProjectionMap(sPhoneLookupProjectionMap);
3162
3163                // Phone lookup cannot be combined with a selection
3164                selection = null;
3165                selectionArgs = null;
3166                break;
3167            }
3168
3169            case GROUPS: {
3170                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
3171                qb.setProjectionMap(sGroupsProjectionMap);
3172                break;
3173            }
3174
3175            case GROUPS_ID: {
3176                long groupId = ContentUris.parseId(uri);
3177                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
3178                qb.setProjectionMap(sGroupsProjectionMap);
3179                qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId);
3180                break;
3181            }
3182
3183            case GROUPS_SUMMARY: {
3184                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
3185                qb.setProjectionMap(sGroupsSummaryProjectionMap);
3186                groupBy = GroupsColumns.CONCRETE_ID;
3187                break;
3188            }
3189
3190            case AGGREGATION_EXCEPTIONS: {
3191                qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
3192                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
3193                break;
3194            }
3195
3196            case AGGREGATION_SUGGESTIONS: {
3197                long contactId = Long.parseLong(uri.getPathSegments().get(1));
3198                final int maxSuggestions;
3199                if (limit != null) {
3200                    maxSuggestions = Integer.parseInt(limit);
3201                } else {
3202                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
3203                }
3204
3205                setTablesAndProjectionMapForContacts(qb, projection);
3206
3207                return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId,
3208                        maxSuggestions);
3209            }
3210
3211            case SETTINGS: {
3212                qb.setTables(Tables.SETTINGS);
3213                qb.setProjectionMap(sSettingsProjectionMap);
3214
3215                // When requesting specific columns, this query requires
3216                // late-binding of the GroupMembership MIME-type.
3217                final String groupMembershipMimetypeId = Long.toString(mOpenHelper
3218                        .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
3219                if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) {
3220                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
3221                }
3222                if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) {
3223                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
3224                }
3225
3226                break;
3227            }
3228
3229            case PRESENCE: {
3230                qb.setTables(Tables.PRESENCE);
3231                qb.setProjectionMap(sPresenceProjectionMap);
3232                break;
3233            }
3234
3235            case PRESENCE_ID: {
3236                qb.setTables(Tables.PRESENCE);
3237                qb.setProjectionMap(sPresenceProjectionMap);
3238                qb.appendWhere(Presence._ID + "=" + ContentUris.parseId(uri));
3239                break;
3240            }
3241
3242            case SEARCH_SUGGESTIONS: {
3243                return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
3244            }
3245
3246            case SEARCH_SHORTCUT: {
3247                long contactId = ContentUris.parseId(uri);
3248                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection);
3249            }
3250
3251            case LIVE_FOLDERS_CONTACTS:
3252                qb.setTables(mOpenHelper.getContactView());
3253                qb.setProjectionMap(sLiveFoldersProjectionMap);
3254                break;
3255
3256            case LIVE_FOLDERS_CONTACTS_WITH_PHONES:
3257                qb.setTables(mOpenHelper.getContactView());
3258                qb.setProjectionMap(sLiveFoldersProjectionMap);
3259                qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1");
3260                break;
3261
3262            case LIVE_FOLDERS_CONTACTS_FAVORITES:
3263                qb.setTables(mOpenHelper.getContactView());
3264                qb.setProjectionMap(sLiveFoldersProjectionMap);
3265                qb.appendWhere(Contacts.STARRED + "=1");
3266                break;
3267
3268            case LIVE_FOLDERS_CONTACTS_GROUP_NAME:
3269                qb.setTables(mOpenHelper.getContactView());
3270                qb.setProjectionMap(sLiveFoldersProjectionMap);
3271                qb.appendWhere(sContactsInGroupSelect);
3272                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3273                break;
3274
3275            default:
3276                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
3277                        sortOrder, limit);
3278        }
3279
3280        return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
3281    }
3282
3283    private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
3284            String selection, String[] selectionArgs, String sortOrder, String groupBy,
3285            String limit) {
3286        if (projection != null && projection.length == 1
3287                && BaseColumns._COUNT.equals(projection[0])) {
3288            qb.setProjectionMap(sCountProjectionMap);
3289        }
3290        final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
3291                sortOrder, limit);
3292        if (c != null) {
3293            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
3294        }
3295        return c;
3296    }
3297
3298    private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
3299        ContactLookupKey key = new ContactLookupKey();
3300        ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
3301
3302        long contactId = lookupContactIdBySourceIds(db, segments);
3303        if (contactId == -1) {
3304            contactId = lookupContactIdByDisplayNames(db, segments);
3305        }
3306
3307        return contactId;
3308    }
3309
3310    private interface LookupBySourceIdQuery {
3311        String TABLE = Tables.RAW_CONTACTS;
3312
3313        String COLUMNS[] = {
3314                RawContacts.CONTACT_ID,
3315                RawContacts.ACCOUNT_TYPE,
3316                RawContacts.ACCOUNT_NAME,
3317                RawContacts.SOURCE_ID
3318        };
3319
3320        int CONTACT_ID = 0;
3321        int ACCOUNT_TYPE = 1;
3322        int ACCOUNT_NAME = 2;
3323        int SOURCE_ID = 3;
3324    }
3325
3326    private long lookupContactIdBySourceIds(SQLiteDatabase db,
3327                ArrayList<LookupKeySegment> segments) {
3328        int sourceIdCount = 0;
3329        for (int i = 0; i < segments.size(); i++) {
3330            LookupKeySegment segment = segments.get(i);
3331            if (segment.sourceIdLookup) {
3332                sourceIdCount++;
3333            }
3334        }
3335
3336        if (sourceIdCount == 0) {
3337            return -1;
3338        }
3339
3340        // First try sync ids
3341        StringBuilder sb = new StringBuilder();
3342        sb.append(RawContacts.SOURCE_ID + " IN (");
3343        for (int i = 0; i < segments.size(); i++) {
3344            LookupKeySegment segment = segments.get(i);
3345            if (segment.sourceIdLookup) {
3346                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
3347                sb.append(",");
3348            }
3349        }
3350        sb.setLength(sb.length() - 1);      // Last comma
3351        sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
3352
3353        Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
3354                 sb.toString(), null, null, null, null);
3355        try {
3356            while (c.moveToNext()) {
3357                String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE);
3358                String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
3359                int accountHashCode =
3360                        ContactLookupKey.getAccountHashCode(accountType, accountName);
3361                String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
3362                for (int i = 0; i < segments.size(); i++) {
3363                    LookupKeySegment segment = segments.get(i);
3364                    if (segment.sourceIdLookup && accountHashCode == segment.accountHashCode
3365                            && segment.key.equals(sourceId)) {
3366                        segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
3367                        break;
3368                    }
3369                }
3370            }
3371        } finally {
3372            c.close();
3373        }
3374
3375        return getMostReferencedContactId(segments);
3376    }
3377
3378    private interface LookupByDisplayNameQuery {
3379        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
3380
3381        String COLUMNS[] = {
3382                RawContacts.CONTACT_ID,
3383                RawContacts.ACCOUNT_TYPE,
3384                RawContacts.ACCOUNT_NAME,
3385                NameLookupColumns.NORMALIZED_NAME
3386        };
3387
3388        int CONTACT_ID = 0;
3389        int ACCOUNT_TYPE = 1;
3390        int ACCOUNT_NAME = 2;
3391        int NORMALIZED_NAME = 3;
3392    }
3393
3394    private long lookupContactIdByDisplayNames(SQLiteDatabase db,
3395                ArrayList<LookupKeySegment> segments) {
3396        int displayNameCount = 0;
3397        for (int i = 0; i < segments.size(); i++) {
3398            LookupKeySegment segment = segments.get(i);
3399            if (!segment.sourceIdLookup) {
3400                displayNameCount++;
3401            }
3402        }
3403
3404        if (displayNameCount == 0) {
3405            return -1;
3406        }
3407
3408        // First try sync ids
3409        StringBuilder sb = new StringBuilder();
3410        sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
3411        for (int i = 0; i < segments.size(); i++) {
3412            LookupKeySegment segment = segments.get(i);
3413            if (!segment.sourceIdLookup) {
3414                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
3415                sb.append(",");
3416            }
3417        }
3418        sb.setLength(sb.length() - 1);      // Last comma
3419        sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
3420                + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
3421
3422        Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
3423                 sb.toString(), null, null, null, null);
3424        try {
3425            while (c.moveToNext()) {
3426                String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE);
3427                String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
3428                int accountHashCode =
3429                        ContactLookupKey.getAccountHashCode(accountType, accountName);
3430                String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
3431                for (int i = 0; i < segments.size(); i++) {
3432                    LookupKeySegment segment = segments.get(i);
3433                    if (!segment.sourceIdLookup && accountHashCode == segment.accountHashCode
3434                            && segment.key.equals(name)) {
3435                        segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
3436                        break;
3437                    }
3438                }
3439            }
3440        } finally {
3441            c.close();
3442        }
3443
3444        return getMostReferencedContactId(segments);
3445    }
3446
3447    /**
3448     * Returns the contact ID that is mentioned the highest number of times.
3449     */
3450    private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
3451        Collections.sort(segments);
3452
3453        long bestContactId = -1;
3454        int bestRefCount = 0;
3455
3456        long contactId = -1;
3457        int count = 0;
3458
3459        int segmentCount = segments.size();
3460        for (int i = 0; i < segmentCount; i++) {
3461            LookupKeySegment segment = segments.get(i);
3462            if (segment.contactId != -1) {
3463                if (segment.contactId == contactId) {
3464                    count++;
3465                } else {
3466                    if (count > bestRefCount) {
3467                        bestContactId = contactId;
3468                        bestRefCount = count;
3469                    }
3470                    contactId = segment.contactId;
3471                    count = 1;
3472                }
3473            }
3474        }
3475        if (count > bestRefCount) {
3476            return contactId;
3477        } else {
3478            return bestContactId;
3479        }
3480    }
3481
3482    private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, String[] projection) {
3483        String contactView = mOpenHelper.getContactView();
3484        boolean needsPresence = mOpenHelper.isInProjection(projection, Contacts.PRESENCE_STATUS,
3485                Contacts.PRESENCE_CUSTOM_STATUS);
3486        if (!needsPresence) {
3487            qb.setTables(contactView);
3488            qb.setProjectionMap(sContactsProjectionMap);
3489        } else {
3490            qb.setTables(contactView + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + " ON ("
3491                    + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ") ");
3492            qb.setProjectionMap(sContactsWithPresenceProjectionMap);
3493
3494        }
3495    }
3496
3497    private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
3498        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
3499        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
3500        if (!TextUtils.isEmpty(accountName)) {
3501            qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
3502                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3503                    + RawContacts.ACCOUNT_TYPE + "="
3504                    + DatabaseUtils.sqlEscapeString(accountType));
3505        } else {
3506            qb.appendWhere("1");
3507        }
3508    }
3509
3510    private String appendAccountToSelection(Uri uri, String selection) {
3511        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
3512        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
3513        if (!TextUtils.isEmpty(accountName)) {
3514            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
3515                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3516                    + RawContacts.ACCOUNT_TYPE + "="
3517                    + DatabaseUtils.sqlEscapeString(accountType));
3518            if (!TextUtils.isEmpty(selection)) {
3519                selectionSb.append(" AND (");
3520                selectionSb.append(selection);
3521                selectionSb.append(')');
3522            }
3523            return selectionSb.toString();
3524        } else {
3525            return selection;
3526        }
3527    }
3528
3529    /**
3530     * Gets the value of the "limit" URI query parameter.
3531     *
3532     * @return A string containing a non-negative integer, or <code>null</code> if
3533     *         the parameter is not set, or is set to an invalid value.
3534     */
3535    private String getLimit(Uri url) {
3536        String limitParam = url.getQueryParameter("limit");
3537        if (limitParam == null) {
3538            return null;
3539        }
3540        // make sure that the limit is a non-negative integer
3541        try {
3542            int l = Integer.parseInt(limitParam);
3543            if (l < 0) {
3544                Log.w(TAG, "Invalid limit parameter: " + limitParam);
3545                return null;
3546            }
3547            return String.valueOf(l);
3548        } catch (NumberFormatException ex) {
3549            Log.w(TAG, "Invalid limit parameter: " + limitParam);
3550            return null;
3551        }
3552    }
3553
3554    /**
3555     * Returns true if all the characters are meaningful as digits
3556     * in a phone number -- letters, digits, and a few punctuation marks.
3557     */
3558    private boolean isPhoneNumber(CharSequence cons) {
3559        int len = cons.length();
3560
3561        for (int i = 0; i < len; i++) {
3562            char c = cons.charAt(i);
3563
3564            if ((c >= '0') && (c <= '9')) {
3565                continue;
3566            }
3567            if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+')
3568                    || (c == '#') || (c == '*')) {
3569                continue;
3570            }
3571            if ((c >= 'A') && (c <= 'Z')) {
3572                continue;
3573            }
3574            if ((c >= 'a') && (c <= 'z')) {
3575                continue;
3576            }
3577
3578            return false;
3579        }
3580
3581        return true;
3582    }
3583
3584    String getContactsRestrictions() {
3585        if (mOpenHelper.hasRestrictedAccess()) {
3586            return "1";
3587        } else {
3588            return RawContacts.IS_RESTRICTED + "=0";
3589        }
3590    }
3591
3592    public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
3593        if (mOpenHelper.hasRestrictedAccess()) {
3594            return "1";
3595        } else {
3596            return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
3597                    + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
3598        }
3599    }
3600
3601    @Override
3602    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
3603        int match = sUriMatcher.match(uri);
3604        switch (match) {
3605            case CONTACTS_PHOTO: {
3606                if (!"r".equals(mode)) {
3607                    throw new FileNotFoundException("Mode " + mode + " not supported.");
3608                }
3609
3610                long contactId = Long.parseLong(uri.getPathSegments().get(1));
3611
3612                String sql =
3613                        "SELECT " + Photo.PHOTO + " FROM " + mOpenHelper.getDataView() +
3614                        " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID
3615                                + " AND " + RawContacts.CONTACT_ID + "=" + contactId;
3616                SQLiteDatabase db = mOpenHelper.getReadableDatabase();
3617                return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null);
3618            }
3619
3620            case CONTACTS_LOOKUP:
3621            case CONTACTS_LOOKUP_ID: {
3622                // TODO: optimize lookup when direct id provided
3623                final String lookupKey = uri.getPathSegments().get(2);
3624                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
3625                final String selection = RawContacts.CONTACT_ID + "=" + contactId;
3626
3627                // When opening a contact as file, we pass back contents as a
3628                // vCard-encoded stream. We build into a local buffer first,
3629                // then pipe into MemoryFile once the exact size is known.
3630                final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
3631                outputRawContactsAsVCard(localStream, selection, null);
3632                return buildAssetFileDescriptor(localStream);
3633            }
3634
3635            default:
3636                throw new FileNotFoundException("No file at: " + uri);
3637        }
3638    }
3639
3640    private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
3641    private static final String VCARD_TYPE_DEFAULT = "default";
3642
3643    /**
3644     * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the
3645     * contents of the given {@link ByteArrayOutputStream}.
3646     */
3647    private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
3648        AssetFileDescriptor fd = null;
3649        try {
3650            stream.flush();
3651
3652            final byte[] byteData = stream.toByteArray();
3653            final int size = byteData.length;
3654
3655            final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size);
3656            memoryFile.writeBytes(byteData, 0, 0, size);
3657            memoryFile.deactivate();
3658
3659            fd = AssetFileDescriptor.fromMemoryFile(memoryFile);
3660        } catch (IOException e) {
3661            Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString());
3662        }
3663        return fd;
3664    }
3665
3666    /**
3667     * Output {@link RawContacts} matching the requested selection in the vCard
3668     * format to the given {@link OutputStream}. This method returns silently if
3669     * any errors encountered.
3670     */
3671    private void outputRawContactsAsVCard(OutputStream stream, String selection,
3672            String[] selectionArgs) {
3673        final Context context = this.getContext();
3674        final VCardComposer composer = new VCardComposer(context, VCARD_TYPE_DEFAULT, false);
3675        composer.addHandler(composer.new HandlerForOutputStream(stream));
3676
3677        // TODO: enforce the callers security clause is used
3678        if (!composer.init(selection, selectionArgs))
3679            return;
3680
3681        while (!composer.isAfterLast()) {
3682            if (!composer.createOneEntry()) {
3683                Log.w(TAG, "Failed to output a contact.");
3684            }
3685        }
3686        composer.terminate();
3687    }
3688
3689    /**
3690     * An implementation of EntityIterator that joins the contacts and data tables
3691     * and consumes all the data rows for a contact in order to build the Entity for a contact.
3692     */
3693    private static class RawContactsEntityIterator implements EntityIterator {
3694        private final Cursor mEntityCursor;
3695        private volatile boolean mIsClosed;
3696
3697        private static final String[] DATA_KEYS = new String[]{
3698                Data.DATA1,
3699                Data.DATA2,
3700                Data.DATA3,
3701                Data.DATA4,
3702                Data.DATA5,
3703                Data.DATA6,
3704                Data.DATA7,
3705                Data.DATA8,
3706                Data.DATA9,
3707                Data.DATA10,
3708                Data.DATA11,
3709                Data.DATA12,
3710                Data.DATA13,
3711                Data.DATA14,
3712                Data.DATA15,
3713                Data.SYNC1,
3714                Data.SYNC2,
3715                Data.SYNC3,
3716                Data.SYNC4};
3717
3718        private static final String[] PROJECTION = new String[]{
3719                RawContacts.ACCOUNT_NAME,
3720                RawContacts.ACCOUNT_TYPE,
3721                RawContacts.SOURCE_ID,
3722                RawContacts.VERSION,
3723                RawContacts.DIRTY,
3724                Data._ID,
3725                Data.RES_PACKAGE,
3726                Data.MIMETYPE,
3727                Data.DATA1,
3728                Data.DATA2,
3729                Data.DATA3,
3730                Data.DATA4,
3731                Data.DATA5,
3732                Data.DATA6,
3733                Data.DATA7,
3734                Data.DATA8,
3735                Data.DATA9,
3736                Data.DATA10,
3737                Data.DATA11,
3738                Data.DATA12,
3739                Data.DATA13,
3740                Data.DATA14,
3741                Data.DATA15,
3742                Data.SYNC1,
3743                Data.SYNC2,
3744                Data.SYNC3,
3745                Data.SYNC4,
3746                Data.RAW_CONTACT_ID,
3747                Data.IS_PRIMARY,
3748                Data.IS_SUPER_PRIMARY,
3749                Data.DATA_VERSION,
3750                GroupMembership.GROUP_SOURCE_ID,
3751                RawContacts.SYNC1,
3752                RawContacts.SYNC2,
3753                RawContacts.SYNC3,
3754                RawContacts.SYNC4,
3755                RawContacts.DELETED,
3756                RawContacts.CONTACT_ID,
3757                RawContacts.STARRED};
3758
3759        private static final int COLUMN_ACCOUNT_NAME = 0;
3760        private static final int COLUMN_ACCOUNT_TYPE = 1;
3761        private static final int COLUMN_SOURCE_ID = 2;
3762        private static final int COLUMN_VERSION = 3;
3763        private static final int COLUMN_DIRTY = 4;
3764        private static final int COLUMN_DATA_ID = 5;
3765        private static final int COLUMN_RES_PACKAGE = 6;
3766        private static final int COLUMN_MIMETYPE = 7;
3767        private static final int COLUMN_DATA1 = 8;
3768        private static final int COLUMN_RAW_CONTACT_ID = 27;
3769        private static final int COLUMN_IS_PRIMARY = 28;
3770        private static final int COLUMN_IS_SUPER_PRIMARY = 29;
3771        private static final int COLUMN_DATA_VERSION = 30;
3772        private static final int COLUMN_GROUP_SOURCE_ID = 31;
3773        private static final int COLUMN_SYNC1 = 32;
3774        private static final int COLUMN_SYNC2 = 33;
3775        private static final int COLUMN_SYNC3 = 34;
3776        private static final int COLUMN_SYNC4 = 35;
3777        private static final int COLUMN_DELETED = 36;
3778        private static final int COLUMN_CONTACT_ID = 37;
3779        private static final int COLUMN_STARRED = 38;
3780
3781        public RawContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri,
3782                String selection, String[] selectionArgs, String sortOrder) {
3783            mIsClosed = false;
3784
3785            final String updatedSortOrder = (sortOrder == null)
3786                    ? Data.RAW_CONTACT_ID
3787                    : (Data.RAW_CONTACT_ID + "," + sortOrder);
3788
3789            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
3790            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
3791            qb.setTables(Tables.CONTACT_ENTITIES);
3792            if (contactsIdString != null) {
3793                qb.appendWhere(Data.RAW_CONTACT_ID + "=" + contactsIdString);
3794            }
3795            final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
3796            final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
3797            if (!TextUtils.isEmpty(accountName)) {
3798                qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
3799                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3800                        + RawContacts.ACCOUNT_TYPE + "="
3801                        + DatabaseUtils.sqlEscapeString(accountType));
3802            }
3803            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
3804                    null, null, updatedSortOrder);
3805            mEntityCursor.moveToFirst();
3806        }
3807
3808        public void reset() throws RemoteException {
3809            if (mIsClosed) {
3810                throw new IllegalStateException("calling reset() when the iterator is closed");
3811            }
3812            mEntityCursor.moveToFirst();
3813        }
3814
3815        public void close() {
3816            if (mIsClosed) {
3817                throw new IllegalStateException("closing when already closed");
3818            }
3819            mIsClosed = true;
3820            mEntityCursor.close();
3821        }
3822
3823        public boolean hasNext() throws RemoteException {
3824            if (mIsClosed) {
3825                throw new IllegalStateException("calling hasNext() when the iterator is closed");
3826            }
3827
3828            return !mEntityCursor.isAfterLast();
3829        }
3830
3831        public Entity next() throws RemoteException {
3832            if (mIsClosed) {
3833                throw new IllegalStateException("calling next() when the iterator is closed");
3834            }
3835            if (!hasNext()) {
3836                throw new IllegalStateException("you may only call next() if hasNext() is true");
3837            }
3838
3839            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
3840
3841            final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID);
3842
3843            // we expect the cursor is already at the row we need to read from
3844            ContentValues contactValues = new ContentValues();
3845            contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
3846            contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
3847            contactValues.put(RawContacts._ID, rawContactId);
3848            contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY));
3849            contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION));
3850            contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
3851            contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1));
3852            contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2));
3853            contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3));
3854            contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4));
3855            contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED));
3856            contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID));
3857            contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED));
3858            Entity contact = new Entity(contactValues);
3859
3860            // read data rows until the contact id changes
3861            do {
3862                if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) {
3863                    break;
3864                }
3865                // add the data to to the contact
3866                ContentValues dataValues = new ContentValues();
3867                dataValues.put(Data._ID, c.getString(COLUMN_DATA_ID));
3868                dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
3869                dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
3870                dataValues.put(Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY));
3871                dataValues.put(Data.IS_SUPER_PRIMARY, c.getString(COLUMN_IS_SUPER_PRIMARY));
3872                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
3873                if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
3874                    dataValues.put(GroupMembership.GROUP_SOURCE_ID,
3875                            c.getString(COLUMN_GROUP_SOURCE_ID));
3876                }
3877                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
3878                for (int i = 0; i < DATA_KEYS.length; i++) {
3879                    final int columnIndex = i + COLUMN_DATA1;
3880                    String key = DATA_KEYS[i];
3881                    if (c.isNull(columnIndex)) {
3882                        // don't put anything
3883                    } else if (c.isLong(columnIndex)) {
3884                        dataValues.put(key, c.getLong(columnIndex));
3885                    } else if (c.isFloat(columnIndex)) {
3886                        dataValues.put(key, c.getFloat(columnIndex));
3887                    } else if (c.isString(columnIndex)) {
3888                        dataValues.put(key, c.getString(columnIndex));
3889                    } else if (c.isBlob(columnIndex)) {
3890                        dataValues.put(key, c.getBlob(columnIndex));
3891                    }
3892                }
3893                contact.addSubValue(Data.CONTENT_URI, dataValues);
3894            } while (mEntityCursor.moveToNext());
3895
3896            return contact;
3897        }
3898    }
3899
3900    /**
3901     * An implementation of EntityIterator that joins the contacts and data tables
3902     * and consumes all the data rows for a contact in order to build the Entity for a contact.
3903     */
3904    private static class GroupsEntityIterator implements EntityIterator {
3905        private final Cursor mEntityCursor;
3906        private volatile boolean mIsClosed;
3907
3908        private static final String[] PROJECTION = new String[]{
3909                Groups._ID,
3910                Groups.ACCOUNT_NAME,
3911                Groups.ACCOUNT_TYPE,
3912                Groups.SOURCE_ID,
3913                Groups.DIRTY,
3914                Groups.VERSION,
3915                Groups.RES_PACKAGE,
3916                Groups.TITLE,
3917                Groups.TITLE_RES,
3918                Groups.GROUP_VISIBLE,
3919                Groups.SYNC1,
3920                Groups.SYNC2,
3921                Groups.SYNC3,
3922                Groups.SYNC4,
3923                Groups.SYSTEM_ID,
3924                Groups.NOTES,
3925                Groups.DELETED};
3926
3927        private static final int COLUMN_ID = 0;
3928        private static final int COLUMN_ACCOUNT_NAME = 1;
3929        private static final int COLUMN_ACCOUNT_TYPE = 2;
3930        private static final int COLUMN_SOURCE_ID = 3;
3931        private static final int COLUMN_DIRTY = 4;
3932        private static final int COLUMN_VERSION = 5;
3933        private static final int COLUMN_RES_PACKAGE = 6;
3934        private static final int COLUMN_TITLE = 7;
3935        private static final int COLUMN_TITLE_RES = 8;
3936        private static final int COLUMN_GROUP_VISIBLE = 9;
3937        private static final int COLUMN_SYNC1 = 10;
3938        private static final int COLUMN_SYNC2 = 11;
3939        private static final int COLUMN_SYNC3 = 12;
3940        private static final int COLUMN_SYNC4 = 13;
3941        private static final int COLUMN_SYSTEM_ID = 14;
3942        private static final int COLUMN_NOTES = 15;
3943        private static final int COLUMN_DELETED = 16;
3944
3945        public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri,
3946                String selection, String[] selectionArgs, String sortOrder) {
3947            mIsClosed = false;
3948
3949            final String updatedSortOrder = (sortOrder == null)
3950                    ? Groups._ID
3951                    : (Groups._ID + "," + sortOrder);
3952
3953            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
3954            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
3955            qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
3956            qb.setProjectionMap(sGroupsProjectionMap);
3957            if (groupIdString != null) {
3958                qb.appendWhere(Groups._ID + "=" + groupIdString);
3959            }
3960            final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
3961            final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
3962            if (!TextUtils.isEmpty(accountName)) {
3963                qb.appendWhere(Groups.ACCOUNT_NAME + "="
3964                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3965                        + Groups.ACCOUNT_TYPE + "="
3966                        + DatabaseUtils.sqlEscapeString(accountType));
3967            }
3968            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
3969                    null, null, updatedSortOrder);
3970            mEntityCursor.moveToFirst();
3971        }
3972
3973        public void close() {
3974            if (mIsClosed) {
3975                throw new IllegalStateException("closing when already closed");
3976            }
3977            mIsClosed = true;
3978            mEntityCursor.close();
3979        }
3980
3981        public boolean hasNext() throws RemoteException {
3982            if (mIsClosed) {
3983                throw new IllegalStateException("calling hasNext() when the iterator is closed");
3984            }
3985
3986            return !mEntityCursor.isAfterLast();
3987        }
3988
3989        public void reset() throws RemoteException {
3990            if (mIsClosed) {
3991                throw new IllegalStateException("calling reset() when the iterator is closed");
3992            }
3993            mEntityCursor.moveToFirst();
3994        }
3995
3996        public Entity next() throws RemoteException {
3997            if (mIsClosed) {
3998                throw new IllegalStateException("calling next() when the iterator is closed");
3999            }
4000            if (!hasNext()) {
4001                throw new IllegalStateException("you may only call next() if hasNext() is true");
4002            }
4003
4004            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
4005
4006            final long groupId = c.getLong(COLUMN_ID);
4007
4008            // we expect the cursor is already at the row we need to read from
4009            ContentValues groupValues = new ContentValues();
4010            groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
4011            groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
4012            groupValues.put(Groups._ID, groupId);
4013            groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY));
4014            groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION));
4015            groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
4016            groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
4017            groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE));
4018            groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES));
4019            groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE));
4020            groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1));
4021            groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2));
4022            groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3));
4023            groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4));
4024            groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID));
4025            groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED));
4026            groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES));
4027            Entity group = new Entity(groupValues);
4028
4029            mEntityCursor.moveToNext();
4030
4031            return group;
4032        }
4033    }
4034
4035    @Override
4036    public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
4037            String sortOrder) {
4038        waitForAccess();
4039
4040        final int match = sUriMatcher.match(uri);
4041        switch (match) {
4042            case RAW_CONTACTS:
4043            case RAW_CONTACTS_ID:
4044                String contactsIdString = null;
4045                if (match == RAW_CONTACTS_ID) {
4046                    contactsIdString = uri.getPathSegments().get(1);
4047                }
4048
4049                return new RawContactsEntityIterator(this, contactsIdString,
4050                        uri, selection, selectionArgs, sortOrder);
4051            case GROUPS:
4052            case GROUPS_ID:
4053                String idString = null;
4054                if (match == GROUPS_ID) {
4055                    idString = uri.getPathSegments().get(1);
4056                }
4057
4058                return new GroupsEntityIterator(this, idString,
4059                        uri, selection, selectionArgs, sortOrder);
4060            default:
4061                throw new UnsupportedOperationException("Unknown uri: " + uri);
4062        }
4063    }
4064
4065    @Override
4066    public String getType(Uri uri) {
4067        final int match = sUriMatcher.match(uri);
4068        switch (match) {
4069            case CONTACTS:
4070            case CONTACTS_LOOKUP:
4071                return Contacts.CONTENT_TYPE;
4072            case CONTACTS_ID:
4073            case CONTACTS_LOOKUP_ID:
4074                return Contacts.CONTENT_ITEM_TYPE;
4075            case RAW_CONTACTS:
4076                return RawContacts.CONTENT_TYPE;
4077            case RAW_CONTACTS_ID:
4078                return RawContacts.CONTENT_ITEM_TYPE;
4079            case DATA_ID:
4080                return mOpenHelper.getDataMimeType(ContentUris.parseId(uri));
4081            case AGGREGATION_EXCEPTIONS:
4082                return AggregationExceptions.CONTENT_TYPE;
4083            case AGGREGATION_EXCEPTION_ID:
4084                return AggregationExceptions.CONTENT_ITEM_TYPE;
4085            case SETTINGS:
4086                return Settings.CONTENT_TYPE;
4087            case AGGREGATION_SUGGESTIONS:
4088                return Contacts.CONTENT_TYPE;
4089            case SEARCH_SUGGESTIONS:
4090                return SearchManager.SUGGEST_MIME_TYPE;
4091            case SEARCH_SHORTCUT:
4092                return SearchManager.SHORTCUT_MIME_TYPE;
4093            default:
4094                return mLegacyApiSupport.getType(uri);
4095        }
4096    }
4097
4098    private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) {
4099        if (displayName != null) {
4100            mRawContactDisplayNameUpdate.bindString(1, displayName);
4101        } else {
4102            mRawContactDisplayNameUpdate.bindNull(1);
4103        }
4104        mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource);
4105        mRawContactDisplayNameUpdate.bindLong(3, rawContactId);
4106        mRawContactDisplayNameUpdate.execute();
4107    }
4108
4109    /**
4110     * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
4111     */
4112    private void setRawContactDirty(long rawContactId) {
4113        mRawContactDirtyUpdate.bindLong(1, rawContactId);
4114        mRawContactDirtyUpdate.execute();
4115    }
4116
4117    /*
4118     * Sets the given dataId record in the "data" table to primary, and resets all data records of
4119     * the same mimetype and under the same contact to not be primary.
4120     *
4121     * @param dataId the id of the data record to be set to primary.
4122     */
4123    private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
4124        mSetPrimaryStatement.bindLong(1, dataId);
4125        mSetPrimaryStatement.bindLong(2, mimeTypeId);
4126        mSetPrimaryStatement.bindLong(3, rawContactId);
4127        mSetPrimaryStatement.execute();
4128    }
4129
4130    /*
4131     * Sets the given dataId record in the "data" table to "super primary", and resets all data
4132     * records of the same mimetype and under the same aggregate to not be "super primary".
4133     *
4134     * @param dataId the id of the data record to be set to primary.
4135     */
4136    private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
4137        mSetSuperPrimaryStatement.bindLong(1, dataId);
4138        mSetSuperPrimaryStatement.bindLong(2, mimeTypeId);
4139        mSetSuperPrimaryStatement.bindLong(3, rawContactId);
4140        mSetSuperPrimaryStatement.execute();
4141    }
4142
4143    private void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
4144        sb.append("(SELECT DISTINCT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS
4145                + " JOIN name_lookup ON(" + RawContactsColumns.CONCRETE_ID + "=raw_contact_id)"
4146                + " WHERE normalized_name GLOB '");
4147        sb.append(NameNormalizer.normalize(filterParam));
4148        sb.append("*')");
4149    }
4150
4151    public String getRawContactsByFilterAsNestedQuery(String filterParam) {
4152        StringBuilder sb = new StringBuilder();
4153        appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
4154        return sb.toString();
4155    }
4156
4157    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
4158            String limit) {
4159        appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit);
4160    }
4161
4162    private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
4163            String limit) {
4164        sb.append("(SELECT DISTINCT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '");
4165        sb.append(normalizedName);
4166        sb.append("*'");
4167        if (limit != null) {
4168            sb.append(" LIMIT ").append(limit);
4169        }
4170        sb.append(")");
4171    }
4172
4173    /**
4174     * Inserts an argument at the beginning of the selection arg list.
4175     */
4176    private String[] insertSelectionArg(String[] selectionArgs, String arg) {
4177        if (selectionArgs == null) {
4178            return new String[] {arg};
4179        } else {
4180            int newLength = selectionArgs.length + 1;
4181            String[] newSelectionArgs = new String[newLength];
4182            newSelectionArgs[0] = arg;
4183            System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
4184            return newSelectionArgs;
4185        }
4186    }
4187
4188    protected Account getDefaultAccount() {
4189        AccountManager accountManager = AccountManager.get(getContext());
4190        try {
4191            Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE,
4192                    new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult();
4193            if (accounts != null && accounts.length > 0) {
4194                return accounts[0];
4195            }
4196        } catch (Throwable e) {
4197            Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
4198        }
4199        return null;
4200    }
4201}
4202