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