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