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