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