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