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