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