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