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