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