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