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