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