ContactsProvider2.java revision cb144e1429596701603c016f4a078f6331e6481d
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: {
2250                // TODO
2251                return 0;
2252            }
2253
2254            case CONTACTS_ID: {
2255                long contactId = ContentUris.parseId(uri);
2256                return deleteContact(contactId);
2257            }
2258
2259            case RAW_CONTACTS: {
2260                final boolean permanently =
2261                        readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false);
2262                int numDeletes = 0;
2263                Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
2264                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
2265                try {
2266                    while (c.moveToNext()) {
2267                        final long rawContactId = c.getLong(0);
2268                        numDeletes += deleteRawContact(rawContactId, permanently);
2269                    }
2270                } finally {
2271                    c.close();
2272                }
2273                return numDeletes;
2274            }
2275
2276            case RAW_CONTACTS_ID: {
2277                final boolean permanently =
2278                        readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false);
2279                final long rawContactId = ContentUris.parseId(uri);
2280                return deleteRawContact(rawContactId, permanently);
2281            }
2282
2283            case DATA: {
2284                return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
2285                        shouldMarkRawContactAsDirty(uri));
2286            }
2287
2288            case DATA_ID: {
2289                long dataId = ContentUris.parseId(uri);
2290                return deleteData(Data._ID + "=" + dataId, null, shouldMarkRawContactAsDirty(uri));
2291            }
2292
2293            case GROUPS_ID: {
2294                boolean markAsDirty = shouldMarkGroupAsDirty(uri);
2295                final boolean deletePermanently =
2296                        readBooleanQueryParameter(uri, Groups.DELETE_PERMANENTLY, false);
2297                return deleteGroup(ContentUris.parseId(uri), markAsDirty, deletePermanently);
2298            }
2299
2300            case GROUPS: {
2301                boolean markAsDirty = shouldMarkGroupAsDirty(uri);
2302                final boolean permanently =
2303                        readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false);
2304                int numDeletes = 0;
2305                Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
2306                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
2307                try {
2308                    while (c.moveToNext()) {
2309                        numDeletes += deleteGroup(c.getLong(0), markAsDirty, permanently);
2310                    }
2311                } finally {
2312                    c.close();
2313                }
2314                return numDeletes;
2315            }
2316
2317            case SETTINGS: {
2318                return deleteSettings(selection, selectionArgs);
2319            }
2320
2321            case PRESENCE: {
2322                return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
2323            }
2324
2325            default:
2326                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
2327        }
2328    }
2329
2330    private boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) {
2331        final String flag = uri.getQueryParameter(name);
2332        return flag == null
2333                ? defaultValue
2334                : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
2335    }
2336
2337    private int deleteGroup(long groupId, boolean markAsDirty, boolean permanently) {
2338        final long groupMembershipMimetypeId = mOpenHelper
2339                .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
2340        mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
2341                + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
2342                + groupId, null);
2343
2344        try {
2345            if (permanently) {
2346                return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
2347            } else {
2348                mValues.clear();
2349                mValues.put(Groups.DELETED, 1);
2350                if (markAsDirty) {
2351                    mValues.put(Groups.DIRTY, 1);
2352                }
2353                return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
2354            }
2355        } finally {
2356            mOpenHelper.updateAllVisible();
2357        }
2358    }
2359
2360    private int deleteSettings(String selection, String[] selectionArgs) {
2361        final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs);
2362        if (count > 0) {
2363            mOpenHelper.updateAllVisible();
2364        }
2365        return count;
2366    }
2367
2368    private int deleteContact(long contactId) {
2369        Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
2370                RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
2371        try {
2372            while (c.moveToNext()) {
2373                long rawContactId = c.getLong(0);
2374                markRawContactAsDeleted(rawContactId);
2375            }
2376        } finally {
2377            c.close();
2378        }
2379
2380        return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
2381    }
2382
2383    public int deleteRawContact(long rawContactId, boolean permanently) {
2384        if (permanently) {
2385            mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
2386            return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
2387        } else {
2388            mOpenHelper.removeContactIfSingleton(rawContactId);
2389            return markRawContactAsDeleted(rawContactId);
2390        }
2391    }
2392
2393    private int markRawContactAsDeleted(long rawContactId) {
2394        mValues.clear();
2395        mValues.put(RawContacts.DELETED, 1);
2396        mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
2397        mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
2398        mValues.putNull(RawContacts.CONTACT_ID);
2399        mValues.put(RawContacts.DIRTY, 1);
2400        return updateRawContact(rawContactId, mValues);
2401    }
2402
2403    private static Account readAccountFromQueryParams(Uri uri) {
2404        final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
2405        final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
2406        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
2407            return null;
2408        }
2409        return new Account(name, type);
2410    }
2411
2412    @Override
2413    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
2414            String[] selectionArgs) {
2415        if (Log.isLoggable(TAG, Log.VERBOSE)) {
2416            Log.v(TAG, "updateInTransaction: " + uri);
2417        }
2418
2419        int count = 0;
2420
2421        final int match = sUriMatcher.match(uri);
2422        if (match == SYNCSTATE_ID && selection == null) {
2423            long rowId = ContentUris.parseId(uri);
2424            Object data = values.get(ContactsContract.SyncStateColumns.DATA);
2425            mUpdatedSyncStates.put(rowId, data);
2426            return 1;
2427        }
2428        flushTransactionalChanges();
2429        switch(match) {
2430            case SYNCSTATE:
2431                return mOpenHelper.getSyncState().update(mDb, values,
2432                        appendAccountToSelection(uri, selection), selectionArgs);
2433
2434            case SYNCSTATE_ID: {
2435                selection = appendAccountToSelection(uri, selection);
2436                String selectionWithId =
2437                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
2438                        + (selection == null ? "" : " AND (" + selection + ")");
2439                return mOpenHelper.getSyncState().update(mDb, values,
2440                        selectionWithId, selectionArgs);
2441            }
2442
2443            // TODO(emillar): We will want to disallow editing the contacts table at some point.
2444            case CONTACTS: {
2445                count = mDb.update(Tables.CONTACTS, values,
2446                        appendAccountToSelection(uri, selection), selectionArgs);
2447                break;
2448            }
2449
2450            case CONTACTS_ID: {
2451                count = updateContactData(ContentUris.parseId(uri), values);
2452                break;
2453            }
2454
2455            case DATA: {
2456                count = updateData(uri, values, appendAccountToSelection(uri, selection),
2457                        selectionArgs, shouldMarkRawContactAsDirty(uri));
2458                break;
2459            }
2460
2461            case DATA_ID: {
2462                count = updateData(uri, values, selection, selectionArgs,
2463                        shouldMarkRawContactAsDirty(uri));
2464                break;
2465            }
2466
2467            case RAW_CONTACTS: {
2468                count = updateRawContacts(values, selection, selectionArgs);
2469                break;
2470            }
2471
2472            case RAW_CONTACTS_ID: {
2473                long rawContactId = ContentUris.parseId(uri);
2474                if (selection != null) {
2475                    count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId
2476                                    + " AND(" + selection + ")", selectionArgs);
2477                } else {
2478                    count = updateRawContact(rawContactId, values);
2479                }
2480                break;
2481            }
2482
2483            case GROUPS: {
2484                count = updateGroups(values, appendAccountToSelection(uri, selection),
2485                        selectionArgs, shouldMarkGroupAsDirty(uri));
2486                break;
2487            }
2488
2489            case GROUPS_ID: {
2490                long groupId = ContentUris.parseId(uri);
2491                String selectionWithId = (Groups._ID + "=" + groupId + " ")
2492                        + (selection == null ? "" : " AND " + selection);
2493                count = updateGroups(values, selectionWithId, selectionArgs,
2494                        shouldMarkGroupAsDirty(uri));
2495                break;
2496            }
2497
2498            case AGGREGATION_EXCEPTIONS: {
2499                count = updateAggregationException(mDb, values);
2500                break;
2501            }
2502
2503            case SETTINGS: {
2504                count = updateSettings(values, selection, selectionArgs);
2505                break;
2506            }
2507
2508            default:
2509                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
2510        }
2511
2512        return count;
2513    }
2514
2515    private int updateGroups(ContentValues values, String selectionWithId,
2516            String[] selectionArgs, boolean markAsDirty) {
2517
2518        ContentValues updatedValues;
2519        if (markAsDirty && !values.containsKey(Groups.DIRTY)) {
2520            updatedValues = mValues;
2521            updatedValues.clear();
2522            updatedValues.putAll(values);
2523            updatedValues.put(Groups.DIRTY, 1);
2524        } else {
2525            updatedValues = values;
2526        }
2527
2528        int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs);
2529
2530        // If changing visibility, then update contacts
2531        if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
2532            mOpenHelper.updateAllVisible();
2533        }
2534        return count;
2535    }
2536
2537    private int updateSettings(ContentValues values, String selection, String[] selectionArgs) {
2538        final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
2539        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
2540            mOpenHelper.updateAllVisible();
2541        }
2542        return count;
2543    }
2544
2545    private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) {
2546        if (values.containsKey(RawContacts.CONTACT_ID)) {
2547            throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
2548                    "in content values. Contact IDs are assigned automatically");
2549        }
2550
2551        int count = 0;
2552        Cursor cursor = mDb.query(mOpenHelper.getRawContactView(),
2553                new String[] { RawContacts.CONTACT_ID }, selection,
2554                selectionArgs, null, null, null);
2555        try {
2556            while (cursor.moveToNext()) {
2557                long rawContactId = cursor.getLong(0);
2558                updateRawContact(rawContactId, values);
2559                count++;
2560            }
2561        } finally {
2562            cursor.close();
2563        }
2564
2565        return count;
2566    }
2567
2568    private int updateRawContact(long rawContactId, ContentValues values) {
2569        int count = mDb.update(Tables.RAW_CONTACTS, values, RawContacts._ID + " = " + rawContactId,
2570                null);
2571        if (count != 0) {
2572            if (values.containsKey(RawContacts.ACCOUNT_TYPE)
2573                    || values.containsKey(RawContacts.ACCOUNT_NAME)
2574                    || values.containsKey(RawContacts.SOURCE_ID)) {
2575                triggerAggregation(rawContactId);
2576            }
2577
2578            if (values.containsKey(RawContacts.STARRED)) {
2579                mContactAggregator.updateStarred(rawContactId);
2580            }
2581            if (values.containsKey(RawContacts.SOURCE_ID)) {
2582                mContactAggregator.updateLookupKey(mDb, rawContactId);
2583            }
2584        }
2585        return count;
2586    }
2587
2588    private int updateData(Uri uri, ContentValues values, String selection,
2589            String[] selectionArgs, boolean markRawContactAsDirty) {
2590        mValues.clear();
2591        mValues.putAll(values);
2592        mValues.remove(Data._ID);
2593        mValues.remove(Data.RAW_CONTACT_ID);
2594        mValues.remove(Data.MIMETYPE);
2595
2596        String packageName = values.getAsString(Data.RES_PACKAGE);
2597        if (packageName != null) {
2598            mValues.remove(Data.RES_PACKAGE);
2599            mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
2600        }
2601
2602        boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
2603        boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
2604
2605        // Remove primary or super primary values being set to 0. This is disallowed by the
2606        // content provider.
2607        if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
2608            containsIsSuperPrimary = false;
2609            mValues.remove(Data.IS_SUPER_PRIMARY);
2610        }
2611        if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
2612            containsIsPrimary = false;
2613            mValues.remove(Data.IS_PRIMARY);
2614        }
2615
2616        int count = 0;
2617
2618        // Note that the query will return data according to the access restrictions,
2619        // so we don't need to worry about updating data we don't have permission to read.
2620        Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null);
2621        try {
2622            while(c.moveToNext()) {
2623                count += updateData(mValues, c, markRawContactAsDirty);
2624            }
2625        } finally {
2626            c.close();
2627        }
2628
2629        return count;
2630    }
2631
2632    private int updateData(ContentValues values, Cursor c, boolean markRawContactAsDirty) {
2633        if (values.size() == 0) {
2634            return 0;
2635        }
2636
2637        final String mimeType = c.getString(DataUpdateQuery.MIMETYPE);
2638        DataRowHandler rowHandler = getDataRowHandler(mimeType);
2639        rowHandler.update(mDb, values, c, markRawContactAsDirty);
2640        long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
2641        if (rowHandler.isAggregationRequired()) {
2642            triggerAggregation(rawContactId);
2643        }
2644
2645        return 1;
2646    }
2647
2648    private int updateContactData(long contactId, ContentValues values) {
2649
2650        // First update all constituent contacts
2651        ContentValues optionValues = new ContentValues(5);
2652        OpenHelper.copyStringValue(optionValues, RawContacts.CUSTOM_RINGTONE,
2653                values, Contacts.CUSTOM_RINGTONE);
2654        OpenHelper.copyLongValue(optionValues, RawContacts.SEND_TO_VOICEMAIL,
2655                values, Contacts.SEND_TO_VOICEMAIL);
2656        OpenHelper.copyLongValue(optionValues, RawContacts.LAST_TIME_CONTACTED,
2657                values, Contacts.LAST_TIME_CONTACTED);
2658        OpenHelper.copyLongValue(optionValues, RawContacts.TIMES_CONTACTED,
2659                values, Contacts.TIMES_CONTACTED);
2660        OpenHelper.copyLongValue(optionValues, RawContacts.STARRED,
2661                values, Contacts.STARRED);
2662
2663        // Nothing to update - just return
2664        if (optionValues.size() == 0) {
2665            return 0;
2666        }
2667
2668        if (optionValues.containsKey(RawContacts.STARRED)) {
2669            // Mark dirty when changing starred to trigger sync
2670            optionValues.put(RawContacts.DIRTY, 1);
2671        }
2672
2673        mDb.update(Tables.RAW_CONTACTS, optionValues,
2674                RawContacts.CONTACT_ID + "=" + contactId, null);
2675        return mDb.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
2676    }
2677
2678    public void updateContactTime(long contactId, long lastTimeContacted) {
2679        mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
2680        mLastTimeContactedUpdate.bindLong(2, contactId);
2681        mLastTimeContactedUpdate.execute();
2682    }
2683
2684    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
2685        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
2686        long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
2687        long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
2688
2689        long rawContactId1, rawContactId2;
2690        if (rcId1 < rcId2) {
2691            rawContactId1 = rcId1;
2692            rawContactId2 = rcId2;
2693        } else {
2694            rawContactId2 = rcId1;
2695            rawContactId1 = rcId2;
2696        }
2697
2698        if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
2699            db.delete(Tables.AGGREGATION_EXCEPTIONS,
2700                    AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND "
2701                    + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null);
2702        } else {
2703            ContentValues exceptionValues = new ContentValues(3);
2704            exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
2705            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
2706            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
2707            db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
2708                    exceptionValues);
2709        }
2710
2711        long contactId1 = mOpenHelper.getContactId(rawContactId1);
2712        mContactAggregator.aggregateContact(db, rawContactId1, contactId1);
2713
2714        long contactId2 = mOpenHelper.getContactId(rawContactId2);
2715        mContactAggregator.aggregateContact(db, rawContactId2, contactId2);
2716
2717        // The return value is fake - we just confirm that we made a change, not count actual
2718        // rows changed.
2719        return 1;
2720    }
2721
2722    public void onAccountsUpdated(Account[] accounts) {
2723        mDb = mOpenHelper.getWritableDatabase();
2724        if (mDb == null) return;
2725
2726        Set<Account> validAccounts = Sets.newHashSet();
2727        for (Account account : accounts) {
2728            validAccounts.add(new Account(account.name, account.type));
2729        }
2730        ArrayList<Account> accountsToDelete = new ArrayList<Account>();
2731
2732        mDb.beginTransaction();
2733        try {
2734            // Find all the accounts the contacts DB knows about, mark the ones that aren't in the
2735            // valid set for deletion.
2736            Cursor c = mDb.rawQuery("SELECT DISTINCT account_name, account_type from "
2737                    + Tables.RAW_CONTACTS, null);
2738            while (c.moveToNext()) {
2739                if (c.getString(0) != null && c.getString(1) != null) {
2740                    Account currAccount = new Account(c.getString(0), c.getString(1));
2741                    if (!validAccounts.contains(currAccount)) {
2742                        accountsToDelete.add(currAccount);
2743                    }
2744                }
2745            }
2746            c.close();
2747
2748            for (Account account : accountsToDelete) {
2749                String[] params = new String[]{account.name, account.type};
2750                mDb.execSQL("DELETE FROM " + Tables.GROUPS
2751                        + " WHERE account_name = ? AND account_type = ?", params);
2752                mDb.execSQL("DELETE FROM " + Tables.PRESENCE
2753                        + " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (SELECT "
2754                        + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS
2755                        + " WHERE account_name = ? AND account_type = ?)", params);
2756                mDb.execSQL("DELETE FROM " + Tables.RAW_CONTACTS
2757                        + " WHERE account_name = ? AND account_type = ?", params);
2758            }
2759            mDb.setTransactionSuccessful();
2760        } finally {
2761            mDb.endTransaction();
2762        }
2763    }
2764
2765    /**
2766     * Test all against {@link TextUtils#isEmpty(CharSequence)}.
2767     */
2768    private static boolean areAllEmpty(ContentValues values, String[] keys) {
2769        for (String key : keys) {
2770            if (!TextUtils.isEmpty(values.getAsString(key))) {
2771                return false;
2772            }
2773        }
2774        return true;
2775    }
2776
2777    @Override
2778    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
2779            String sortOrder) {
2780
2781        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2782
2783        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2784        String groupBy = null;
2785        String limit = getLimit(uri);
2786
2787        // TODO: Consider writing a test case for RestrictionExceptions when you
2788        // write a new query() block to make sure it protects restricted data.
2789        final int match = sUriMatcher.match(uri);
2790        switch (match) {
2791            case SYNCSTATE:
2792                return mOpenHelper.getSyncState().query(db, projection, selection,  selectionArgs,
2793                        sortOrder);
2794
2795            case CONTACTS: {
2796                setTablesAndProjectionMapForContacts(qb, projection);
2797                break;
2798            }
2799
2800            case CONTACTS_ID: {
2801                long contactId = ContentUris.parseId(uri);
2802                setTablesAndProjectionMapForContacts(qb, projection);
2803                qb.appendWhere(Contacts._ID + "=" + contactId);
2804                break;
2805            }
2806
2807            case CONTACTS_LOOKUP:
2808            case CONTACTS_LOOKUP_ID: {
2809                List<String> pathSegments = uri.getPathSegments();
2810                int segmentCount = pathSegments.size();
2811                if (segmentCount < 3) {
2812                    throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
2813                }
2814                String lookupKey = pathSegments.get(2);
2815                if (segmentCount == 4) {
2816                    long contactId = Long.parseLong(pathSegments.get(3));
2817                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
2818                    setTablesAndProjectionMapForContacts(lookupQb, projection);
2819                    lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " +
2820                            Contacts.LOOKUP_KEY + "=");
2821                    lookupQb.appendWhereEscapeString(lookupKey);
2822                    Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder,
2823                            groupBy, limit);
2824                    if (c.getCount() != 0) {
2825                        return c;
2826                    }
2827
2828                    c.close();
2829                }
2830
2831                setTablesAndProjectionMapForContacts(qb, projection);
2832                qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
2833                break;
2834            }
2835
2836            case CONTACTS_FILTER: {
2837                setTablesAndProjectionMapForContacts(qb, projection);
2838                if (uri.getPathSegments().size() > 2) {
2839                    String filterParam = uri.getLastPathSegment();
2840                    StringBuilder sb = new StringBuilder();
2841                    sb.append(Contacts._ID + " IN ");
2842                    appendContactFilterAsNestedQuery(sb, filterParam);
2843                    qb.appendWhere(sb.toString());
2844                }
2845                break;
2846            }
2847
2848            case CONTACTS_STREQUENT_FILTER:
2849            case CONTACTS_STREQUENT: {
2850                String filterSql = null;
2851                if (match == CONTACTS_STREQUENT_FILTER
2852                        && uri.getPathSegments().size() > 3) {
2853                    String filterParam = uri.getLastPathSegment();
2854                    StringBuilder sb = new StringBuilder();
2855                    sb.append(Contacts._ID + " IN ");
2856                    appendContactFilterAsNestedQuery(sb, filterParam);
2857                    filterSql = sb.toString();
2858                }
2859
2860                setTablesAndProjectionMapForContacts(qb, projection);
2861
2862                // Build the first query for starred
2863                if (filterSql != null) {
2864                    qb.appendWhere(filterSql);
2865                }
2866                final String starredQuery = qb.buildQuery(projection, Contacts.STARRED + "=1",
2867                        null, Contacts._ID, null, null, null);
2868
2869                // Build the second query for frequent
2870                qb = new SQLiteQueryBuilder();
2871                setTablesAndProjectionMapForContacts(qb, projection);
2872                if (filterSql != null) {
2873                    qb.appendWhere(filterSql);
2874                }
2875                final String frequentQuery = qb.buildQuery(projection,
2876                        Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
2877                        + " = 0 OR " + Contacts.STARRED + " IS NULL)",
2878                        null, Contacts._ID, null, null, null);
2879
2880                // Put them together
2881                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
2882                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
2883                Cursor c = db.rawQuery(query, null);
2884                if (c != null) {
2885                    c.setNotificationUri(getContext().getContentResolver(),
2886                            ContactsContract.AUTHORITY_URI);
2887                }
2888                return c;
2889            }
2890
2891            case CONTACTS_GROUP: {
2892                setTablesAndProjectionMapForContacts(qb, projection);
2893                if (uri.getPathSegments().size() > 2) {
2894                    qb.appendWhere(sContactsInGroupSelect);
2895                    selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
2896                }
2897                break;
2898            }
2899
2900            case CONTACTS_DATA: {
2901                long contactId = Long.parseLong(uri.getPathSegments().get(1));
2902
2903                qb.setTables(mOpenHelper.getDataView());
2904                qb.setProjectionMap(sDataProjectionMap);
2905                appendAccountFromParameter(qb, uri);
2906                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
2907                break;
2908            }
2909
2910            case CONTACTS_PHOTO: {
2911                long contactId = Long.parseLong(uri.getPathSegments().get(1));
2912
2913                qb.setTables(mOpenHelper.getDataView());
2914                qb.setProjectionMap(sDataProjectionMap);
2915                appendAccountFromParameter(qb, uri);
2916                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
2917                qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
2918                break;
2919            }
2920
2921            case PHONES: {
2922                qb.setTables(mOpenHelper.getDataView());
2923                qb.setProjectionMap(sDataProjectionMap);
2924                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
2925                break;
2926            }
2927
2928            case PHONES_FILTER: {
2929                qb.setTables(mOpenHelper.getDataView());
2930                qb.setProjectionMap(sDistinctDataProjectionMap);
2931                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
2932                if (uri.getPathSegments().size() > 2) {
2933                    String filterParam = uri.getLastPathSegment();
2934                    StringBuilder sb = new StringBuilder();
2935                    sb.append("(");
2936
2937                    boolean orNeeded = false;
2938                    String normalizedName = NameNormalizer.normalize(filterParam);
2939                    if (normalizedName.length() > 0) {
2940                        sb.append(Data.RAW_CONTACT_ID + " IN ");
2941                        appendRawContactsByNormalizedNameFilter(sb, normalizedName, null);
2942                        orNeeded = true;
2943                    }
2944
2945                    if (isPhoneNumber(filterParam)) {
2946                        if (orNeeded) {
2947                            sb.append(" OR ");
2948                        }
2949                        String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam);
2950                        String reversed = PhoneNumberUtils.getStrippedReversed(number);
2951                        sb.append(Data._ID +
2952                                " IN (SELECT " + PhoneLookupColumns.DATA_ID
2953                                  + " FROM " + Tables.PHONE_LOOKUP
2954                                  + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%");
2955                        sb.append(reversed);
2956                        sb.append("')");
2957                    }
2958                    sb.append(")");
2959                    qb.appendWhere(" AND " + sb);
2960                }
2961                groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
2962                break;
2963            }
2964
2965            case EMAILS: {
2966                qb.setTables(mOpenHelper.getDataView());
2967                qb.setProjectionMap(sDataProjectionMap);
2968                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
2969                break;
2970            }
2971
2972            case EMAILS_LOOKUP: {
2973                qb.setTables(mOpenHelper.getDataView());
2974                qb.setProjectionMap(sDataProjectionMap);
2975                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
2976                if (uri.getPathSegments().size() > 2) {
2977                    qb.appendWhere(" AND " + Email.DATA + "=");
2978                    qb.appendWhereEscapeString(uri.getLastPathSegment());
2979                }
2980                break;
2981            }
2982
2983            case EMAILS_FILTER: {
2984                qb.setTables(mOpenHelper.getDataView());
2985                qb.setProjectionMap(sDistinctDataProjectionMap);
2986                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
2987                if (uri.getPathSegments().size() > 2) {
2988                    String filterParam = uri.getLastPathSegment();
2989                    StringBuilder sb = new StringBuilder();
2990                    sb.append("(");
2991
2992                    String normalizedName = NameNormalizer.normalize(filterParam);
2993                    if (normalizedName.length() > 0) {
2994                        sb.append(Data.RAW_CONTACT_ID + " IN ");
2995                        appendRawContactsByNormalizedNameFilter(sb, normalizedName, null);
2996                        sb.append(" OR ");
2997                    }
2998
2999                    sb.append(Email.DATA + " LIKE ");
3000                    sb.append(DatabaseUtils.sqlEscapeString(filterParam));
3001                    sb.append(")");
3002                    qb.appendWhere(" AND " + sb);
3003                }
3004                groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
3005                break;
3006            }
3007
3008            case POSTALS: {
3009                qb.setTables(mOpenHelper.getDataView());
3010                qb.setProjectionMap(sDataProjectionMap);
3011                qb.appendWhere(Data.MIMETYPE + " = '" + StructuredPostal.CONTENT_ITEM_TYPE + "'");
3012                break;
3013            }
3014
3015            case RAW_CONTACTS: {
3016                qb.setTables(mOpenHelper.getRawContactView());
3017                qb.setProjectionMap(sRawContactsProjectionMap);
3018                break;
3019            }
3020
3021            case RAW_CONTACTS_ID: {
3022                long rawContactId = ContentUris.parseId(uri);
3023                qb.setTables(mOpenHelper.getRawContactView());
3024                qb.setProjectionMap(sRawContactsProjectionMap);
3025                qb.appendWhere(RawContacts._ID + "=" + rawContactId);
3026                break;
3027            }
3028
3029            case RAW_CONTACTS_DATA: {
3030                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
3031                qb.setTables(mOpenHelper.getDataView());
3032                qb.setProjectionMap(sDataProjectionMap);
3033                qb.appendWhere(Data.RAW_CONTACT_ID + "=" + rawContactId);
3034                break;
3035            }
3036
3037            case DATA: {
3038                qb.setTables(mOpenHelper.getDataView());
3039                qb.setProjectionMap(sDataProjectionMap);
3040                appendAccountFromParameter(qb, uri);
3041                break;
3042            }
3043
3044            case DATA_ID: {
3045                qb.setTables(mOpenHelper.getDataView());
3046                qb.setProjectionMap(sDataProjectionMap);
3047                qb.appendWhere(Data._ID + "=" + ContentUris.parseId(uri));
3048                break;
3049            }
3050
3051            case DATA_WITH_PRESENCE: {
3052                qb.setTables(mOpenHelper.getDataView() + " data"
3053                        + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE
3054                        + " ON (" + AggregatedPresenceColumns.CONTACT_ID + "="
3055                                + RawContacts.CONTACT_ID + ")");
3056                qb.setProjectionMap(sDataWithPresenceProjectionMap);
3057                appendAccountFromParameter(qb, uri);
3058                break;
3059            }
3060
3061            case PHONE_LOOKUP: {
3062
3063                if (TextUtils.isEmpty(sortOrder)) {
3064                    // Default the sort order to something reasonable so we get consistent
3065                    // results when callers don't request an ordering
3066                    sortOrder = RawContactsColumns.CONCRETE_ID;
3067                }
3068
3069                String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
3070                mOpenHelper.buildPhoneLookupAndContactQuery(qb, number);
3071                qb.setProjectionMap(sPhoneLookupProjectionMap);
3072
3073                // Phone lookup cannot be combined with a selection
3074                selection = null;
3075                selectionArgs = null;
3076                break;
3077            }
3078
3079            case GROUPS: {
3080                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
3081                qb.setProjectionMap(sGroupsProjectionMap);
3082                break;
3083            }
3084
3085            case GROUPS_ID: {
3086                long groupId = ContentUris.parseId(uri);
3087                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
3088                qb.setProjectionMap(sGroupsProjectionMap);
3089                qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId);
3090                break;
3091            }
3092
3093            case GROUPS_SUMMARY: {
3094                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
3095                qb.setProjectionMap(sGroupsSummaryProjectionMap);
3096                groupBy = GroupsColumns.CONCRETE_ID;
3097                break;
3098            }
3099
3100            case AGGREGATION_EXCEPTIONS: {
3101                qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
3102                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
3103                break;
3104            }
3105
3106            case AGGREGATION_SUGGESTIONS: {
3107                long contactId = Long.parseLong(uri.getPathSegments().get(1));
3108                final int maxSuggestions;
3109                if (limit != null) {
3110                    maxSuggestions = Integer.parseInt(limit);
3111                } else {
3112                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
3113                }
3114
3115                return mContactAggregator.queryAggregationSuggestions(contactId, projection,
3116                        sContactsProjectionMap, maxSuggestions);
3117            }
3118
3119            case SETTINGS: {
3120                qb.setTables(Tables.SETTINGS);
3121                qb.setProjectionMap(sSettingsProjectionMap);
3122
3123                // When requesting specific columns, this query requires
3124                // late-binding of the GroupMembership MIME-type.
3125                final String groupMembershipMimetypeId = Long.toString(mOpenHelper
3126                        .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
3127                if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) {
3128                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
3129                }
3130                if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) {
3131                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
3132                }
3133
3134                break;
3135            }
3136
3137            case PRESENCE: {
3138                qb.setTables(Tables.PRESENCE);
3139                qb.setProjectionMap(sPresenceProjectionMap);
3140                break;
3141            }
3142
3143            case PRESENCE_ID: {
3144                qb.setTables(Tables.PRESENCE);
3145                qb.setProjectionMap(sPresenceProjectionMap);
3146                qb.appendWhere(Presence._ID + "=" + ContentUris.parseId(uri));
3147                break;
3148            }
3149
3150            case SEARCH_SUGGESTIONS: {
3151                return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
3152            }
3153
3154            case SEARCH_SHORTCUT: {
3155                long contactId = ContentUris.parseId(uri);
3156                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection);
3157            }
3158
3159            case LIVE_FOLDERS_CONTACTS:
3160                qb.setTables(mOpenHelper.getContactView());
3161                qb.setProjectionMap(sLiveFoldersProjectionMap);
3162                break;
3163
3164            case LIVE_FOLDERS_CONTACTS_WITH_PHONES:
3165                qb.setTables(mOpenHelper.getContactView());
3166                qb.setProjectionMap(sLiveFoldersProjectionMap);
3167                qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1");
3168                break;
3169
3170            case LIVE_FOLDERS_CONTACTS_FAVORITES:
3171                qb.setTables(mOpenHelper.getContactView());
3172                qb.setProjectionMap(sLiveFoldersProjectionMap);
3173                qb.appendWhere(Contacts.STARRED + "=1");
3174                break;
3175
3176            case LIVE_FOLDERS_CONTACTS_GROUP_NAME:
3177                qb.setTables(mOpenHelper.getContactView());
3178                qb.setProjectionMap(sLiveFoldersProjectionMap);
3179                qb.appendWhere(sContactsInGroupSelect);
3180                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3181                break;
3182
3183            default:
3184                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
3185                        sortOrder, limit);
3186        }
3187
3188        return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
3189    }
3190
3191    private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
3192            String selection, String[] selectionArgs, String sortOrder, String groupBy,
3193            String limit) {
3194        if (projection != null && projection.length == 1
3195                && BaseColumns._COUNT.equals(projection[0])) {
3196            qb.setProjectionMap(sCountProjectionMap);
3197        }
3198        final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
3199                sortOrder, limit);
3200        if (c != null) {
3201            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
3202        }
3203        return c;
3204    }
3205
3206    private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
3207        ContactLookupKey key = new ContactLookupKey();
3208        ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
3209
3210        long contactId = lookupContactIdBySourceIds(db, segments);
3211        if (contactId == -1) {
3212            contactId = lookupContactIdByDisplayNames(db, segments);
3213        }
3214
3215        return contactId;
3216    }
3217
3218    private interface LookupBySourceIdQuery {
3219        String TABLE = Tables.RAW_CONTACTS;
3220
3221        String COLUMNS[] = {
3222                RawContacts.CONTACT_ID,
3223                RawContacts.ACCOUNT_TYPE,
3224                RawContacts.ACCOUNT_NAME,
3225                RawContacts.SOURCE_ID
3226        };
3227
3228        int CONTACT_ID = 0;
3229        int ACCOUNT_TYPE = 1;
3230        int ACCOUNT_NAME = 2;
3231        int SOURCE_ID = 3;
3232    }
3233
3234    private long lookupContactIdBySourceIds(SQLiteDatabase db,
3235                ArrayList<LookupKeySegment> segments) {
3236        int sourceIdCount = 0;
3237        for (int i = 0; i < segments.size(); i++) {
3238            LookupKeySegment segment = segments.get(i);
3239            if (segment.sourceIdLookup) {
3240                sourceIdCount++;
3241            }
3242        }
3243
3244        if (sourceIdCount == 0) {
3245            return -1;
3246        }
3247
3248        // First try sync ids
3249        StringBuilder sb = new StringBuilder();
3250        sb.append(RawContacts.SOURCE_ID + " IN (");
3251        for (int i = 0; i < segments.size(); i++) {
3252            LookupKeySegment segment = segments.get(i);
3253            if (segment.sourceIdLookup) {
3254                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
3255                sb.append(",");
3256            }
3257        }
3258        sb.setLength(sb.length() - 1);      // Last comma
3259        sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
3260
3261        Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
3262                 sb.toString(), null, null, null, null);
3263        try {
3264            while (c.moveToNext()) {
3265                String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE);
3266                String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
3267                int accountHashCode =
3268                        ContactLookupKey.getAccountHashCode(accountType, accountName);
3269                String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
3270                for (int i = 0; i < segments.size(); i++) {
3271                    LookupKeySegment segment = segments.get(i);
3272                    if (segment.sourceIdLookup && accountHashCode == segment.accountHashCode
3273                            && segment.key.equals(sourceId)) {
3274                        segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
3275                        break;
3276                    }
3277                }
3278            }
3279        } finally {
3280            c.close();
3281        }
3282
3283        return getMostReferencedContactId(segments);
3284    }
3285
3286    private interface LookupByDisplayNameQuery {
3287        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
3288
3289        String COLUMNS[] = {
3290                RawContacts.CONTACT_ID,
3291                RawContacts.ACCOUNT_TYPE,
3292                RawContacts.ACCOUNT_NAME,
3293                NameLookupColumns.NORMALIZED_NAME
3294        };
3295
3296        int CONTACT_ID = 0;
3297        int ACCOUNT_TYPE = 1;
3298        int ACCOUNT_NAME = 2;
3299        int NORMALIZED_NAME = 3;
3300    }
3301
3302    private long lookupContactIdByDisplayNames(SQLiteDatabase db,
3303                ArrayList<LookupKeySegment> segments) {
3304        int displayNameCount = 0;
3305        for (int i = 0; i < segments.size(); i++) {
3306            LookupKeySegment segment = segments.get(i);
3307            if (!segment.sourceIdLookup) {
3308                displayNameCount++;
3309            }
3310        }
3311
3312        if (displayNameCount == 0) {
3313            return -1;
3314        }
3315
3316        // First try sync ids
3317        StringBuilder sb = new StringBuilder();
3318        sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
3319        for (int i = 0; i < segments.size(); i++) {
3320            LookupKeySegment segment = segments.get(i);
3321            if (!segment.sourceIdLookup) {
3322                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
3323                sb.append(",");
3324            }
3325        }
3326        sb.setLength(sb.length() - 1);      // Last comma
3327        sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
3328                + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
3329
3330        Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
3331                 sb.toString(), null, null, null, null);
3332        try {
3333            while (c.moveToNext()) {
3334                String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE);
3335                String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
3336                int accountHashCode =
3337                        ContactLookupKey.getAccountHashCode(accountType, accountName);
3338                String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
3339                for (int i = 0; i < segments.size(); i++) {
3340                    LookupKeySegment segment = segments.get(i);
3341                    if (!segment.sourceIdLookup && accountHashCode == segment.accountHashCode
3342                            && segment.key.equals(name)) {
3343                        segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
3344                        break;
3345                    }
3346                }
3347            }
3348        } finally {
3349            c.close();
3350        }
3351
3352        return getMostReferencedContactId(segments);
3353    }
3354
3355    /**
3356     * Returns the contact ID that is mentioned the highest number of times.
3357     */
3358    private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
3359        Collections.sort(segments);
3360
3361        long bestContactId = -1;
3362        int bestRefCount = 0;
3363
3364        long contactId = -1;
3365        int count = 0;
3366
3367        int segmentCount = segments.size();
3368        for (int i = 0; i < segmentCount; i++) {
3369            LookupKeySegment segment = segments.get(i);
3370            if (segment.contactId != -1) {
3371                if (segment.contactId == contactId) {
3372                    count++;
3373                } else {
3374                    if (count > bestRefCount) {
3375                        bestContactId = contactId;
3376                        bestRefCount = count;
3377                    }
3378                    contactId = segment.contactId;
3379                    count = 1;
3380                }
3381            }
3382        }
3383        if (count > bestRefCount) {
3384            return contactId;
3385        } else {
3386            return bestContactId;
3387        }
3388    }
3389
3390    private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, String[] projection) {
3391        String contactView = mOpenHelper.getContactView();
3392        boolean needsPresence = mOpenHelper.isInProjection(projection, Contacts.PRESENCE_STATUS,
3393                Contacts.PRESENCE_CUSTOM_STATUS);
3394        if (!needsPresence) {
3395            qb.setTables(contactView);
3396            qb.setProjectionMap(sContactsProjectionMap);
3397        } else {
3398            qb.setTables(contactView + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + " ON ("
3399                    + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ") ");
3400            qb.setProjectionMap(sContactsWithPresenceProjectionMap);
3401
3402        }
3403    }
3404
3405    private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
3406        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
3407        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
3408        if (!TextUtils.isEmpty(accountName)) {
3409            qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
3410                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3411                    + RawContacts.ACCOUNT_TYPE + "="
3412                    + DatabaseUtils.sqlEscapeString(accountType));
3413        } else {
3414            qb.appendWhere("1");
3415        }
3416    }
3417
3418    private String appendAccountToSelection(Uri uri, String selection) {
3419        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
3420        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
3421        if (!TextUtils.isEmpty(accountName)) {
3422            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
3423                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3424                    + RawContacts.ACCOUNT_TYPE + "="
3425                    + DatabaseUtils.sqlEscapeString(accountType));
3426            if (!TextUtils.isEmpty(selection)) {
3427                selectionSb.append(" AND (");
3428                selectionSb.append(selection);
3429                selectionSb.append(')');
3430            }
3431            return selectionSb.toString();
3432        } else {
3433            return selection;
3434        }
3435    }
3436
3437    /**
3438     * Gets the value of the "limit" URI query parameter.
3439     *
3440     * @return A string containing a non-negative integer, or <code>null</code> if
3441     *         the parameter is not set, or is set to an invalid value.
3442     */
3443    private String getLimit(Uri url) {
3444        String limitParam = url.getQueryParameter("limit");
3445        if (limitParam == null) {
3446            return null;
3447        }
3448        // make sure that the limit is a non-negative integer
3449        try {
3450            int l = Integer.parseInt(limitParam);
3451            if (l < 0) {
3452                Log.w(TAG, "Invalid limit parameter: " + limitParam);
3453                return null;
3454            }
3455            return String.valueOf(l);
3456        } catch (NumberFormatException ex) {
3457            Log.w(TAG, "Invalid limit parameter: " + limitParam);
3458            return null;
3459        }
3460    }
3461
3462    /**
3463     * Returns true if all the characters are meaningful as digits
3464     * in a phone number -- letters, digits, and a few punctuation marks.
3465     */
3466    private boolean isPhoneNumber(CharSequence cons) {
3467        int len = cons.length();
3468
3469        for (int i = 0; i < len; i++) {
3470            char c = cons.charAt(i);
3471
3472            if ((c >= '0') && (c <= '9')) {
3473                continue;
3474            }
3475            if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+')
3476                    || (c == '#') || (c == '*')) {
3477                continue;
3478            }
3479            if ((c >= 'A') && (c <= 'Z')) {
3480                continue;
3481            }
3482            if ((c >= 'a') && (c <= 'z')) {
3483                continue;
3484            }
3485
3486            return false;
3487        }
3488
3489        return true;
3490    }
3491
3492    String getContactsRestrictions() {
3493        if (mOpenHelper.hasRestrictedAccess()) {
3494            return "1";
3495        } else {
3496            return RawContacts.IS_RESTRICTED + "=0";
3497        }
3498    }
3499
3500    public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
3501        if (mOpenHelper.hasRestrictedAccess()) {
3502            return "1";
3503        } else {
3504            return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
3505                    + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
3506        }
3507    }
3508
3509    @Override
3510    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
3511        int match = sUriMatcher.match(uri);
3512        switch (match) {
3513            case CONTACTS_PHOTO:
3514                if (!"r".equals(mode)) {
3515                    throw new FileNotFoundException("Mode " + mode + " not supported.");
3516                }
3517
3518                long contactId = Long.parseLong(uri.getPathSegments().get(1));
3519
3520                String sql =
3521                        "SELECT " + Photo.PHOTO + " FROM " + mOpenHelper.getDataView() +
3522                        " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID
3523                                + " AND " + RawContacts.CONTACT_ID + "=" + contactId;
3524                SQLiteDatabase db = mOpenHelper.getReadableDatabase();
3525                return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null);
3526
3527            default:
3528                throw new FileNotFoundException("No file at: " + uri);
3529        }
3530    }
3531
3532
3533
3534    /**
3535     * An implementation of EntityIterator that joins the contacts and data tables
3536     * and consumes all the data rows for a contact in order to build the Entity for a contact.
3537     */
3538    private static class ContactsEntityIterator implements EntityIterator {
3539        private final Cursor mEntityCursor;
3540        private volatile boolean mIsClosed;
3541
3542        private static final String[] DATA_KEYS = new String[]{
3543                Data.DATA1,
3544                Data.DATA2,
3545                Data.DATA3,
3546                Data.DATA4,
3547                Data.DATA5,
3548                Data.DATA6,
3549                Data.DATA7,
3550                Data.DATA8,
3551                Data.DATA9,
3552                Data.DATA10,
3553                Data.DATA11,
3554                Data.DATA12,
3555                Data.DATA13,
3556                Data.DATA14,
3557                Data.DATA15,
3558                Data.SYNC1,
3559                Data.SYNC2,
3560                Data.SYNC3,
3561                Data.SYNC4};
3562
3563        private static final String[] PROJECTION = new String[]{
3564                RawContacts.ACCOUNT_NAME,
3565                RawContacts.ACCOUNT_TYPE,
3566                RawContacts.SOURCE_ID,
3567                RawContacts.VERSION,
3568                RawContacts.DIRTY,
3569                Data._ID,
3570                Data.RES_PACKAGE,
3571                Data.MIMETYPE,
3572                Data.DATA1,
3573                Data.DATA2,
3574                Data.DATA3,
3575                Data.DATA4,
3576                Data.DATA5,
3577                Data.DATA6,
3578                Data.DATA7,
3579                Data.DATA8,
3580                Data.DATA9,
3581                Data.DATA10,
3582                Data.DATA11,
3583                Data.DATA12,
3584                Data.DATA13,
3585                Data.DATA14,
3586                Data.DATA15,
3587                Data.SYNC1,
3588                Data.SYNC2,
3589                Data.SYNC3,
3590                Data.SYNC4,
3591                Data.RAW_CONTACT_ID,
3592                Data.IS_PRIMARY,
3593                Data.IS_SUPER_PRIMARY,
3594                Data.DATA_VERSION,
3595                GroupMembership.GROUP_SOURCE_ID,
3596                RawContacts.SYNC1,
3597                RawContacts.SYNC2,
3598                RawContacts.SYNC3,
3599                RawContacts.SYNC4,
3600                RawContacts.DELETED,
3601                RawContacts.CONTACT_ID,
3602                RawContacts.STARRED};
3603
3604        private static final int COLUMN_ACCOUNT_NAME = 0;
3605        private static final int COLUMN_ACCOUNT_TYPE = 1;
3606        private static final int COLUMN_SOURCE_ID = 2;
3607        private static final int COLUMN_VERSION = 3;
3608        private static final int COLUMN_DIRTY = 4;
3609        private static final int COLUMN_DATA_ID = 5;
3610        private static final int COLUMN_RES_PACKAGE = 6;
3611        private static final int COLUMN_MIMETYPE = 7;
3612        private static final int COLUMN_DATA1 = 8;
3613        private static final int COLUMN_RAW_CONTACT_ID = 27;
3614        private static final int COLUMN_IS_PRIMARY = 28;
3615        private static final int COLUMN_IS_SUPER_PRIMARY = 29;
3616        private static final int COLUMN_DATA_VERSION = 30;
3617        private static final int COLUMN_GROUP_SOURCE_ID = 31;
3618        private static final int COLUMN_SYNC1 = 32;
3619        private static final int COLUMN_SYNC2 = 33;
3620        private static final int COLUMN_SYNC3 = 34;
3621        private static final int COLUMN_SYNC4 = 35;
3622        private static final int COLUMN_DELETED = 36;
3623        private static final int COLUMN_CONTACT_ID = 37;
3624        private static final int COLUMN_STARRED = 38;
3625
3626        public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri,
3627                String selection, String[] selectionArgs, String sortOrder) {
3628            mIsClosed = false;
3629
3630            final String updatedSortOrder = (sortOrder == null)
3631                    ? Data.RAW_CONTACT_ID
3632                    : (Data.RAW_CONTACT_ID + "," + sortOrder);
3633
3634            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
3635            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
3636            qb.setTables(Tables.CONTACT_ENTITIES);
3637            if (contactsIdString != null) {
3638                qb.appendWhere(Data.RAW_CONTACT_ID + "=" + contactsIdString);
3639            }
3640            final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
3641            final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
3642            if (!TextUtils.isEmpty(accountName)) {
3643                qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
3644                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3645                        + RawContacts.ACCOUNT_TYPE + "="
3646                        + DatabaseUtils.sqlEscapeString(accountType));
3647            }
3648            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
3649                    null, null, updatedSortOrder);
3650            mEntityCursor.moveToFirst();
3651        }
3652
3653        public void reset() throws RemoteException {
3654            if (mIsClosed) {
3655                throw new IllegalStateException("calling reset() when the iterator is closed");
3656            }
3657            mEntityCursor.moveToFirst();
3658        }
3659
3660        public void close() {
3661            if (mIsClosed) {
3662                throw new IllegalStateException("closing when already closed");
3663            }
3664            mIsClosed = true;
3665            mEntityCursor.close();
3666        }
3667
3668        public boolean hasNext() throws RemoteException {
3669            if (mIsClosed) {
3670                throw new IllegalStateException("calling hasNext() when the iterator is closed");
3671            }
3672
3673            return !mEntityCursor.isAfterLast();
3674        }
3675
3676        public Entity next() throws RemoteException {
3677            if (mIsClosed) {
3678                throw new IllegalStateException("calling next() when the iterator is closed");
3679            }
3680            if (!hasNext()) {
3681                throw new IllegalStateException("you may only call next() if hasNext() is true");
3682            }
3683
3684            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
3685
3686            final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID);
3687
3688            // we expect the cursor is already at the row we need to read from
3689            ContentValues contactValues = new ContentValues();
3690            contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
3691            contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
3692            contactValues.put(RawContacts._ID, rawContactId);
3693            contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY));
3694            contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION));
3695            contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
3696            contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1));
3697            contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2));
3698            contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3));
3699            contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4));
3700            contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED));
3701            contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID));
3702            contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED));
3703            Entity contact = new Entity(contactValues);
3704
3705            // read data rows until the contact id changes
3706            do {
3707                if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) {
3708                    break;
3709                }
3710                // add the data to to the contact
3711                ContentValues dataValues = new ContentValues();
3712                dataValues.put(Data._ID, c.getString(COLUMN_DATA_ID));
3713                dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
3714                dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
3715                dataValues.put(Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY));
3716                dataValues.put(Data.IS_SUPER_PRIMARY, c.getString(COLUMN_IS_SUPER_PRIMARY));
3717                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
3718                if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
3719                    dataValues.put(GroupMembership.GROUP_SOURCE_ID,
3720                            c.getString(COLUMN_GROUP_SOURCE_ID));
3721                }
3722                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
3723                for (int i = 0; i < DATA_KEYS.length; i++) {
3724                    final int columnIndex = i + COLUMN_DATA1;
3725                    String key = DATA_KEYS[i];
3726                    if (c.isNull(columnIndex)) {
3727                        // don't put anything
3728                    } else if (c.isLong(columnIndex)) {
3729                        dataValues.put(key, c.getLong(columnIndex));
3730                    } else if (c.isFloat(columnIndex)) {
3731                        dataValues.put(key, c.getFloat(columnIndex));
3732                    } else if (c.isString(columnIndex)) {
3733                        dataValues.put(key, c.getString(columnIndex));
3734                    } else if (c.isBlob(columnIndex)) {
3735                        dataValues.put(key, c.getBlob(columnIndex));
3736                    }
3737                }
3738                contact.addSubValue(Data.CONTENT_URI, dataValues);
3739            } while (mEntityCursor.moveToNext());
3740
3741            return contact;
3742        }
3743    }
3744
3745    /**
3746     * An implementation of EntityIterator that joins the contacts and data tables
3747     * and consumes all the data rows for a contact in order to build the Entity for a contact.
3748     */
3749    private static class GroupsEntityIterator implements EntityIterator {
3750        private final Cursor mEntityCursor;
3751        private volatile boolean mIsClosed;
3752
3753        private static final String[] PROJECTION = new String[]{
3754                Groups._ID,
3755                Groups.ACCOUNT_NAME,
3756                Groups.ACCOUNT_TYPE,
3757                Groups.SOURCE_ID,
3758                Groups.DIRTY,
3759                Groups.VERSION,
3760                Groups.RES_PACKAGE,
3761                Groups.TITLE,
3762                Groups.TITLE_RES,
3763                Groups.GROUP_VISIBLE,
3764                Groups.SYNC1,
3765                Groups.SYNC2,
3766                Groups.SYNC3,
3767                Groups.SYNC4,
3768                Groups.SYSTEM_ID,
3769                Groups.NOTES,
3770                Groups.DELETED};
3771
3772        private static final int COLUMN_ID = 0;
3773        private static final int COLUMN_ACCOUNT_NAME = 1;
3774        private static final int COLUMN_ACCOUNT_TYPE = 2;
3775        private static final int COLUMN_SOURCE_ID = 3;
3776        private static final int COLUMN_DIRTY = 4;
3777        private static final int COLUMN_VERSION = 5;
3778        private static final int COLUMN_RES_PACKAGE = 6;
3779        private static final int COLUMN_TITLE = 7;
3780        private static final int COLUMN_TITLE_RES = 8;
3781        private static final int COLUMN_GROUP_VISIBLE = 9;
3782        private static final int COLUMN_SYNC1 = 10;
3783        private static final int COLUMN_SYNC2 = 11;
3784        private static final int COLUMN_SYNC3 = 12;
3785        private static final int COLUMN_SYNC4 = 13;
3786        private static final int COLUMN_SYSTEM_ID = 14;
3787        private static final int COLUMN_NOTES = 15;
3788        private static final int COLUMN_DELETED = 16;
3789
3790        public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri,
3791                String selection, String[] selectionArgs, String sortOrder) {
3792            mIsClosed = false;
3793
3794            final String updatedSortOrder = (sortOrder == null)
3795                    ? Groups._ID
3796                    : (Groups._ID + "," + sortOrder);
3797
3798            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
3799            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
3800            qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
3801            qb.setProjectionMap(sGroupsProjectionMap);
3802            if (groupIdString != null) {
3803                qb.appendWhere(Groups._ID + "=" + groupIdString);
3804            }
3805            final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
3806            final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
3807            if (!TextUtils.isEmpty(accountName)) {
3808                qb.appendWhere(Groups.ACCOUNT_NAME + "="
3809                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3810                        + Groups.ACCOUNT_TYPE + "="
3811                        + DatabaseUtils.sqlEscapeString(accountType));
3812            }
3813            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
3814                    null, null, updatedSortOrder);
3815            mEntityCursor.moveToFirst();
3816        }
3817
3818        public void close() {
3819            if (mIsClosed) {
3820                throw new IllegalStateException("closing when already closed");
3821            }
3822            mIsClosed = true;
3823            mEntityCursor.close();
3824        }
3825
3826        public boolean hasNext() throws RemoteException {
3827            if (mIsClosed) {
3828                throw new IllegalStateException("calling hasNext() when the iterator is closed");
3829            }
3830
3831            return !mEntityCursor.isAfterLast();
3832        }
3833
3834        public void reset() throws RemoteException {
3835            if (mIsClosed) {
3836                throw new IllegalStateException("calling reset() when the iterator is closed");
3837            }
3838            mEntityCursor.moveToFirst();
3839        }
3840
3841        public Entity next() throws RemoteException {
3842            if (mIsClosed) {
3843                throw new IllegalStateException("calling next() when the iterator is closed");
3844            }
3845            if (!hasNext()) {
3846                throw new IllegalStateException("you may only call next() if hasNext() is true");
3847            }
3848
3849            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
3850
3851            final long groupId = c.getLong(COLUMN_ID);
3852
3853            // we expect the cursor is already at the row we need to read from
3854            ContentValues groupValues = new ContentValues();
3855            groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
3856            groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
3857            groupValues.put(Groups._ID, groupId);
3858            groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY));
3859            groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION));
3860            groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
3861            groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
3862            groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE));
3863            groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES));
3864            groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE));
3865            groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1));
3866            groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2));
3867            groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3));
3868            groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4));
3869            groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID));
3870            groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED));
3871            groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES));
3872            Entity group = new Entity(groupValues);
3873
3874            mEntityCursor.moveToNext();
3875
3876            return group;
3877        }
3878    }
3879
3880    @Override
3881    public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
3882            String sortOrder) {
3883        waitForAccess();
3884
3885        final int match = sUriMatcher.match(uri);
3886        switch (match) {
3887            case RAW_CONTACTS:
3888            case RAW_CONTACTS_ID:
3889                String contactsIdString = null;
3890                if (match == RAW_CONTACTS_ID) {
3891                    contactsIdString = uri.getPathSegments().get(1);
3892                }
3893
3894                return new ContactsEntityIterator(this, contactsIdString,
3895                        uri, selection, selectionArgs, sortOrder);
3896            case GROUPS:
3897            case GROUPS_ID:
3898                String idString = null;
3899                if (match == GROUPS_ID) {
3900                    idString = uri.getPathSegments().get(1);
3901                }
3902
3903                return new GroupsEntityIterator(this, idString,
3904                        uri, selection, selectionArgs, sortOrder);
3905            default:
3906                throw new UnsupportedOperationException("Unknown uri: " + uri);
3907        }
3908    }
3909
3910    @Override
3911    public String getType(Uri uri) {
3912        final int match = sUriMatcher.match(uri);
3913        switch (match) {
3914            case CONTACTS:
3915            case CONTACTS_LOOKUP:
3916                return Contacts.CONTENT_TYPE;
3917            case CONTACTS_ID:
3918            case CONTACTS_LOOKUP_ID:
3919                return Contacts.CONTENT_ITEM_TYPE;
3920            case RAW_CONTACTS:
3921                return RawContacts.CONTENT_TYPE;
3922            case RAW_CONTACTS_ID:
3923                return RawContacts.CONTENT_ITEM_TYPE;
3924            case DATA_ID:
3925                return mOpenHelper.getDataMimeType(ContentUris.parseId(uri));
3926            case AGGREGATION_EXCEPTIONS:
3927                return AggregationExceptions.CONTENT_TYPE;
3928            case AGGREGATION_EXCEPTION_ID:
3929                return AggregationExceptions.CONTENT_ITEM_TYPE;
3930            case SETTINGS:
3931                return Settings.CONTENT_TYPE;
3932            case AGGREGATION_SUGGESTIONS:
3933                return Contacts.CONTENT_TYPE;
3934            case SEARCH_SUGGESTIONS:
3935                return SearchManager.SUGGEST_MIME_TYPE;
3936            case SEARCH_SHORTCUT:
3937                return SearchManager.SHORTCUT_MIME_TYPE;
3938            default:
3939                return mLegacyApiSupport.getType(uri);
3940        }
3941    }
3942
3943    private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) {
3944        if (displayName != null) {
3945            mRawContactDisplayNameUpdate.bindString(1, displayName);
3946        } else {
3947            mRawContactDisplayNameUpdate.bindNull(1);
3948        }
3949        mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource);
3950        mRawContactDisplayNameUpdate.bindLong(3, rawContactId);
3951        mRawContactDisplayNameUpdate.execute();
3952    }
3953
3954    /**
3955     * Checks the {@link Data#MARK_AS_DIRTY} query parameter.
3956     *
3957     * Returns true if the parameter is missing or is either "true" or "1".
3958     */
3959    private boolean shouldMarkRawContactAsDirty(Uri uri) {
3960        if (mImportMode) {
3961            return false;
3962        }
3963
3964        String param = uri.getQueryParameter(Data.MARK_AS_DIRTY);
3965        return param == null || (!param.equalsIgnoreCase("false") && !param.equals("0"));
3966    }
3967
3968    /**
3969     * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
3970     */
3971    private void setRawContactDirty(long rawContactId) {
3972        mRawContactDirtyUpdate.bindLong(1, rawContactId);
3973        mRawContactDirtyUpdate.execute();
3974    }
3975
3976    /**
3977     * Checks the {@link Groups#MARK_AS_DIRTY} query parameter.
3978     *
3979     * Returns true if the parameter is missing or is either "true" or "1".
3980     */
3981    private boolean shouldMarkGroupAsDirty(Uri uri) {
3982        if (mImportMode) {
3983            return false;
3984        }
3985
3986        return readBooleanQueryParameter(uri, Groups.MARK_AS_DIRTY, true);
3987    }
3988
3989    /*
3990     * Sets the given dataId record in the "data" table to primary, and resets all data records of
3991     * the same mimetype and under the same contact to not be primary.
3992     *
3993     * @param dataId the id of the data record to be set to primary.
3994     */
3995    private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
3996        mSetPrimaryStatement.bindLong(1, dataId);
3997        mSetPrimaryStatement.bindLong(2, mimeTypeId);
3998        mSetPrimaryStatement.bindLong(3, rawContactId);
3999        mSetPrimaryStatement.execute();
4000    }
4001
4002    /*
4003     * Sets the given dataId record in the "data" table to "super primary", and resets all data
4004     * records of the same mimetype and under the same aggregate to not be "super primary".
4005     *
4006     * @param dataId the id of the data record to be set to primary.
4007     */
4008    private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
4009        mSetSuperPrimaryStatement.bindLong(1, dataId);
4010        mSetSuperPrimaryStatement.bindLong(2, mimeTypeId);
4011        mSetSuperPrimaryStatement.bindLong(3, rawContactId);
4012        mSetSuperPrimaryStatement.execute();
4013    }
4014
4015    private void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
4016        sb.append("(SELECT DISTINCT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS
4017                + " JOIN name_lookup ON(" + RawContactsColumns.CONCRETE_ID + "=raw_contact_id)"
4018                + " WHERE normalized_name GLOB '");
4019        sb.append(NameNormalizer.normalize(filterParam));
4020        sb.append("*')");
4021    }
4022
4023    public String getRawContactsByFilterAsNestedQuery(String filterParam) {
4024        StringBuilder sb = new StringBuilder();
4025        appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
4026        return sb.toString();
4027    }
4028
4029    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
4030            String limit) {
4031        appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit);
4032    }
4033
4034    private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
4035            String limit) {
4036        sb.append("(SELECT DISTINCT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '");
4037        sb.append(normalizedName);
4038        sb.append("*'");
4039        if (limit != null) {
4040            sb.append(" LIMIT ").append(limit);
4041        }
4042        sb.append(")");
4043    }
4044
4045    /**
4046     * Inserts an argument at the beginning of the selection arg list.
4047     */
4048    private String[] insertSelectionArg(String[] selectionArgs, String arg) {
4049        if (selectionArgs == null) {
4050            return new String[] {arg};
4051        } else {
4052            int newLength = selectionArgs.length + 1;
4053            String[] newSelectionArgs = new String[newLength];
4054            newSelectionArgs[0] = arg;
4055            System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
4056            return newSelectionArgs;
4057        }
4058    }
4059
4060    protected Account getDefaultAccount() {
4061        AccountManager accountManager = AccountManager.get(getContext());
4062        try {
4063            Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE,
4064                    new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult();
4065            if (accounts != null && accounts.length > 0) {
4066                return accounts[0];
4067            }
4068        } catch (Throwable e) {
4069            Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
4070        }
4071        return null;
4072    }
4073}
4074