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