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