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