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