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