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