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