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