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