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