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