ContactsProvider2.java revision 88e5f4d32aa9cd3af0ac9654de479f1b8113f712
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.OpenHelper.AggregationExceptionColumns;
21import com.android.providers.contacts.OpenHelper.Clauses;
22import com.android.providers.contacts.OpenHelper.ContactsColumns;
23import com.android.providers.contacts.OpenHelper.DataColumns;
24import com.android.providers.contacts.OpenHelper.GroupsColumns;
25import com.android.providers.contacts.OpenHelper.MimetypesColumns;
26import com.android.providers.contacts.OpenHelper.PackagesColumns;
27import com.android.providers.contacts.OpenHelper.PhoneColumns;
28import com.android.providers.contacts.OpenHelper.PhoneLookupColumns;
29import com.android.providers.contacts.OpenHelper.RawContactsColumns;
30import com.android.providers.contacts.OpenHelper.Tables;
31import com.google.android.collect.Lists;
32
33import android.accounts.Account;
34import android.accounts.AccountManager;
35import android.app.SearchManager;
36import android.content.ContentUris;
37import android.content.ContentValues;
38import android.content.Context;
39import android.content.Entity;
40import android.content.EntityIterator;
41import android.content.SharedPreferences;
42import android.content.UriMatcher;
43import android.content.SharedPreferences.Editor;
44import android.database.Cursor;
45import android.database.DatabaseUtils;
46import android.database.sqlite.SQLiteCursor;
47import android.database.sqlite.SQLiteDatabase;
48import android.database.sqlite.SQLiteQueryBuilder;
49import android.database.sqlite.SQLiteStatement;
50import android.net.Uri;
51import android.os.RemoteException;
52import android.preference.PreferenceManager;
53import android.provider.BaseColumns;
54import android.provider.ContactsContract;
55import android.provider.Contacts.People;
56import android.provider.ContactsContract.AggregationExceptions;
57import android.provider.ContactsContract.CommonDataKinds;
58import android.provider.ContactsContract.Contacts;
59import android.provider.ContactsContract.Data;
60import android.provider.ContactsContract.Groups;
61import android.provider.ContactsContract.Presence;
62import android.provider.ContactsContract.RawContacts;
63import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
64import android.provider.ContactsContract.CommonDataKinds.Email;
65import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
66import android.provider.ContactsContract.CommonDataKinds.Im;
67import android.provider.ContactsContract.CommonDataKinds.Nickname;
68import android.provider.ContactsContract.CommonDataKinds.Organization;
69import android.provider.ContactsContract.CommonDataKinds.Phone;
70import android.provider.ContactsContract.CommonDataKinds.StructuredName;
71import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
72import android.telephony.PhoneNumberUtils;
73import android.text.TextUtils;
74import android.util.Log;
75
76import java.util.ArrayList;
77import java.util.HashMap;
78
79/**
80 * Contacts content provider. The contract between this provider and applications
81 * is defined in {@link ContactsContract}.
82 */
83public class ContactsProvider2 extends SQLiteContentProvider {
84
85    // TODO: clean up debug tag and rename this class
86    private static final String TAG = "ContactsProvider ~~~~";
87
88    // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
89    // TODO: check for restricted flag during insert(), update(), and delete() calls
90
91    /** Default for the maximum number of returned aggregation suggestions. */
92    private static final int DEFAULT_MAX_SUGGESTIONS = 5;
93
94    /**
95     * Shared preference key for the legacy contact import version. The need for a version
96     * as opposed to a boolean flag is that if we discover bugs in the contact import process,
97     * we can trigger re-import by incrementing the import version.
98     */
99    private static final String PREF_CONTACTS_IMPORTED = "contacts_imported_v1";
100    private static final int PREF_CONTACTS_IMPORT_VERSION = 1;
101
102    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
103
104    private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
105            + Contacts.TIMES_CONTACTED + " DESC, "
106            + Contacts.DISPLAY_NAME + " ASC";
107    private static final String STREQUENT_LIMIT =
108            "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
109            + Contacts.STARRED + "=1) + 25";
110
111    private static final int CONTACTS = 1000;
112    private static final int CONTACTS_ID = 1001;
113    private static final int CONTACTS_DATA = 1002;
114    private static final int CONTACTS_SUMMARY = 1003;
115    private static final int CONTACTS_SUMMARY_ID = 1005;
116    private static final int CONTACTS_SUMMARY_FILTER = 1006;
117    private static final int CONTACTS_SUMMARY_STREQUENT = 1007;
118    private static final int CONTACTS_SUMMARY_STREQUENT_FILTER = 1008;
119    private static final int CONTACTS_SUMMARY_GROUP = 1009;
120
121    private static final int RAW_CONTACTS = 2002;
122    private static final int RAW_CONTACTS_ID = 2003;
123    private static final int RAW_CONTACTS_DATA = 2004;
124
125    private static final int DATA = 3000;
126    private static final int DATA_ID = 3001;
127    private static final int PHONES = 3002;
128    private static final int PHONES_FILTER = 3003;
129    private static final int EMAILS = 3004;
130    private static final int EMAILS_FILTER = 3005;
131    private static final int POSTALS = 3006;
132
133    private static final int PHONE_LOOKUP = 4000;
134
135    private static final int AGGREGATION_EXCEPTIONS = 6000;
136    private static final int AGGREGATION_EXCEPTION_ID = 6001;
137
138    private static final int PRESENCE = 7000;
139    private static final int PRESENCE_ID = 7001;
140
141    private static final int AGGREGATION_SUGGESTIONS = 8000;
142
143    private static final int GROUPS = 10000;
144    private static final int GROUPS_ID = 10001;
145    private static final int GROUPS_SUMMARY = 10003;
146
147    private static final int SYNCSTATE = 11000;
148
149    private static final int SEARCH_SUGGESTIONS = 12001;
150    private static final int SEARCH_SHORTCUT = 12002;
151
152    private interface ContactsQuery {
153        public static final String TABLE = Tables.RAW_CONTACTS;
154
155        public static final String[] PROJECTION = new String[] {
156            RawContactsColumns.CONCRETE_ID,
157            RawContacts.ACCOUNT_NAME,
158            RawContacts.ACCOUNT_TYPE,
159        };
160
161        public static final int RAW_CONTACT_ID = 0;
162        public static final int ACCOUNT_NAME = 1;
163        public static final int ACCOUNT_TYPE = 2;
164    }
165
166    private interface DataRawContactsQuery {
167        public static final String TABLE = Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS;
168
169        public static final String[] PROJECTION = new String[] {
170            RawContactsColumns.CONCRETE_ID,
171            DataColumns.CONCRETE_ID,
172            RawContacts.CONTACT_ID,
173            RawContacts.IS_RESTRICTED,
174            Data.MIMETYPE,
175        };
176
177        public static final int RAW_CONTACT_ID = 0;
178        public static final int DATA_ID = 1;
179        public static final int CONTACT_ID = 2;
180        public static final int IS_RESTRICTED = 3;
181        public static final int MIMETYPE = 4;
182    }
183
184    private interface DataContactsQuery {
185        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS;
186
187        public static final String[] PROJECTION = new String[] {
188            RawContactsColumns.CONCRETE_ID,
189            DataColumns.CONCRETE_ID,
190            ContactsColumns.CONCRETE_ID,
191            MimetypesColumns.CONCRETE_ID,
192        };
193
194        public static final int RAW_CONTACT_ID = 0;
195        public static final int DATA_ID = 1;
196        public static final int CONTACT_ID = 2;
197        public static final int MIMETYPE_ID = 3;
198    }
199
200    private interface DisplayNameQuery {
201        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
202
203        public static final String[] COLUMNS = new String[] {
204            MimetypesColumns.MIMETYPE,
205            Data.IS_PRIMARY,
206            Data.DATA2,
207            StructuredName.DISPLAY_NAME,
208        };
209
210        public static final int MIMETYPE = 0;
211        public static final int IS_PRIMARY = 1;
212        public static final int DATA2 = 2;
213        public static final int DISPLAY_NAME = 3;
214    }
215
216    private interface DataQuery {
217        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
218
219        public static final String[] CONCRETE_COLUMNS = new String[] {
220            DataColumns.CONCRETE_ID,
221            MimetypesColumns.MIMETYPE,
222            Data.RAW_CONTACT_ID,
223            Data.IS_PRIMARY,
224            Data.DATA2,
225        };
226
227        public static final String[] COLUMNS = new String[] {
228            Data._ID,
229            MimetypesColumns.MIMETYPE,
230            Data.RAW_CONTACT_ID,
231            Data.IS_PRIMARY,
232            Data.DATA2,
233        };
234
235        public static final int ID = 0;
236        public static final int MIMETYPE = 1;
237        public static final int RAW_CONTACT_ID = 2;
238        public static final int IS_PRIMARY = 3;
239        public static final int DATA2 = 4;
240    }
241
242    private interface DataIdQuery {
243        String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };
244
245        int _ID = 0;
246        int RAW_CONTACT_ID = 1;
247        int MIMETYPE = 2;
248    }
249
250    // Higher number represents higher priority in choosing what data to use for the display name
251    private static final int DISPLAY_NAME_PRIORITY_EMAIL = 1;
252    private static final int DISPLAY_NAME_PRIORITY_PHONE = 2;
253    private static final int DISPLAY_NAME_PRIORITY_ORGANIZATION = 3;
254    private static final int DISPLAY_NAME_PRIORITY_STRUCTURED_NAME = 4;
255
256    private static final HashMap<String, Integer> sDisplayNamePriorities;
257    static {
258        sDisplayNamePriorities = new HashMap<String, Integer>();
259        sDisplayNamePriorities.put(StructuredName.CONTENT_ITEM_TYPE,
260                DISPLAY_NAME_PRIORITY_STRUCTURED_NAME);
261        sDisplayNamePriorities.put(Organization.CONTENT_ITEM_TYPE,
262                DISPLAY_NAME_PRIORITY_ORGANIZATION);
263        sDisplayNamePriorities.put(Phone.CONTENT_ITEM_TYPE,
264                DISPLAY_NAME_PRIORITY_PHONE);
265        sDisplayNamePriorities.put(Email.CONTENT_ITEM_TYPE,
266                DISPLAY_NAME_PRIORITY_EMAIL);
267    }
268
269    public static final String DEFAULT_ACCOUNT_TYPE = "com.google.GAIA";
270    public static final String FEATURE_APPS_FOR_DOMAIN = "google_or_dasher";
271
272    /** Contains just the contacts columns
273     * @deprecated*/
274    @Deprecated
275    private static final HashMap<String, String> sDeprecatedContactsProjectionMap;
276
277    private static final HashMap<String, String> sContactsProjectionMap;
278
279
280    /** Contains the contact columns along with primary phone */
281    private static final HashMap<String, String> sContactsSummaryProjectionMap;
282    /** Contains just the contacts columns */
283    private static final HashMap<String, String> sRawContactsProjectionMap;
284
285    /**
286     * Contains just the contacts columns
287     *
288     * @deprecated
289     */
290    @Deprecated
291    private static final HashMap<String, String> sDeprecatedRawContactsProjectionMap;
292
293    /**
294     * Contains just the data columns
295     *
296     * @deprecated
297     */
298    @Deprecated
299    private static final HashMap<String, String> sDeprecatedDataGroupsProjectionMap;
300
301    /** Contains columns from the data view */
302    private static final HashMap<String, String> sDataProjectionMap;
303
304    /** Contains the data and contacts columns, for joined tables */
305    private static final HashMap<String, String> sDataRawContactsGroupsProjectionMap;
306    /** Contains the data and contacts columns, for joined tables */
307    private static final HashMap<String, String> sDataRawContactsProjectionMap;
308    /** Contains the just the {@link Groups} columns */
309    private static final HashMap<String, String> sGroupsProjectionMap;
310    /** Contains {@link Groups} columns along with summary details */
311    private static final HashMap<String, String> sGroupsSummaryProjectionMap;
312    /** Contains the agg_exceptions columns */
313    private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
314    /** Contains Presence columns */
315    private static final HashMap<String, String> sPresenceProjectionMap;
316
317    /** Sql select statement that returns the contact id associated with a data record. */
318    private static final String sNestedRawContactIdSelect;
319    /** Sql select statement that returns the mimetype id associated with a data record. */
320    private static final String sNestedMimetypeSelect;
321    /** Sql select statement that returns the contact id associated with a contact record. */
322    private static final String sNestedContactIdSelect;
323    /** Sql select statement that returns a list of contact ids associated with an contact record. */
324    private static final String sNestedContactIdListSelect;
325    /** Sql where statement used to match all the data records that need to be updated when a new
326     * "primary" is selected.*/
327    private static final String sSetPrimaryWhere;
328    /** Sql where statement used to match all the data records that need to be updated when a new
329     * "super primary" is selected.*/
330    private static final String sSetSuperPrimaryWhere;
331    /** Sql where statement for filtering on groups. */
332    private static final String sContactsInGroupSelect;
333    /** Precompiled sql statement for setting a data record to the primary. */
334    private SQLiteStatement mSetPrimaryStatement;
335    /** Precompiled sql statement for setting a data record to the super primary. */
336    private SQLiteStatement mSetSuperPrimaryStatement;
337    /** Precompiled sql statement for incrementing times contacted for an contact */
338    private SQLiteStatement mLastTimeContactedUpdate;
339    /** Precompiled sql statement for updating a contact display name */
340    private SQLiteStatement mContactDisplayNameUpdate;
341    /** Precompiled sql statement for marking a raw contact as dirty */
342    private SQLiteStatement mRawContactDirtyUpdate;
343
344    static {
345        // Contacts URI matching table
346        final UriMatcher matcher = sUriMatcher;
347        matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
348        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
349        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
350        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary", CONTACTS_SUMMARY);
351        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/#", CONTACTS_SUMMARY_ID);
352        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/filter/*",
353                CONTACTS_SUMMARY_FILTER);
354        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/strequent/",
355                CONTACTS_SUMMARY_STREQUENT);
356        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/strequent/filter/*",
357                CONTACTS_SUMMARY_STREQUENT_FILTER);
358        matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/group/*",
359                CONTACTS_SUMMARY_GROUP);
360        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
361                AGGREGATION_SUGGESTIONS);
362        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
363        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
364        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
365
366        matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
367        matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
368        matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
369        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
370        matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
371        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
372        matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
373
374        matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
375        matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
376        matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
377
378        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
379
380        matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
381        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
382                AGGREGATION_EXCEPTIONS);
383        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
384                AGGREGATION_EXCEPTION_ID);
385
386        matcher.addURI(ContactsContract.AUTHORITY, "presence", PRESENCE);
387        matcher.addURI(ContactsContract.AUTHORITY, "presence/#", PRESENCE_ID);
388
389        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
390                SEARCH_SUGGESTIONS);
391        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
392                SEARCH_SUGGESTIONS);
393        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
394                SEARCH_SHORTCUT);
395
396        sContactsProjectionMap = new HashMap<String, String>();
397        sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
398        sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
399        sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
400        sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
401        sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
402        sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
403        sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
404        sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
405        sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER);
406        sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
407
408        sContactsSummaryProjectionMap = new HashMap<String, String>();
409        sContactsSummaryProjectionMap.putAll(sContactsProjectionMap);
410        sContactsSummaryProjectionMap.put(Contacts.PRESENCE_STATUS,
411                "MAX(" + Presence.PRESENCE_STATUS + ") AS " + Contacts.PRESENCE_STATUS);
412
413        HashMap<String, String> columns;
414
415        // Contacts projection map
416        columns = new HashMap<String, String>();
417        columns.put(Contacts._ID, "contacts._id AS _id");
418        columns.put(Contacts.DISPLAY_NAME, ContactsColumns.CONCRETE_DISPLAY_NAME + " AS "
419                + Contacts.DISPLAY_NAME);
420        columns.put(Contacts.LAST_TIME_CONTACTED, ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
421                + " AS " + Contacts.LAST_TIME_CONTACTED);
422        columns.put(Contacts.TIMES_CONTACTED, ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS "
423                + Contacts.TIMES_CONTACTED);
424        columns.put(Contacts.STARRED, ContactsColumns.CONCRETE_STARRED + " AS "
425                + Contacts.STARRED);
426        columns.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
427        columns.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
428        columns.put(Contacts.CUSTOM_RINGTONE, ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS "
429                + Contacts.CUSTOM_RINGTONE);
430        columns.put(Contacts.SEND_TO_VOICEMAIL, ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
431                + " AS " + Contacts.SEND_TO_VOICEMAIL);
432        sDeprecatedContactsProjectionMap = columns;
433
434        columns = new HashMap<String, String>();
435        columns.putAll(sDeprecatedContactsProjectionMap);
436
437//        // Contacts primaries projection map. The overall presence status is
438//        // the most-present value, as indicated by the largest value.
439//        columns.put(Contacts.PRESENCE_STATUS, "MAX(" + Presence.PRESENCE_STATUS + ") AS "
440//                + Contacts.PRESENCE_STATUS);
441//        columns.put(Contacts.PRIMARY_PHONE_TYPE, CommonDataKinds.Phone.TYPE);
442//        columns.put(Contacts.PRIMARY_PHONE_LABEL, CommonDataKinds.Phone.LABEL);
443//        columns.put(Contacts.PRIMARY_PHONE_NUMBER, CommonDataKinds.Phone.NUMBER);
444//        sContactsSummaryProjectionMap = columns;
445
446        // RawContacts projection map
447        columns = new HashMap<String, String>();
448        columns.put(RawContacts._ID, Tables.RAW_CONTACTS + "." + RawContacts._ID + " AS _id");
449        columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
450        columns.put(RawContacts.ACCOUNT_NAME,
451                OpenHelper.RawContactsColumns.CONCRETE_ACCOUNT_NAME
452                        + " AS " + RawContacts.ACCOUNT_NAME);
453        columns.put(RawContacts.ACCOUNT_TYPE,
454                OpenHelper.RawContactsColumns.CONCRETE_ACCOUNT_TYPE
455                        + " AS " + RawContacts.ACCOUNT_TYPE);
456        columns.put(RawContacts.SOURCE_ID,
457                OpenHelper.RawContactsColumns.CONCRETE_SOURCE_ID
458                        + " AS " + RawContacts.SOURCE_ID);
459        columns.put(RawContacts.VERSION,
460                OpenHelper.RawContactsColumns.CONCRETE_VERSION
461                        + " AS " + RawContacts.VERSION);
462        columns.put(RawContacts.DIRTY,
463                OpenHelper.RawContactsColumns.CONCRETE_DIRTY
464                        + " AS " + RawContacts.DIRTY);
465        columns.put(RawContacts.DELETED,
466                OpenHelper.RawContactsColumns.CONCRETE_DELETED
467                        + " AS " + RawContacts.DELETED);
468        columns.put(RawContacts.TIMES_CONTACTED,
469                Tables.RAW_CONTACTS + "." + RawContacts.TIMES_CONTACTED
470                        + " AS " + People.TIMES_CONTACTED);
471        columns.put(RawContacts.LAST_TIME_CONTACTED,
472                Tables.RAW_CONTACTS + "." + RawContacts.LAST_TIME_CONTACTED
473                        + " AS " + People.LAST_TIME_CONTACTED);
474        columns.put(RawContacts.CUSTOM_RINGTONE,
475                Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE
476                        + " AS " + People.CUSTOM_RINGTONE);
477        columns.put(RawContacts.SEND_TO_VOICEMAIL,
478                Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL
479                        + " AS " + People.SEND_TO_VOICEMAIL);
480        columns.put(RawContacts.STARRED,
481                Tables.RAW_CONTACTS + "." + RawContacts.STARRED
482                        + " AS " + People.STARRED);
483        columns.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE);
484        columns.put(RawContacts.SYNC1,
485                Tables.RAW_CONTACTS + "." + RawContacts.SYNC1 + " AS " + RawContacts.SYNC1);
486        columns.put(RawContacts.SYNC2,
487                Tables.RAW_CONTACTS + "." + RawContacts.SYNC2 + " AS " + RawContacts.SYNC2);
488        columns.put(RawContacts.SYNC3,
489                Tables.RAW_CONTACTS + "." + RawContacts.SYNC3 + " AS " + RawContacts.SYNC3);
490        columns.put(RawContacts.SYNC4,
491                Tables.RAW_CONTACTS + "." + RawContacts.SYNC4 + " AS " + RawContacts.SYNC4);
492        sDeprecatedRawContactsProjectionMap = columns;
493
494        sRawContactsProjectionMap = new HashMap<String, String>();
495        sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
496        sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
497        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
498        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
499        sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
500        sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
501        sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
502        sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
503        sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
504        sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
505                RawContacts.LAST_TIME_CONTACTED);
506        sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE);
507        sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL);
508        sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED);
509        sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE);
510        sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1);
511        sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2);
512        sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3);
513        sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4);
514
515        // Data projection map
516        columns = new HashMap<String, String>();
517        columns.put(Data._ID, Tables.DATA + "." + Data._ID + " AS _id");
518        columns.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
519        columns.put(Data.RES_PACKAGE, PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE);
520        columns.put(Data.MIMETYPE, Data.MIMETYPE);
521        columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
522        columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
523        columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
524        columns.put(Data.DATA1, "data.data1 as data1");
525        columns.put(Data.DATA2, "data.data2 as data2");
526        columns.put(Data.DATA3, "data.data3 as data3");
527        columns.put(Data.DATA4, "data.data4 as data4");
528        columns.put(Data.DATA5, "data.data5 as data5");
529        columns.put(Data.DATA6, "data.data6 as data6");
530        columns.put(Data.DATA7, "data.data7 as data7");
531        columns.put(Data.DATA8, "data.data8 as data8");
532        columns.put(Data.DATA9, "data.data9 as data9");
533        columns.put(Data.DATA10, "data.data10 as data10");
534        columns.put(Data.DATA11, "data.data11 as data11");
535        columns.put(Data.DATA12, "data.data12 as data12");
536        columns.put(Data.DATA13, "data.data13 as data13");
537        columns.put(Data.DATA14, "data.data14 as data14");
538        columns.put(Data.DATA15, "data.data15 as data15");
539        columns.put(Data.SYNC1, Tables.DATA + "." + Data.SYNC1 + " AS " + Data.SYNC1);
540        columns.put(Data.SYNC2, Tables.DATA + "." + Data.SYNC2 + " AS " + Data.SYNC2);
541        columns.put(Data.SYNC3, Tables.DATA + "." + Data.SYNC3 + " AS " + Data.SYNC3);
542        columns.put(Data.SYNC4, Tables.DATA + "." + Data.SYNC4 + " AS " + Data.SYNC4);
543        columns.put(GroupMembership.GROUP_SOURCE_ID, GroupsColumns.CONCRETE_SOURCE_ID + " AS "
544                + GroupMembership.GROUP_SOURCE_ID);
545
546        // TODO: remove this projection
547        // Mappings used for backwards compatibility.
548        columns.put("number", Phone.NUMBER);
549        sDeprecatedDataGroupsProjectionMap = columns;
550
551        sDataProjectionMap = new HashMap<String, String>();
552        sDataProjectionMap.put(Data._ID, Data._ID);
553        sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
554        sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
555        sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
556        sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
557        sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
558        sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
559        sDataProjectionMap.put(Data.DATA1, Data.DATA1);
560        sDataProjectionMap.put(Data.DATA2, Data.DATA2);
561        sDataProjectionMap.put(Data.DATA3, Data.DATA3);
562        sDataProjectionMap.put(Data.DATA4, Data.DATA4);
563        sDataProjectionMap.put(Data.DATA5, Data.DATA5);
564        sDataProjectionMap.put(Data.DATA6, Data.DATA6);
565        sDataProjectionMap.put(Data.DATA7, Data.DATA7);
566        sDataProjectionMap.put(Data.DATA8, Data.DATA8);
567        sDataProjectionMap.put(Data.DATA9, Data.DATA9);
568        sDataProjectionMap.put(Data.DATA10, Data.DATA10);
569        sDataProjectionMap.put(Data.DATA11, Data.DATA11);
570        sDataProjectionMap.put(Data.DATA12, Data.DATA12);
571        sDataProjectionMap.put(Data.DATA13, Data.DATA13);
572        sDataProjectionMap.put(Data.DATA14, Data.DATA14);
573        sDataProjectionMap.put(Data.DATA15, Data.DATA15);
574        sDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
575        sDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
576        sDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
577        sDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
578        sDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
579        sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
580        sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
581        sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
582        sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
583        sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
584        sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
585        sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
586        sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
587        sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
588        sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
589        sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
590        sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
591        sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
592
593        // Data, groups and contacts projection map for joins. _id comes from the data table
594        columns = new HashMap<String, String>();
595        columns.putAll(sDeprecatedRawContactsProjectionMap);
596        columns.putAll(sDeprecatedDataGroupsProjectionMap);
597        columns.put(Data.RAW_CONTACT_ID, DataColumns.CONCRETE_RAW_CONTACT_ID);
598        sDataRawContactsGroupsProjectionMap = columns;
599
600        // Data and contacts projection map for joins. _id comes from the data table
601        columns = new HashMap<String, String>();
602        columns.putAll(sDataRawContactsGroupsProjectionMap);
603        columns.remove(GroupMembership.GROUP_SOURCE_ID);
604        sDataRawContactsProjectionMap = columns;
605
606        // Groups projection map
607        columns = new HashMap<String, String>();
608        columns.put(Groups._ID, "groups._id AS _id");
609        columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
610        columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
611        columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
612        columns.put(Groups.DIRTY, Groups.DIRTY);
613        columns.put(Groups.VERSION, Groups.VERSION);
614        columns.put(Groups.RES_PACKAGE, PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE);
615        columns.put(Groups.TITLE, Groups.TITLE);
616        columns.put(Groups.TITLE_RES, Groups.TITLE_RES);
617        columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
618        columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID);
619        columns.put(Groups.DELETED, Groups.DELETED);
620        columns.put(Groups.NOTES, Groups.NOTES);
621        columns.put(Groups.SYNC1, Tables.GROUPS + "." + Groups.SYNC1 + " AS " + Groups.SYNC1);
622        columns.put(Groups.SYNC2, Tables.GROUPS + "." + Groups.SYNC2 + " AS " + Groups.SYNC2);
623        columns.put(Groups.SYNC3, Tables.GROUPS + "." + Groups.SYNC3 + " AS " + Groups.SYNC3);
624        columns.put(Groups.SYNC4, Tables.GROUPS + "." + Groups.SYNC4 + " AS " + Groups.SYNC4);
625        sGroupsProjectionMap = columns;
626
627        // RawContacts and groups projection map
628        columns = new HashMap<String, String>();
629        columns.putAll(sGroupsProjectionMap);
630
631        columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
632                + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
633                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
634                + ") AS " + Groups.SUMMARY_COUNT);
635
636        columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
637                + ContactsColumns.CONCRETE_ID + ") FROM "
638                + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
639                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
640                + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES);
641
642        sGroupsSummaryProjectionMap = columns;
643
644        // Aggregate exception projection map
645        columns = new HashMap<String, String>();
646        columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
647        columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
648        columns.put(AggregationExceptions.CONTACT_ID,
649                "raw_contacts1." + RawContacts.CONTACT_ID
650                + " AS " + AggregationExceptions.CONTACT_ID);
651        columns.put(AggregationExceptions.RAW_CONTACT_ID, AggregationExceptionColumns.RAW_CONTACT_ID2);
652        sAggregationExceptionsProjectionMap = columns;
653
654
655        columns = new HashMap<String, String>();
656        columns.put(Presence._ID, Presence._ID);
657        columns.put(Presence.RAW_CONTACT_ID, Presence.RAW_CONTACT_ID);
658        columns.put(Presence.DATA_ID, Presence.DATA_ID);
659        columns.put(Presence.IM_ACCOUNT, Presence.IM_ACCOUNT);
660        columns.put(Presence.IM_HANDLE, Presence.IM_HANDLE);
661        columns.put(Presence.IM_PROTOCOL, Presence.IM_PROTOCOL);
662        columns.put(Presence.PRESENCE_STATUS, Presence.PRESENCE_STATUS);
663        columns.put(Presence.PRESENCE_CUSTOM_STATUS, Presence.PRESENCE_CUSTOM_STATUS);
664        sPresenceProjectionMap = columns;
665
666        sNestedRawContactIdSelect = "SELECT " + Data.RAW_CONTACT_ID + " FROM " + Tables.DATA + " WHERE "
667                + Data._ID + "=?";
668        sNestedMimetypeSelect = "SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA
669                + " WHERE " + Data._ID + "=?";
670        sNestedContactIdSelect = "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS
671                + " WHERE " + RawContacts._ID + "=(" + sNestedRawContactIdSelect + ")";
672        sNestedContactIdListSelect = "SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS
673                + " WHERE " + RawContacts.CONTACT_ID + "=(" + sNestedContactIdSelect + ")";
674        sSetPrimaryWhere = Data.RAW_CONTACT_ID + "=(" + sNestedRawContactIdSelect + ") AND "
675                + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
676        sSetSuperPrimaryWhere = Data.RAW_CONTACT_ID + " IN (" + sNestedContactIdListSelect + ") AND "
677                + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
678        sContactsInGroupSelect = Contacts._ID + " IN "
679                + "(SELECT " + RawContacts.CONTACT_ID
680                + " FROM " + Tables.RAW_CONTACTS
681                + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
682                        + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
683                        + " FROM " + Tables.DATA_JOIN_MIMETYPES
684                        + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
685                                + "' AND " + GroupMembership.GROUP_ROW_ID + "="
686                                + "(SELECT " + Tables.GROUPS + "." + Groups._ID
687                                + " FROM " + Tables.GROUPS
688                                + " WHERE " + Groups.TITLE + "=?)))";
689    }
690
691    /**
692     * Handles inserts and update for a specific Data type.
693     */
694    private abstract class DataRowHandler {
695
696        protected final String mMimetype;
697
698        public DataRowHandler(String mimetype) {
699            mMimetype = mimetype;
700        }
701
702        /**
703         * Inserts a row into the {@link Data} table.
704         */
705        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
706            final long dataId = db.insert(Tables.DATA, null, values);
707
708            Integer primary = values.getAsInteger(Data.IS_PRIMARY);
709            if (primary != null && primary != 0) {
710                setIsPrimary(dataId);
711            }
712
713            fixContactDisplayName(db, rawContactId);
714            return dataId;
715        }
716
717        /**
718         * Validates data and updates a {@link Data} row using the cursor, which contains
719         * the current data.
720         */
721        public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
722            throw new UnsupportedOperationException();
723        }
724
725        public int delete(SQLiteDatabase db, Cursor c) {
726            long dataId = c.getLong(DataQuery.ID);
727            long rawContactId = c.getLong(DataQuery.RAW_CONTACT_ID);
728            boolean primary = c.getInt(DataQuery.IS_PRIMARY) != 0;
729            int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
730            if (count != 0 && primary) {
731                fixPrimary(db, rawContactId);
732                fixContactDisplayName(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                ContactsProvider2.this.setIsPrimary(newPrimaryId);
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(DataQuery.ID);
751                    int type = c.getInt(DataQuery.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(DataQuery.TABLE, DataQuery.CONCRETE_COLUMNS, Data.RAW_CONTACT_ID + "="
773                    + rawContactId + " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'",
774                    null, null, null, null);
775        }
776
777        protected void fixContactDisplayName(SQLiteDatabase db, long rawContactId) {
778            if (!sDisplayNamePriorities.containsKey(mMimetype)) {
779                return;
780            }
781
782            String bestDisplayName = null;
783            Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS,
784                    Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null);
785            try {
786                int maxPriority = -1;
787                while (c.moveToNext()) {
788                    String mimeType = c.getString(DisplayNameQuery.MIMETYPE);
789                    boolean primary;
790                    String name;
791
792                    if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
793                        name = c.getString(DisplayNameQuery.DISPLAY_NAME);
794                        primary = true;
795                    } else {
796                        name = c.getString(DisplayNameQuery.DATA2);
797                        primary = (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0);
798                    }
799
800                    if (primary && name != null) {
801                        Integer priority = sDisplayNamePriorities.get(mimeType);
802                        if (priority != null && priority > maxPriority) {
803                            maxPriority = priority;
804                            bestDisplayName = name;
805                        }
806                    }
807                }
808
809            } finally {
810                c.close();
811            }
812
813            ContactsProvider2.this.setDisplayName(rawContactId, bestDisplayName);
814        }
815    }
816
817    public class CustomDataRowHandler extends DataRowHandler {
818
819        public CustomDataRowHandler(String mimetype) {
820            super(mimetype);
821        }
822    }
823
824    public class StructuredNameRowHandler extends DataRowHandler {
825
826        private final NameSplitter mNameSplitter;
827
828        public StructuredNameRowHandler(NameSplitter nameSplitter) {
829            super(StructuredName.CONTENT_ITEM_TYPE);
830            mNameSplitter = nameSplitter;
831        }
832
833        @Override
834        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
835            fixStructuredNameComponents(values);
836            return super.insert(db, rawContactId, values);
837        }
838
839        @Override
840        public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
841            // TODO Parse the full name if it has changed and replace pre-existing piece parts.
842        }
843
844        /**
845         * Parses the supplied display name, but only if the incoming values do not already contain
846         * structured name parts.  Also, if the display name is not provided, generate one by
847         * concatenating first name and last name
848         *
849         * TODO see if the order of first and last names needs to be conditionally reversed for
850         * some locales, e.g. China.
851         */
852        private void fixStructuredNameComponents(ContentValues values) {
853            String fullName = values.getAsString(StructuredName.DISPLAY_NAME);
854            if (!TextUtils.isEmpty(fullName)
855                    && TextUtils.isEmpty(values.getAsString(StructuredName.PREFIX))
856                    && TextUtils.isEmpty(values.getAsString(StructuredName.GIVEN_NAME))
857                    && TextUtils.isEmpty(values.getAsString(StructuredName.MIDDLE_NAME))
858                    && TextUtils.isEmpty(values.getAsString(StructuredName.FAMILY_NAME))
859                    && TextUtils.isEmpty(values.getAsString(StructuredName.SUFFIX))) {
860                NameSplitter.Name name = new NameSplitter.Name();
861                mNameSplitter.split(name, fullName);
862
863                values.put(StructuredName.PREFIX, name.getPrefix());
864                values.put(StructuredName.GIVEN_NAME, name.getGivenNames());
865                values.put(StructuredName.MIDDLE_NAME, name.getMiddleName());
866                values.put(StructuredName.FAMILY_NAME, name.getFamilyName());
867                values.put(StructuredName.SUFFIX, name.getSuffix());
868            }
869
870            if (TextUtils.isEmpty(fullName)) {
871                String givenName = values.getAsString(StructuredName.GIVEN_NAME);
872                String familyName = values.getAsString(StructuredName.FAMILY_NAME);
873                if (TextUtils.isEmpty(givenName)) {
874                    fullName = familyName;
875                } else if (TextUtils.isEmpty(familyName)) {
876                    fullName = givenName;
877                } else {
878                    fullName = givenName + " " + familyName;
879                }
880
881                if (!TextUtils.isEmpty(fullName)) {
882                    values.put(StructuredName.DISPLAY_NAME, fullName);
883                }
884            }
885        }
886    }
887
888    public class CommonDataRowHandler extends DataRowHandler {
889
890        private final String mTypeColumn;
891        private final String mLabelColumn;
892
893        public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) {
894            super(mimetype);
895            mTypeColumn = typeColumn;
896            mLabelColumn = labelColumn;
897        }
898
899        @Override
900        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
901            int type;
902            String label;
903            if (values.containsKey(mTypeColumn)) {
904                type = values.getAsInteger(mTypeColumn);
905            } else {
906                type = BaseTypes.TYPE_CUSTOM;
907            }
908            if (values.containsKey(mLabelColumn)) {
909                label = values.getAsString(mLabelColumn);
910            } else {
911                label = null;
912            }
913
914            if (type != BaseTypes.TYPE_CUSTOM && label != null) {
915                throw new IllegalArgumentException(mLabelColumn + " value can only be specified with "
916                        + mTypeColumn + "=" + BaseTypes.TYPE_CUSTOM + "(custom)");
917            }
918
919            if (type == BaseTypes.TYPE_CUSTOM && label == null) {
920                throw new IllegalArgumentException(mLabelColumn + " value must be specified when "
921                        + mTypeColumn + "=" + BaseTypes.TYPE_CUSTOM + "(custom)");
922            }
923
924            return super.insert(db, rawContactId, values);
925        }
926
927        @Override
928        public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
929            // TODO read the data and check the constraint
930        }
931    }
932
933    public class OrganizationDataRowHandler extends CommonDataRowHandler {
934
935        public OrganizationDataRowHandler() {
936            super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
937        }
938
939        @Override
940        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
941            long id = super.insert(db, rawContactId, values);
942            fixContactDisplayName(db, rawContactId);
943            return id;
944        }
945
946        @Override
947        protected int getTypeRank(int type) {
948            switch (type) {
949                case Organization.TYPE_WORK: return 0;
950                case Organization.TYPE_CUSTOM: return 1;
951                case Organization.TYPE_OTHER: return 2;
952                default: return 1000;
953            }
954        }
955    }
956
957    public class EmailDataRowHandler extends CommonDataRowHandler {
958
959        public EmailDataRowHandler() {
960            super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
961        }
962
963        @Override
964        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
965            long id = super.insert(db, rawContactId, values);
966            fixContactDisplayName(db, rawContactId);
967            return id;
968        }
969
970        @Override
971        protected int getTypeRank(int type) {
972            switch (type) {
973                case Email.TYPE_HOME: return 0;
974                case Email.TYPE_WORK: return 1;
975                case Email.TYPE_CUSTOM: return 2;
976                case Email.TYPE_OTHER: return 3;
977                default: return 1000;
978            }
979        }
980    }
981
982    public class PhoneDataRowHandler extends CommonDataRowHandler {
983
984        public PhoneDataRowHandler() {
985            super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
986        }
987
988        @Override
989        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
990            ContentValues phoneValues = new ContentValues();
991            String number = values.getAsString(Phone.NUMBER);
992            String normalizedNumber = null;
993            if (number != null) {
994                normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
995                values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
996            }
997
998            long id = super.insert(db, rawContactId, values);
999
1000            if (number != null) {
1001                phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
1002                phoneValues.put(PhoneLookupColumns.DATA_ID, id);
1003                phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
1004                db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
1005            }
1006
1007            return id;
1008        }
1009
1010        @Override
1011        protected int getTypeRank(int type) {
1012            switch (type) {
1013                case Phone.TYPE_MOBILE: return 0;
1014                case Phone.TYPE_WORK: return 1;
1015                case Phone.TYPE_HOME: return 2;
1016                case Phone.TYPE_PAGER: return 3;
1017                case Phone.TYPE_CUSTOM: return 4;
1018                case Phone.TYPE_OTHER: return 5;
1019                case Phone.TYPE_FAX_WORK: return 6;
1020                case Phone.TYPE_FAX_HOME: return 7;
1021                default: return 1000;
1022            }
1023        }
1024    }
1025
1026    private HashMap<String, DataRowHandler> mDataRowHandlers;
1027    private final ContactAggregationScheduler mAggregationScheduler;
1028    private OpenHelper mOpenHelper;
1029
1030    private ContactAggregator mContactAggregator;
1031    private NameSplitter mNameSplitter;
1032    private LegacyApiSupport mLegacyApiSupport;
1033    private GlobalSearchSupport mGlobalSearchSupport;
1034
1035    private ContentValues mValues = new ContentValues();
1036
1037    private boolean mImportMode;
1038
1039    private boolean mScheduleAggregation;
1040
1041    public ContactsProvider2() {
1042        this(new ContactAggregationScheduler());
1043    }
1044
1045    /**
1046     * Constructor for testing.
1047     */
1048    /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
1049        mAggregationScheduler = scheduler;
1050    }
1051
1052    @Override
1053    public boolean onCreate() {
1054        super.onCreate();
1055
1056        final Context context = getContext();
1057        mOpenHelper = (OpenHelper)getOpenHelper();
1058        mGlobalSearchSupport = new GlobalSearchSupport(this);
1059        mLegacyApiSupport = new LegacyApiSupport(context, mOpenHelper, this, mGlobalSearchSupport);
1060        mContactAggregator = new ContactAggregator(context, mOpenHelper, mAggregationScheduler);
1061
1062        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1063        mSetPrimaryStatement = db.compileStatement(
1064                "UPDATE " + Tables.DATA + " SET " + Data.IS_PRIMARY
1065                + "=(_id=?) WHERE " + sSetPrimaryWhere);
1066        mSetSuperPrimaryStatement = db.compileStatement(
1067                "UPDATE " + Tables.DATA + " SET " + Data.IS_SUPER_PRIMARY
1068                + "=(_id=?) WHERE " + sSetSuperPrimaryWhere);
1069        mLastTimeContactedUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
1070                + RawContacts.TIMES_CONTACTED + "=" + RawContacts.TIMES_CONTACTED + "+1,"
1071                + RawContacts.LAST_TIME_CONTACTED + "=? WHERE " + RawContacts.CONTACT_ID + "=?");
1072
1073        mContactDisplayNameUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
1074                + RawContactsColumns.DISPLAY_NAME + "=? WHERE " + RawContacts._ID + "=?");
1075
1076        mRawContactDirtyUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
1077                + RawContacts.DIRTY + "=1 WHERE " + RawContacts._ID + "=?");
1078
1079        mNameSplitter = new NameSplitter(
1080                context.getString(com.android.internal.R.string.common_name_prefixes),
1081                context.getString(com.android.internal.R.string.common_last_name_prefixes),
1082                context.getString(com.android.internal.R.string.common_name_suffixes),
1083                context.getString(com.android.internal.R.string.common_name_conjunctions));
1084
1085        mDataRowHandlers = new HashMap<String, DataRowHandler>();
1086
1087        mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
1088        mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
1089                new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
1090        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
1091                StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
1092        mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
1093        mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
1094        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
1095                Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL));
1096        mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
1097                new StructuredNameRowHandler(mNameSplitter));
1098
1099        if (isLegacyContactImportNeeded()) {
1100            if (!importLegacyContacts()) {
1101                return false;
1102            }
1103        }
1104        return (db != null);
1105    }
1106
1107    /* Visible for testing */
1108    @Override
1109    protected OpenHelper getOpenHelper(final Context context) {
1110        return OpenHelper.getInstance(context);
1111    }
1112
1113    protected boolean isLegacyContactImportNeeded() {
1114        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1115        return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION;
1116    }
1117
1118    private boolean importLegacyContacts() {
1119        if (importLegacyContacts(getLegacyContactImporter())) {
1120            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1121            Editor editor = prefs.edit();
1122            editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION);
1123            editor.commit();
1124            return true;
1125        } else {
1126            return false;
1127        }
1128    }
1129
1130    protected LegacyContactImporter getLegacyContactImporter() {
1131        return new LegacyContactImporter(getContext(), this);
1132    }
1133
1134    /* Visible for testing */
1135    /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
1136        mContactAggregator.setEnabled(false);
1137        mImportMode = true;
1138        try {
1139            importer.importContacts();
1140            mContactAggregator.setEnabled(true);
1141            mContactAggregator.run();
1142            return true;
1143        } catch (Throwable e) {
1144           Log.e(TAG, "Legacy contact import failed", e);
1145           return false;
1146        } finally {
1147            mImportMode = false;
1148        }
1149    }
1150
1151    @Override
1152    protected void finalize() throws Throwable {
1153        if (mContactAggregator != null) {
1154            mContactAggregator.quit();
1155        }
1156
1157        super.finalize();
1158    }
1159
1160    /**
1161     * Wipes all data from the contacts database.
1162     */
1163    /* package */ void wipeData() {
1164        mOpenHelper.wipeData();
1165    }
1166
1167    @Override
1168    protected void onTransactionComplete() {
1169        if (mScheduleAggregation) {
1170            mScheduleAggregation = false;
1171            mContactAggregator.schedule();
1172        }
1173        super.onTransactionComplete();
1174    }
1175
1176    private DataRowHandler getDataRowHandler(final String mimeType) {
1177        DataRowHandler handler = mDataRowHandlers.get(mimeType);
1178        if (handler == null) {
1179            handler = new CustomDataRowHandler(mimeType);
1180            mDataRowHandlers.put(mimeType, handler);
1181        }
1182        return handler;
1183    }
1184
1185    @Override
1186    protected Uri insertInTransaction(Uri uri, ContentValues values) {
1187        final int match = sUriMatcher.match(uri);
1188        long id = 0;
1189
1190        switch (match) {
1191            case SYNCSTATE:
1192                id = mOpenHelper.getSyncState().insert(mDb, values);
1193                break;
1194
1195            case CONTACTS: {
1196                insertContact(values);
1197                break;
1198            }
1199
1200            case RAW_CONTACTS: {
1201                final Account account = readAccountFromQueryParams(uri);
1202                id = insertRawContact(values, account);
1203                break;
1204            }
1205
1206            case RAW_CONTACTS_DATA: {
1207                values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
1208                id = insertData(values, shouldMarkRawContactAsDirty(uri));
1209                break;
1210            }
1211
1212            case DATA: {
1213                id = insertData(values, shouldMarkRawContactAsDirty(uri));
1214                break;
1215            }
1216
1217            case GROUPS: {
1218                final Account account = readAccountFromQueryParams(uri);
1219                id = insertGroup(values, account, shouldMarkGroupAsDirty(uri));
1220                break;
1221            }
1222
1223            case PRESENCE: {
1224                id = insertPresence(values);
1225                break;
1226            }
1227
1228            default:
1229                return mLegacyApiSupport.insert(uri, values);
1230        }
1231
1232        if (id < 0) {
1233            return null;
1234        }
1235
1236        return ContentUris.withAppendedId(uri, id);
1237    }
1238
1239    /**
1240     * If account is non-null then store it in the values. If the account is already
1241     * specified in the values then it must be consistent with the account, if it is non-null.
1242     * @param values the ContentValues to read from and update
1243     * @param account the explicitly provided Account
1244     * @return false if the accounts are inconsistent
1245     */
1246    private boolean resolveAccount(ContentValues values, Account account) {
1247        // If either is specified then both must be specified.
1248        final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
1249        final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
1250        if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
1251            final Account valuesAccount = new Account(accountName, accountType);
1252            if (account != null && !valuesAccount.equals(account)) {
1253                return false;
1254            }
1255            account = valuesAccount;
1256        }
1257        if (account != null) {
1258            values.put(RawContacts.ACCOUNT_NAME, account.mName);
1259            values.put(RawContacts.ACCOUNT_TYPE, account.mType);
1260        }
1261        return true;
1262    }
1263
1264    /**
1265     * Inserts an item in the contacts table
1266     *
1267     * @param values the values for the new row
1268     * @return the row ID of the newly created row
1269     */
1270    private long insertContact(ContentValues values) {
1271        throw new UnsupportedOperationException("Aggregate contacts are created automatically");
1272    }
1273
1274    /**
1275     * Inserts an item in the contacts table
1276     *
1277     * @param values the values for the new row
1278     * @param account the account this contact should be associated with. may be null.
1279     * @return the row ID of the newly created row
1280     */
1281    private long insertRawContact(ContentValues values, Account account) {
1282        /*
1283         * The contact record is inserted in the contacts table, but it needs to
1284         * be processed by the aggregator before it will be returned by the
1285         * "aggregates" queries.
1286         */
1287        ContentValues overriddenValues = new ContentValues(values);
1288        overriddenValues.putNull(RawContacts.CONTACT_ID);
1289        if (!resolveAccount(overriddenValues, account)) {
1290            return -1;
1291        }
1292
1293        if (values.containsKey(RawContacts.DELETED)
1294                && values.getAsInteger(RawContacts.DELETED) != 0) {
1295            overriddenValues.put(RawContacts.AGGREGATION_MODE,
1296                    RawContacts.AGGREGATION_MODE_DISABLED);
1297        }
1298
1299        return mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
1300    }
1301
1302    /**
1303     * Inserts an item in the data table
1304     *
1305     * @param values the values for the new row
1306     * @return the row ID of the newly created row
1307     */
1308    private long insertData(ContentValues values, boolean markRawContactAsDirty) {
1309        int aggregationMode = RawContacts.AGGREGATION_MODE_DISABLED;
1310        long id = 0;
1311        mValues.clear();
1312        mValues.putAll(values);
1313
1314        long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
1315
1316        // Replace package with internal mapping
1317        final String packageName = mValues.getAsString(Data.RES_PACKAGE);
1318        if (packageName != null) {
1319            mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1320        }
1321        mValues.remove(Data.RES_PACKAGE);
1322
1323        // Replace mimetype with internal mapping
1324        final String mimeType = mValues.getAsString(Data.MIMETYPE);
1325        if (TextUtils.isEmpty(mimeType)) {
1326            throw new IllegalArgumentException(Data.MIMETYPE + " is required");
1327        }
1328
1329        mValues.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
1330        mValues.remove(Data.MIMETYPE);
1331
1332        // TODO create GroupMembershipRowHandler and move this code there
1333        resolveGroupSourceIdInValues(rawContactId, mimeType, mDb, mValues, true /* isInsert */);
1334
1335        id = getDataRowHandler(mimeType).insert(mDb, rawContactId, mValues);
1336        if (markRawContactAsDirty) {
1337            setRawContactDirty(rawContactId);
1338        }
1339
1340        aggregationMode = mContactAggregator.markContactForAggregation(mDb, rawContactId);
1341
1342        triggerAggregation(id, aggregationMode);
1343        return id;
1344    }
1345
1346    private void triggerAggregation(long rawContactId, int aggregationMode) {
1347        switch (aggregationMode) {
1348            case RawContacts.AGGREGATION_MODE_DEFAULT:
1349                mScheduleAggregation = true;
1350                break;
1351
1352            case RawContacts.AGGREGATION_MODE_IMMEDITATE:
1353                mContactAggregator.aggregateContact(mDb, rawContactId);
1354                break;
1355
1356            case RawContacts.AGGREGATION_MODE_DISABLED:
1357                // Do nothing
1358                break;
1359        }
1360    }
1361
1362    /**
1363     * Returns the group id of the group with sourceId and the same account as rawContactId.
1364     * If the group doesn't already exist then it is first created,
1365     * @param db SQLiteDatabase to use for this operation
1366     * @param rawContactId the contact this group is associated with
1367     * @param sourceId the sourceIf of the group to query or create
1368     * @return the group id of the existing or created group
1369     * @throws IllegalArgumentException if the contact is not associated with an account
1370     * @throws IllegalStateException if a group needs to be created but the creation failed
1371     */
1372    private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) {
1373        Account account = null;
1374        Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "="
1375                + rawContactId, null, null, null, null);
1376        try {
1377            if (c.moveToNext()) {
1378                final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
1379                final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
1380                if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
1381                    account = new Account(accountName, accountType);
1382                }
1383            }
1384        } finally {
1385            c.close();
1386        }
1387        if (account == null) {
1388            throw new IllegalArgumentException("if the groupmembership only "
1389                    + "has a sourceid the the contact must be associate with "
1390                    + "an account");
1391        }
1392
1393        // look up the group that contains this sourceId and has the same account name and type
1394        // as the contact refered to by rawContactId
1395        c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
1396                Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
1397                new String[]{sourceId, account.mName, account.mType}, null, null, null);
1398        try {
1399            if (c.moveToNext()) {
1400                return c.getLong(0);
1401            } else {
1402                ContentValues groupValues = new ContentValues();
1403                groupValues.put(Groups.ACCOUNT_NAME, account.mName);
1404                groupValues.put(Groups.ACCOUNT_TYPE, account.mType);
1405                groupValues.put(Groups.SOURCE_ID, sourceId);
1406                long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
1407                if (groupId < 0) {
1408                    throw new IllegalStateException("unable to create a new group with "
1409                            + "this sourceid: " + groupValues);
1410                }
1411                return groupId;
1412            }
1413        } finally {
1414            c.close();
1415        }
1416    }
1417
1418    /**
1419     * Delete data row by row so that fixing of primaries etc work correctly.
1420     */
1421    private int deleteData(String selection, String[] selectionArgs,
1422            boolean markRawContactAsDirty) {
1423        int count = 0;
1424
1425        // Note that the query will return data according to the access restrictions,
1426        // so we don't need to worry about deleting data we don't have permission to read.
1427        Cursor c = query(Data.CONTENT_URI, DataQuery.COLUMNS, selection, selectionArgs, null);
1428        try {
1429            while(c.moveToNext()) {
1430                long rawContactId = c.getLong(DataQuery.RAW_CONTACT_ID);
1431                String mimeType = c.getString(DataQuery.MIMETYPE);
1432                count += getDataRowHandler(mimeType).delete(mDb, c);
1433                if (markRawContactAsDirty) {
1434                    setRawContactDirty(rawContactId);
1435                }
1436            }
1437        } finally {
1438            c.close();
1439        }
1440
1441        return count;
1442    }
1443
1444    /**
1445     * Delete a data row provided that it is one of the allowed mime types.
1446     */
1447    public int deleteData(long dataId, String[] allowedMimeTypes) {
1448
1449        // Note that the query will return data according to the access restrictions,
1450        // so we don't need to worry about deleting data we don't have permission to read.
1451        Cursor c = query(Data.CONTENT_URI, DataQuery.COLUMNS, Data._ID + "=" + dataId, null, null);
1452
1453        try {
1454            if (!c.moveToFirst()) {
1455                return 0;
1456            }
1457
1458            String mimeType = c.getString(DataQuery.MIMETYPE);
1459            boolean valid = false;
1460            for (int i = 0; i < allowedMimeTypes.length; i++) {
1461                if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
1462                    valid = true;
1463                    break;
1464                }
1465            }
1466
1467            if (!valid) {
1468                throw new IllegalArgumentException("Data type mismatch: expected "
1469                        + Lists.newArrayList(allowedMimeTypes));
1470            }
1471
1472            return getDataRowHandler(mimeType).delete(mDb, c);
1473        } finally {
1474            c.close();
1475        }
1476    }
1477
1478    /**
1479     * Inserts an item in the groups table
1480     */
1481    private long insertGroup(ContentValues values, Account account, boolean markAsDirty) {
1482        ContentValues overriddenValues = new ContentValues(values);
1483        if (!resolveAccount(overriddenValues, account)) {
1484            return -1;
1485        }
1486
1487        // Replace package with internal mapping
1488        final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
1489        if (packageName != null) {
1490            overriddenValues.put(GroupsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1491        }
1492        overriddenValues.remove(Groups.RES_PACKAGE);
1493
1494        if (markAsDirty) {
1495            overriddenValues.put(Groups.DIRTY, 1);
1496        }
1497
1498        return mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
1499    }
1500
1501    /**
1502     * Inserts a presence update.
1503     */
1504    public long insertPresence(ContentValues values) {
1505        final String handle = values.getAsString(Presence.IM_HANDLE);
1506        final String protocol = values.getAsString(Presence.IM_PROTOCOL);
1507        if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
1508            throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required");
1509        }
1510
1511        // TODO: generalize to allow other providers to match against email
1512        boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == Integer.parseInt(protocol);
1513
1514        StringBuilder selection = new StringBuilder();
1515        String[] selectionArgs;
1516        if (matchEmail) {
1517            selection.append("(" + Clauses.WHERE_IM_MATCHES + ") OR ("
1518                    + Clauses.WHERE_EMAIL_MATCHES + ")");
1519            selectionArgs = new String[] { protocol, handle, handle };
1520        } else {
1521            selection.append(Clauses.WHERE_IM_MATCHES);
1522            selectionArgs = new String[] { protocol, handle };
1523        }
1524
1525        if (values.containsKey(Presence.DATA_ID)) {
1526            selection.append(" AND " + DataColumns.CONCRETE_ID + "=")
1527                    .append(values.getAsLong(Presence.DATA_ID));
1528        }
1529
1530        if (values.containsKey(Presence.RAW_CONTACT_ID)) {
1531            selection.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + "=")
1532                    .append(values.getAsLong(Presence.RAW_CONTACT_ID));
1533        }
1534
1535        selection.append(" AND ").append(getContactsRestrictions());
1536
1537        long dataId = -1;
1538        long rawContactId = -1;
1539
1540        Cursor cursor = null;
1541        try {
1542            cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
1543                    selection.toString(), selectionArgs, null, null, null);
1544            if (cursor.moveToFirst()) {
1545                dataId = cursor.getLong(DataContactsQuery.DATA_ID);
1546                rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
1547            } else {
1548                // No contact found, return a null URI
1549                return -1;
1550            }
1551        } finally {
1552            if (cursor != null) {
1553                cursor.close();
1554            }
1555        }
1556
1557        values.put(Presence.DATA_ID, dataId);
1558        values.put(Presence.RAW_CONTACT_ID, rawContactId);
1559
1560        // Insert the presence update
1561        long presenceId = mDb.replace(Tables.PRESENCE, null, values);
1562        return presenceId;
1563    }
1564
1565    @Override
1566    protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
1567        final int match = sUriMatcher.match(uri);
1568        switch (match) {
1569            case SYNCSTATE:
1570                return mOpenHelper.getSyncState().delete(mDb, selection, selectionArgs);
1571
1572            case CONTACTS_ID: {
1573                long contactId = ContentUris.parseId(uri);
1574
1575                // Remove references to the contact first
1576                ContentValues values = new ContentValues();
1577                values.putNull(RawContacts.CONTACT_ID);
1578                mDb.update(Tables.RAW_CONTACTS, values,
1579                        RawContacts.CONTACT_ID + "=" + contactId, null);
1580
1581                return mDb.delete(Tables.CONTACTS, BaseColumns._ID + "=" + contactId, null);
1582            }
1583
1584            case RAW_CONTACTS_ID: {
1585                return deleteRawContact(uri);
1586            }
1587
1588            case DATA: {
1589                return deleteData(selection, selectionArgs, shouldMarkRawContactAsDirty(uri));
1590            }
1591
1592            case DATA_ID: {
1593                long dataId = ContentUris.parseId(uri);
1594                return deleteData(Data._ID + "=" + dataId, null, shouldMarkRawContactAsDirty(uri));
1595            }
1596
1597            case GROUPS_ID: {
1598                return deleteGroup(uri);
1599            }
1600
1601            case PRESENCE: {
1602                return mDb.delete(Tables.PRESENCE, null, null);
1603            }
1604
1605            default:
1606                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
1607        }
1608    }
1609
1610    private int deleteGroup(Uri uri) {
1611        final String flag = uri.getQueryParameter(Groups.DELETE_PERMANENTLY);
1612        final boolean permanently = flag != null && "true".equals(flag.toLowerCase());
1613        boolean markAsDirty = shouldMarkGroupAsDirty(uri);
1614        return deleteGroup(ContentUris.parseId(uri), markAsDirty, permanently);
1615    }
1616
1617    private int deleteGroup(long groupId, boolean markAsDirty, boolean permanently) {
1618        final long groupMembershipMimetypeId = mOpenHelper
1619                .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
1620        mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
1621                + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
1622                + groupId, null);
1623
1624        try {
1625            if (permanently) {
1626                return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
1627            } else {
1628                mValues.clear();
1629                mValues.put(Groups.DELETED, 1);
1630                if (markAsDirty) {
1631                    mValues.put(Groups.DIRTY, 1);
1632                }
1633                return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
1634            }
1635        } finally {
1636            mOpenHelper.updateAllVisible();
1637        }
1638    }
1639
1640    private int deleteRawContact(Uri uri) {
1641        final String flag = uri.getQueryParameter(RawContacts.DELETE_PERMANENTLY);
1642        final boolean permanently = flag != null && "true".equals(flag.toLowerCase());
1643        final long rawContactId = ContentUris.parseId(uri);
1644        return deleteRawContact(rawContactId, permanently);
1645    }
1646
1647    public int deleteRawContact(long rawContactId, boolean permanently) {
1648        // TODO delete aggregation exceptions
1649        mOpenHelper.removeContactIfSingleton(rawContactId);
1650        if (permanently) {
1651            mDb.delete(Tables.PRESENCE, Presence.RAW_CONTACT_ID + "=" + rawContactId, null);
1652            return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
1653        } else {
1654            mValues.clear();
1655            mValues.put(RawContacts.DELETED, 1);
1656            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
1657            mValues.putNull(RawContacts.CONTACT_ID);
1658            mValues.put(RawContacts.DIRTY, 1);
1659            return updateRawContact(rawContactId, mValues, null, null);
1660        }
1661    }
1662
1663    private static Account readAccountFromQueryParams(Uri uri) {
1664        final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
1665        final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
1666        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
1667            return null;
1668        }
1669        return new Account(name, type);
1670    }
1671
1672    @Override
1673    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
1674            String[] selectionArgs) {
1675        int count = 0;
1676
1677        final int match = sUriMatcher.match(uri);
1678        switch(match) {
1679            case SYNCSTATE:
1680                return mOpenHelper.getSyncState().update(mDb, values, selection, selectionArgs);
1681
1682            // TODO(emillar): We will want to disallow editing the contacts table at some point.
1683            case CONTACTS: {
1684                count = mDb.update(Tables.CONTACTS, values, selection, selectionArgs);
1685                break;
1686            }
1687
1688            case CONTACTS_ID: {
1689                count = updateContactData(ContentUris.parseId(uri), values);
1690                break;
1691            }
1692
1693            case DATA: {
1694                count = updateData(uri, values, selection, selectionArgs,
1695                        shouldMarkRawContactAsDirty(uri));
1696                break;
1697            }
1698
1699            case DATA_ID: {
1700                count = updateData(uri, values, selection, selectionArgs,
1701                        shouldMarkRawContactAsDirty(uri));
1702                break;
1703            }
1704
1705            case RAW_CONTACTS: {
1706
1707                // TODO: security checks
1708                count = mDb.update(Tables.RAW_CONTACTS, values, selection, selectionArgs);
1709                break;
1710            }
1711
1712            case RAW_CONTACTS_ID: {
1713                long rawContactId = ContentUris.parseId(uri);
1714                count = updateRawContact(rawContactId, values, selection, selectionArgs);
1715                break;
1716            }
1717
1718            case GROUPS: {
1719                count = updateGroups(values, selection, selectionArgs,
1720                        shouldMarkGroupAsDirty(uri));
1721                break;
1722            }
1723
1724            case GROUPS_ID: {
1725                long groupId = ContentUris.parseId(uri);
1726                String selectionWithId = (Groups._ID + "=" + groupId + " ")
1727                        + (selection == null ? "" : " AND " + selection);
1728                count = updateGroups(values, selectionWithId, selectionArgs,
1729                        shouldMarkGroupAsDirty(uri));
1730                break;
1731            }
1732
1733            case AGGREGATION_EXCEPTIONS: {
1734                count = updateAggregationException(mDb, values);
1735                break;
1736            }
1737
1738            default:
1739                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
1740        }
1741
1742        return count;
1743    }
1744
1745    private int updateGroups(ContentValues values, String selectionWithId,
1746            String[] selectionArgs, boolean markAsDirty) {
1747
1748        ContentValues updatedValues;
1749        if (markAsDirty) {
1750            updatedValues = mValues;
1751            updatedValues.clear();
1752            updatedValues.putAll(values);
1753            updatedValues.put(Groups.DIRTY, 1);
1754        } else {
1755            updatedValues = values;
1756        }
1757
1758        int count = mDb.update(Tables.GROUPS, values, selectionWithId, selectionArgs);
1759
1760        // If changing visibility, then update contacts
1761        if (values.containsKey(Groups.GROUP_VISIBLE)) {
1762            mOpenHelper.updateAllVisible();
1763        }
1764        return count;
1765    }
1766
1767    private int updateRawContact(long rawContactId, ContentValues values, String selection,
1768            String[] selectionArgs) {
1769
1770        // TODO: security checks
1771        String selectionWithId = (RawContacts._ID + " = " + rawContactId + " ")
1772                + (selection == null ? "" : " AND " + selection);
1773        return mDb.update(Tables.RAW_CONTACTS, values, selectionWithId, selectionArgs);
1774    }
1775
1776    private int updateData(Uri uri, ContentValues values, String selection,
1777            String[] selectionArgs, boolean markRawContactAsDirty) {
1778        int count = 0;
1779
1780        // Note that the query will return data according to the access restrictions,
1781        // so we don't need to worry about updating data we don't have permission to read.
1782        Cursor c = query(uri, DataIdQuery.COLUMNS, selection, selectionArgs, null);
1783        try {
1784            while(c.moveToNext()) {
1785                final long dataId = c.getLong(DataIdQuery._ID);
1786                final long rawContactId = c.getLong(DataIdQuery.RAW_CONTACT_ID);
1787                final String mimetype = c.getString(DataIdQuery.MIMETYPE);
1788                count += updateData(dataId, rawContactId, mimetype, values,
1789                        markRawContactAsDirty);
1790            }
1791        } finally {
1792            c.close();
1793        }
1794
1795        return count;
1796    }
1797
1798    private int updateData(long dataId, long rawContactId, String mimeType, ContentValues values,
1799            boolean markRawContactAsDirty) {
1800        mValues.clear();
1801        mValues.putAll(values);
1802        mValues.remove(Data._ID);
1803        mValues.remove(Data.RAW_CONTACT_ID);
1804        mValues.remove(Data.MIMETYPE);
1805
1806        String packageName = values.getAsString(Data.RES_PACKAGE);
1807        if (packageName != null) {
1808            mValues.remove(Data.RES_PACKAGE);
1809            mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1810        }
1811
1812        boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
1813        boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
1814
1815        // Remove primary or super primary values being set to 0. This is disallowed by the
1816        // content provider.
1817        if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
1818            containsIsSuperPrimary = false;
1819            mValues.remove(Data.IS_SUPER_PRIMARY);
1820        }
1821        if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
1822            containsIsPrimary = false;
1823            mValues.remove(Data.IS_PRIMARY);
1824        }
1825
1826        if (containsIsSuperPrimary) {
1827            setIsSuperPrimary(dataId);
1828            setIsPrimary(dataId);
1829
1830            // Now that we've taken care of setting these, remove them from "values".
1831            mValues.remove(Data.IS_SUPER_PRIMARY);
1832            if (containsIsPrimary) {
1833                mValues.remove(Data.IS_PRIMARY);
1834            }
1835        } else if (containsIsPrimary) {
1836            setIsPrimary(dataId);
1837
1838            // Now that we've taken care of setting this, remove it from "values".
1839            mValues.remove(Data.IS_PRIMARY);
1840        }
1841
1842        // TODO create GroupMembershipRowHandler and move this code there
1843        resolveGroupSourceIdInValues(rawContactId, mimeType, mDb, mValues, false /* isInsert */);
1844
1845        if (mValues.size() > 0) {
1846            mDb.update(Tables.DATA, mValues, Data._ID + " = " + dataId, null);
1847            if (markRawContactAsDirty) {
1848                setRawContactDirty(rawContactId);
1849            }
1850
1851            return 1;
1852        }
1853        return 0;
1854    }
1855
1856    private void resolveGroupSourceIdInValues(long rawContactId, String mimeType, SQLiteDatabase db,
1857            ContentValues values, boolean isInsert) {
1858        if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
1859            boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
1860            boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
1861            if (containsGroupSourceId && containsGroupId) {
1862                throw new IllegalArgumentException(
1863                        "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
1864                                + "and GroupMembership.GROUP_ROW_ID");
1865            }
1866
1867            if (!containsGroupSourceId && !containsGroupId) {
1868                if (isInsert) {
1869                    throw new IllegalArgumentException(
1870                            "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
1871                                    + "and GroupMembership.GROUP_ROW_ID");
1872                } else {
1873                    return;
1874                }
1875            }
1876
1877            if (containsGroupSourceId) {
1878                final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
1879                final long groupId = getOrMakeGroup(db, rawContactId, sourceId);
1880                values.remove(GroupMembership.GROUP_SOURCE_ID);
1881                values.put(GroupMembership.GROUP_ROW_ID, groupId);
1882            }
1883        }
1884    }
1885
1886    private int updateContactData(long contactId, ContentValues values) {
1887
1888        // First update all constituent contacts
1889        ContentValues optionValues = new ContentValues(5);
1890        OpenHelper.copyStringValue(optionValues, RawContacts.CUSTOM_RINGTONE,
1891                values, Contacts.CUSTOM_RINGTONE);
1892        OpenHelper.copyLongValue(optionValues, RawContacts.SEND_TO_VOICEMAIL,
1893                values, Contacts.SEND_TO_VOICEMAIL);
1894        OpenHelper.copyLongValue(optionValues, RawContacts.LAST_TIME_CONTACTED,
1895                values, Contacts.LAST_TIME_CONTACTED);
1896        OpenHelper.copyLongValue(optionValues, RawContacts.TIMES_CONTACTED,
1897                values, Contacts.TIMES_CONTACTED);
1898        OpenHelper.copyLongValue(optionValues, RawContacts.STARRED,
1899                values, Contacts.STARRED);
1900
1901        // Nothing to update - just return
1902        if (optionValues.size() == 0) {
1903            return 0;
1904        }
1905
1906        mDb.update(Tables.RAW_CONTACTS, optionValues,
1907                RawContacts.CONTACT_ID + "=" + contactId, null);
1908        return mDb.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
1909    }
1910
1911    public void updateContactTime(long contactId, long lastTimeContacted) {
1912        mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
1913        mLastTimeContactedUpdate.bindLong(2, contactId);
1914        mLastTimeContactedUpdate.execute();
1915    }
1916
1917    private static class RawContactPair {
1918        final long rawContactId1;
1919        final long rawContactId2;
1920
1921        /**
1922         * Constructor that ensures that this.rawContactId1 &lt; this.rawContactId2
1923         */
1924        public RawContactPair(long rawContactId1, long rawContactId2) {
1925            if (rawContactId1 < rawContactId2) {
1926                this.rawContactId1 = rawContactId1;
1927                this.rawContactId2 = rawContactId2;
1928            } else {
1929                this.rawContactId2 = rawContactId1;
1930                this.rawContactId1 = rawContactId2;
1931            }
1932        }
1933    }
1934
1935    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
1936        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
1937        long contactId = values.getAsInteger(AggregationExceptions.CONTACT_ID);
1938        long rawContactId = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID);
1939
1940        // First, we build a list of rawContactID-rawContactID pairs for the given contact.
1941        ArrayList<RawContactPair> pairs = new ArrayList<RawContactPair>();
1942        Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts.CONTACT_ID
1943                + "=" + contactId, null, null, null, null);
1944        try {
1945            while (c.moveToNext()) {
1946                long aggregatedContactId = c.getLong(ContactsQuery.RAW_CONTACT_ID);
1947                if (aggregatedContactId != rawContactId) {
1948                    pairs.add(new RawContactPair(aggregatedContactId, rawContactId));
1949                }
1950            }
1951        } finally {
1952            c.close();
1953        }
1954
1955        // Now we iterate through all contact pairs to see if we need to insert/delete/update
1956        // the corresponding exception
1957        ContentValues exceptionValues = new ContentValues(3);
1958        exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
1959        for (RawContactPair pair : pairs) {
1960            final String whereClause =
1961                    AggregationExceptionColumns.RAW_CONTACT_ID1 + "=" + pair.rawContactId1 + " AND "
1962                    + AggregationExceptionColumns.RAW_CONTACT_ID2 + "=" + pair.rawContactId2;
1963            if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
1964                db.delete(Tables.AGGREGATION_EXCEPTIONS, whereClause, null);
1965            } else {
1966                exceptionValues.put(AggregationExceptionColumns.RAW_CONTACT_ID1, pair.rawContactId1);
1967                exceptionValues.put(AggregationExceptionColumns.RAW_CONTACT_ID2, pair.rawContactId2);
1968                db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
1969                        exceptionValues);
1970            }
1971        }
1972
1973        int aggregationMode = mContactAggregator.markContactForAggregation(mDb, rawContactId);
1974        if (aggregationMode != RawContacts.AGGREGATION_MODE_DISABLED) {
1975            mContactAggregator.aggregateContact(db, rawContactId);
1976            if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC
1977                    || exceptionType == AggregationExceptions.TYPE_KEEP_OUT) {
1978                mContactAggregator.updateAggregateData(contactId);
1979            }
1980        }
1981
1982        // The return value is fake - we just confirm that we made a change, not count actual
1983        // rows changed.
1984        return 1;
1985    }
1986
1987    /**
1988     * Test if a {@link String} value appears in the given list.
1989     */
1990    private boolean isContained(String[] array, String value) {
1991        if (array != null) {
1992            for (String test : array) {
1993                if (value.equals(test)) {
1994                    return true;
1995                }
1996            }
1997        }
1998        return false;
1999    }
2000
2001    /**
2002     * Test if a {@link String} value appears in the given list, and add to the
2003     * array if the value doesn't already appear.
2004     */
2005    private String[] assertContained(String[] array, String value) {
2006        if (array != null && !isContained(array, value)) {
2007            String[] newArray = new String[array.length + 1];
2008            System.arraycopy(array, 0, newArray, 0, array.length);
2009            newArray[array.length] = value;
2010            array = newArray;
2011        }
2012        return array;
2013    }
2014
2015    @Override
2016    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
2017            String sortOrder) {
2018        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2019
2020        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2021        String groupBy = null;
2022        String limit = getLimit(uri);
2023
2024        // TODO: Consider writing a test case for RestrictionExceptions when you
2025        // write a new query() block to make sure it protects restricted data.
2026        final int match = sUriMatcher.match(uri);
2027        switch (match) {
2028            case SYNCSTATE:
2029                return mOpenHelper.getSyncState().query(db, projection, selection,  selectionArgs,
2030                        sortOrder);
2031
2032            case CONTACTS: {
2033                qb.setTables(mOpenHelper.getContactView());
2034                qb.setProjectionMap(sContactsProjectionMap);
2035                break;
2036            }
2037
2038            case CONTACTS_ID: {
2039                long contactId = ContentUris.parseId(uri);
2040                qb.setTables(mOpenHelper.getContactView());
2041                qb.setProjectionMap(sContactsProjectionMap);
2042                qb.appendWhere(Contacts._ID + "=" + contactId);
2043                break;
2044            }
2045
2046            case CONTACTS_SUMMARY: {
2047                // TODO: join into social status tables
2048                qb.setTables(mOpenHelper.getContactSummaryView());
2049                qb.setProjectionMap(sContactsSummaryProjectionMap);
2050                groupBy = Contacts._ID;
2051                break;
2052            }
2053
2054            case CONTACTS_SUMMARY_ID: {
2055                // TODO: join into social status tables
2056                long contactId = ContentUris.parseId(uri);
2057                qb.setTables(mOpenHelper.getContactSummaryView());
2058                qb.setProjectionMap(sContactsSummaryProjectionMap);
2059                groupBy = Contacts._ID;
2060                qb.appendWhere(Contacts._ID + "=" + contactId);
2061                break;
2062            }
2063
2064            case CONTACTS_SUMMARY_FILTER: {
2065                qb.setTables(mOpenHelper.getContactSummaryView());
2066                qb.setProjectionMap(sContactsSummaryProjectionMap);
2067                groupBy = Contacts._ID;
2068
2069                if (uri.getPathSegments().size() > 2) {
2070                    String filterParam = uri.getLastPathSegment();
2071                    StringBuilder sb = new StringBuilder();
2072                    sb.append("raw_contact_id IN ");
2073                    appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
2074                    qb.appendWhere(sb.toString());
2075                }
2076                break;
2077            }
2078
2079            case CONTACTS_SUMMARY_STREQUENT_FILTER:
2080            case CONTACTS_SUMMARY_STREQUENT: {
2081                String filterSql = null;
2082                if (match == CONTACTS_SUMMARY_STREQUENT_FILTER
2083                        && uri.getPathSegments().size() > 3) {
2084                    String filterParam = uri.getLastPathSegment();
2085                    StringBuilder sb = new StringBuilder();
2086                    sb.append("raw_contact_id IN ");
2087                    appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
2088                    filterSql = sb.toString();
2089                }
2090
2091                // Build the first query for starred
2092                qb.setTables(mOpenHelper.getContactSummaryView());
2093                qb.setProjectionMap(sContactsSummaryProjectionMap);
2094                if (filterSql != null) {
2095                    qb.appendWhere(filterSql);
2096                }
2097                final String starredQuery = qb.buildQuery(projection, Contacts.STARRED + "=1",
2098                        null, Contacts._ID, null, null, null);
2099
2100                // Build the second query for frequent
2101                qb = new SQLiteQueryBuilder();
2102                qb.setTables(mOpenHelper.getContactSummaryView());
2103                qb.setProjectionMap(sContactsSummaryProjectionMap);
2104                if (filterSql != null) {
2105                    qb.appendWhere(filterSql);
2106                }
2107                final String frequentQuery = qb.buildQuery(projection,
2108                        Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
2109                        + " = 0 OR " + Contacts.STARRED + " IS NULL)",
2110                        null, Contacts._ID, null, null, null);
2111
2112                // Put them together
2113                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
2114                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
2115                Cursor c = db.rawQuery(query, null);
2116                if (c != null) {
2117                    c.setNotificationUri(getContext().getContentResolver(),
2118                            ContactsContract.AUTHORITY_URI);
2119                }
2120                return c;
2121            }
2122
2123            case CONTACTS_SUMMARY_GROUP: {
2124                qb.setTables(mOpenHelper.getContactSummaryView());
2125                qb.setProjectionMap(sContactsSummaryProjectionMap);
2126                if (uri.getPathSegments().size() > 2) {
2127                    qb.appendWhere(sContactsInGroupSelect);
2128                    selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
2129                }
2130                groupBy = Contacts._ID;
2131                break;
2132            }
2133
2134            case CONTACTS_DATA: {
2135                long contactId = Long.parseLong(uri.getPathSegments().get(1));
2136
2137                qb.setTables(mOpenHelper.getDataView());
2138                qb.setProjectionMap(sDataProjectionMap);
2139                appendAccountFromParameter(qb, uri);
2140                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
2141                break;
2142            }
2143
2144            case PHONES: {
2145                qb.setTables(mOpenHelper.getDataView());
2146                qb.setProjectionMap(sDataProjectionMap);
2147                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
2148                break;
2149            }
2150
2151            case PHONES_FILTER: {
2152                qb.setTables(mOpenHelper.getDataView());
2153                qb.setProjectionMap(sDataProjectionMap);
2154                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
2155                if (uri.getPathSegments().size() > 2) {
2156                    String filterParam = uri.getLastPathSegment();
2157                    StringBuilder sb = new StringBuilder();
2158                    sb.append(Data.RAW_CONTACT_ID + " IN ");
2159                    appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
2160                    qb.appendWhere(" AND " + sb);
2161                }
2162                break;
2163            }
2164
2165            case EMAILS: {
2166                qb.setTables(mOpenHelper.getDataView());
2167                qb.setProjectionMap(sDataProjectionMap);
2168                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
2169                break;
2170            }
2171
2172            case EMAILS_FILTER: {
2173                qb.setTables(mOpenHelper.getDataView());
2174                qb.setProjectionMap(sDataProjectionMap);
2175                qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
2176                if (uri.getPathSegments().size() > 2) {
2177                    qb.appendWhere(" AND " + CommonDataKinds.Email.DATA + "=");
2178                    qb.appendWhereEscapeString(uri.getLastPathSegment());
2179                }
2180                break;
2181            }
2182
2183            case POSTALS: {
2184                qb.setTables(mOpenHelper.getDataView());
2185                qb.setProjectionMap(sDataProjectionMap);
2186                qb.appendWhere(Data.MIMETYPE + " = '" + StructuredPostal.CONTENT_ITEM_TYPE + "'");
2187                break;
2188            }
2189
2190            case RAW_CONTACTS: {
2191                qb.setTables(mOpenHelper.getRawContactView());
2192                qb.setProjectionMap(sRawContactsProjectionMap);
2193                break;
2194            }
2195
2196            case RAW_CONTACTS_ID: {
2197                long rawContactId = ContentUris.parseId(uri);
2198                qb.setTables(mOpenHelper.getRawContactView());
2199                qb.setProjectionMap(sRawContactsProjectionMap);
2200                qb.appendWhere(RawContacts._ID + "=" + rawContactId);
2201                break;
2202            }
2203
2204            case RAW_CONTACTS_DATA: {
2205                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
2206                qb.setTables(mOpenHelper.getDataView());
2207                qb.setProjectionMap(sDataProjectionMap);
2208                qb.appendWhere(Data.RAW_CONTACT_ID + "=" + rawContactId);
2209                break;
2210            }
2211
2212            case DATA: {
2213                qb.setTables(mOpenHelper.getDataView());
2214                qb.setProjectionMap(sDataProjectionMap);
2215                appendAccountFromParameter(qb, uri);
2216                break;
2217            }
2218
2219            case DATA_ID: {
2220                qb.setTables(mOpenHelper.getDataView());
2221                qb.setProjectionMap(sDataProjectionMap);
2222                qb.appendWhere(Data._ID + "=" + ContentUris.parseId(uri));
2223                break;
2224            }
2225
2226            case PHONE_LOOKUP: {
2227
2228                // TODO: optimize this query returning fewer fields and using an int mimetype
2229                // TODO: filter query based on callingUid
2230                if (TextUtils.isEmpty(sortOrder)) {
2231                    // Default the sort order to something reasonable so we get consistent
2232                    // results when callers don't request an ordering
2233                    sortOrder = Data.RAW_CONTACT_ID;
2234                }
2235
2236                final String number = uri.getLastPathSegment();
2237                OpenHelper.buildPhoneLookupQuery(qb, number);
2238                qb.setProjectionMap(sDataRawContactsProjectionMap);
2239                break;
2240            }
2241
2242            case GROUPS: {
2243                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2244                qb.setProjectionMap(sGroupsProjectionMap);
2245                break;
2246            }
2247
2248            case GROUPS_ID: {
2249                long groupId = ContentUris.parseId(uri);
2250                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2251                qb.setProjectionMap(sGroupsProjectionMap);
2252                qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId);
2253                break;
2254            }
2255
2256            case GROUPS_SUMMARY: {
2257                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2258                qb.setProjectionMap(sGroupsSummaryProjectionMap);
2259                groupBy = GroupsColumns.CONCRETE_ID;
2260                break;
2261            }
2262
2263            case AGGREGATION_EXCEPTIONS: {
2264                qb.setTables(Tables.AGGREGATION_EXCEPTIONS_JOIN_RAW_CONTACTS);
2265                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
2266                break;
2267            }
2268
2269            case AGGREGATION_SUGGESTIONS: {
2270                long contactId = Long.parseLong(uri.getPathSegments().get(1));
2271                final int maxSuggestions;
2272                if (limit != null) {
2273                    maxSuggestions = Integer.parseInt(limit);
2274                } else {
2275                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
2276                }
2277
2278                return mContactAggregator.queryAggregationSuggestions(contactId, projection,
2279                        sContactsProjectionMap, maxSuggestions);
2280            }
2281
2282            case PRESENCE: {
2283                qb.setTables(Tables.PRESENCE);
2284                qb.setProjectionMap(sPresenceProjectionMap);
2285                break;
2286            }
2287
2288            case PRESENCE_ID: {
2289                qb.setTables(Tables.PRESENCE);
2290                qb.setProjectionMap(sPresenceProjectionMap);
2291                qb.appendWhere(Presence._ID + "=" + ContentUris.parseId(uri));
2292                break;
2293            }
2294
2295            case SEARCH_SUGGESTIONS: {
2296                return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
2297            }
2298
2299            case SEARCH_SHORTCUT: {
2300                // TODO
2301                break;
2302            }
2303
2304            default:
2305                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
2306                        sortOrder, limit);
2307        }
2308
2309        // Perform the query and set the notification uri
2310        final Cursor c = qb.query(db, projection, selection, selectionArgs,
2311                groupBy, null, sortOrder, limit);
2312        if (c != null) {
2313            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
2314        }
2315        return c;
2316    }
2317
2318    private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
2319        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
2320        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
2321        if (!TextUtils.isEmpty(accountName)) {
2322            qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
2323                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2324                    + RawContacts.ACCOUNT_TYPE + "="
2325                    + DatabaseUtils.sqlEscapeString(accountType));
2326        } else {
2327            qb.appendWhere("1");
2328        }
2329    }
2330
2331    /**
2332     * Gets the value of the "limit" URI query parameter.
2333     *
2334     * @return A string containing a non-negative integer, or <code>null</code> if
2335     *         the parameter is not set, or is set to an invalid value.
2336     */
2337    private String getLimit(Uri url) {
2338        String limitParam = url.getQueryParameter("limit");
2339        if (limitParam == null) {
2340            return null;
2341        }
2342        // make sure that the limit is a non-negative integer
2343        try {
2344            int l = Integer.parseInt(limitParam);
2345            if (l < 0) {
2346                Log.w(TAG, "Invalid limit parameter: " + limitParam);
2347                return null;
2348            }
2349            return String.valueOf(l);
2350        } catch (NumberFormatException ex) {
2351            Log.w(TAG, "Invalid limit parameter: " + limitParam);
2352            return null;
2353        }
2354    }
2355
2356    String getContactsRestrictions() {
2357        if (mOpenHelper.hasRestrictedAccess()) {
2358            return "1";
2359        } else {
2360            return RawContacts.IS_RESTRICTED + "=0";
2361        }
2362    }
2363
2364    public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
2365        if (mOpenHelper.hasRestrictedAccess()) {
2366            return "1";
2367        } else {
2368            return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
2369                    + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
2370        }
2371    }
2372
2373    /**
2374     * An implementation of EntityIterator that joins the contacts and data tables
2375     * and consumes all the data rows for a contact in order to build the Entity for a contact.
2376     */
2377    private static class ContactsEntityIterator implements EntityIterator {
2378        private final Cursor mEntityCursor;
2379        private volatile boolean mIsClosed;
2380
2381        private static final String[] DATA_KEYS = new String[]{
2382                Data.DATA1,
2383                Data.DATA2,
2384                Data.DATA3,
2385                Data.DATA4,
2386                Data.DATA5,
2387                Data.DATA6,
2388                Data.DATA7,
2389                Data.DATA8,
2390                Data.DATA9,
2391                Data.DATA10,
2392                Data.DATA11,
2393                Data.DATA12,
2394                Data.DATA13,
2395                Data.DATA14,
2396                Data.DATA15,
2397                Data.SYNC1,
2398                Data.SYNC2,
2399                Data.SYNC3,
2400                Data.SYNC4};
2401
2402        private static final String[] PROJECTION = new String[]{
2403                RawContacts.ACCOUNT_NAME,
2404                RawContacts.ACCOUNT_TYPE,
2405                RawContacts.SOURCE_ID,
2406                RawContacts.VERSION,
2407                RawContacts.DIRTY,
2408                Data._ID,
2409                Data.RES_PACKAGE,
2410                Data.MIMETYPE,
2411                Data.DATA1,
2412                Data.DATA2,
2413                Data.DATA3,
2414                Data.DATA4,
2415                Data.DATA5,
2416                Data.DATA6,
2417                Data.DATA7,
2418                Data.DATA8,
2419                Data.DATA9,
2420                Data.DATA10,
2421                Data.DATA11,
2422                Data.DATA12,
2423                Data.DATA13,
2424                Data.DATA14,
2425                Data.DATA15,
2426                Data.SYNC1,
2427                Data.SYNC2,
2428                Data.SYNC3,
2429                Data.SYNC4,
2430                Data.RAW_CONTACT_ID,
2431                Data.IS_PRIMARY,
2432                Data.DATA_VERSION,
2433                GroupMembership.GROUP_SOURCE_ID,
2434                RawContacts.SYNC1,
2435                RawContacts.SYNC2,
2436                RawContacts.SYNC3,
2437                RawContacts.SYNC4,
2438                RawContacts.DELETED};
2439
2440        private static final int COLUMN_ACCOUNT_NAME = 0;
2441        private static final int COLUMN_ACCOUNT_TYPE = 1;
2442        private static final int COLUMN_SOURCE_ID = 2;
2443        private static final int COLUMN_VERSION = 3;
2444        private static final int COLUMN_DIRTY = 4;
2445        private static final int COLUMN_DATA_ID = 5;
2446        private static final int COLUMN_RES_PACKAGE = 6;
2447        private static final int COLUMN_MIMETYPE = 7;
2448        private static final int COLUMN_DATA1 = 8;
2449        private static final int COLUMN_RAW_CONTACT_ID = 27;
2450        private static final int COLUMN_IS_PRIMARY = 28;
2451        private static final int COLUMN_DATA_VERSION = 29;
2452        private static final int COLUMN_GROUP_SOURCE_ID = 30;
2453        private static final int COLUMN_SYNC1 = 31;
2454        private static final int COLUMN_SYNC2 = 32;
2455        private static final int COLUMN_SYNC3 = 33;
2456        private static final int COLUMN_SYNC4 = 34;
2457        private static final int COLUMN_DELETED = 35;
2458
2459        public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri,
2460                String selection, String[] selectionArgs, String sortOrder) {
2461            mIsClosed = false;
2462
2463            final String updatedSortOrder = (sortOrder == null)
2464                    ? Data.RAW_CONTACT_ID
2465                    : (Data.RAW_CONTACT_ID + "," + sortOrder);
2466
2467            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
2468            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2469            qb.setTables(Tables.CONTACT_ENTITIES);
2470            if (contactsIdString != null) {
2471                qb.appendWhere(Data.RAW_CONTACT_ID + "=" + contactsIdString);
2472            }
2473            final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
2474            final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
2475            if (!TextUtils.isEmpty(accountName)) {
2476                qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
2477                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2478                        + RawContacts.ACCOUNT_TYPE + "="
2479                        + DatabaseUtils.sqlEscapeString(accountType));
2480            }
2481            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
2482                    null, null, updatedSortOrder);
2483            mEntityCursor.moveToFirst();
2484        }
2485
2486        public void close() {
2487            if (mIsClosed) {
2488                throw new IllegalStateException("closing when already closed");
2489            }
2490            mIsClosed = true;
2491            mEntityCursor.close();
2492        }
2493
2494        public boolean hasNext() throws RemoteException {
2495            if (mIsClosed) {
2496                throw new IllegalStateException("calling hasNext() when the iterator is closed");
2497            }
2498
2499            return !mEntityCursor.isAfterLast();
2500        }
2501
2502        public Entity next() throws RemoteException {
2503            if (mIsClosed) {
2504                throw new IllegalStateException("calling next() when the iterator is closed");
2505            }
2506            if (!hasNext()) {
2507                throw new IllegalStateException("you may only call next() if hasNext() is true");
2508            }
2509
2510            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
2511
2512            final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID);
2513
2514            // we expect the cursor is already at the row we need to read from
2515            ContentValues contactValues = new ContentValues();
2516            contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
2517            contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
2518            contactValues.put(RawContacts._ID, rawContactId);
2519            contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY));
2520            contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION));
2521            contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
2522            contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1));
2523            contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2));
2524            contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3));
2525            contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4));
2526            contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED));
2527            Entity contact = new Entity(contactValues);
2528
2529            // read data rows until the contact id changes
2530            do {
2531                if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) {
2532                    break;
2533                }
2534                // add the data to to the contact
2535                ContentValues dataValues = new ContentValues();
2536                dataValues.put(Data._ID, c.getString(COLUMN_DATA_ID));
2537                dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
2538                dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
2539                dataValues.put(Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY));
2540                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
2541                if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
2542                    dataValues.put(GroupMembership.GROUP_SOURCE_ID,
2543                            c.getString(COLUMN_GROUP_SOURCE_ID));
2544                }
2545                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
2546                for (int i = 0; i < DATA_KEYS.length; i++) {
2547                    final int columnIndex = i + COLUMN_DATA1;
2548                    String key = DATA_KEYS[i];
2549                    if (c.isNull(columnIndex)) {
2550                        // don't put anything
2551                    } else if (c.isLong(columnIndex)) {
2552                        dataValues.put(key, c.getLong(columnIndex));
2553                    } else if (c.isFloat(columnIndex)) {
2554                        dataValues.put(key, c.getFloat(columnIndex));
2555                    } else if (c.isString(columnIndex)) {
2556                        dataValues.put(key, c.getString(columnIndex));
2557                    } else if (c.isBlob(columnIndex)) {
2558                        dataValues.put(key, c.getBlob(columnIndex));
2559                    }
2560                }
2561                contact.addSubValue(Data.CONTENT_URI, dataValues);
2562            } while (mEntityCursor.moveToNext());
2563
2564            return contact;
2565        }
2566    }
2567
2568    /**
2569     * An implementation of EntityIterator that joins the contacts and data tables
2570     * and consumes all the data rows for a contact in order to build the Entity for a contact.
2571     */
2572    private static class GroupsEntityIterator implements EntityIterator {
2573        private final Cursor mEntityCursor;
2574        private volatile boolean mIsClosed;
2575
2576        private static final String[] PROJECTION = new String[]{
2577                Groups._ID,
2578                Groups.ACCOUNT_NAME,
2579                Groups.ACCOUNT_TYPE,
2580                Groups.SOURCE_ID,
2581                Groups.DIRTY,
2582                Groups.VERSION,
2583                Groups.RES_PACKAGE,
2584                Groups.TITLE,
2585                Groups.TITLE_RES,
2586                Groups.GROUP_VISIBLE,
2587                Groups.SYNC1,
2588                Groups.SYNC2,
2589                Groups.SYNC3,
2590                Groups.SYNC4,
2591                Groups.SYSTEM_ID,
2592                Groups.NOTES,
2593                Groups.DELETED};
2594
2595        private static final int COLUMN_ID = 0;
2596        private static final int COLUMN_ACCOUNT_NAME = 1;
2597        private static final int COLUMN_ACCOUNT_TYPE = 2;
2598        private static final int COLUMN_SOURCE_ID = 3;
2599        private static final int COLUMN_DIRTY = 4;
2600        private static final int COLUMN_VERSION = 5;
2601        private static final int COLUMN_RES_PACKAGE = 6;
2602        private static final int COLUMN_TITLE = 7;
2603        private static final int COLUMN_TITLE_RES = 8;
2604        private static final int COLUMN_GROUP_VISIBLE = 9;
2605        private static final int COLUMN_SYNC1 = 10;
2606        private static final int COLUMN_SYNC2 = 11;
2607        private static final int COLUMN_SYNC3 = 12;
2608        private static final int COLUMN_SYNC4 = 13;
2609        private static final int COLUMN_SYSTEM_ID = 14;
2610        private static final int COLUMN_NOTES = 15;
2611        private static final int COLUMN_DELETED = 16;
2612
2613        public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri,
2614                String selection, String[] selectionArgs, String sortOrder) {
2615            mIsClosed = false;
2616
2617            final String updatedSortOrder = (sortOrder == null)
2618                    ? Groups._ID
2619                    : (Groups._ID + "," + sortOrder);
2620
2621            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
2622            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2623            qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2624            qb.setProjectionMap(sGroupsProjectionMap);
2625            if (groupIdString != null) {
2626                qb.appendWhere(Groups._ID + "=" + groupIdString);
2627            }
2628            final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
2629            final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
2630            if (!TextUtils.isEmpty(accountName)) {
2631                qb.appendWhere(Groups.ACCOUNT_NAME + "="
2632                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2633                        + Groups.ACCOUNT_TYPE + "="
2634                        + DatabaseUtils.sqlEscapeString(accountType));
2635            }
2636            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
2637                    null, null, updatedSortOrder);
2638            mEntityCursor.moveToFirst();
2639        }
2640
2641        public void close() {
2642            if (mIsClosed) {
2643                throw new IllegalStateException("closing when already closed");
2644            }
2645            mIsClosed = true;
2646            mEntityCursor.close();
2647        }
2648
2649        public boolean hasNext() throws RemoteException {
2650            if (mIsClosed) {
2651                throw new IllegalStateException("calling hasNext() when the iterator is closed");
2652            }
2653
2654            return !mEntityCursor.isAfterLast();
2655        }
2656
2657        public Entity next() throws RemoteException {
2658            if (mIsClosed) {
2659                throw new IllegalStateException("calling next() when the iterator is closed");
2660            }
2661            if (!hasNext()) {
2662                throw new IllegalStateException("you may only call next() if hasNext() is true");
2663            }
2664
2665            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
2666
2667            final long groupId = c.getLong(COLUMN_ID);
2668
2669            // we expect the cursor is already at the row we need to read from
2670            ContentValues groupValues = new ContentValues();
2671            groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
2672            groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
2673            groupValues.put(Groups._ID, groupId);
2674            groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY));
2675            groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION));
2676            groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
2677            groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
2678            groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE));
2679            groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES));
2680            groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE));
2681            groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1));
2682            groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2));
2683            groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3));
2684            groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4));
2685            groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID));
2686            groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED));
2687            groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES));
2688            Entity group = new Entity(groupValues);
2689
2690            mEntityCursor.moveToNext();
2691
2692            return group;
2693        }
2694    }
2695
2696    @Override
2697    public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
2698            String sortOrder) {
2699        final int match = sUriMatcher.match(uri);
2700        switch (match) {
2701            case RAW_CONTACTS:
2702            case RAW_CONTACTS_ID:
2703                String contactsIdString = null;
2704                if (match == RAW_CONTACTS_ID) {
2705                    contactsIdString = uri.getPathSegments().get(1);
2706                }
2707
2708                return new ContactsEntityIterator(this, contactsIdString,
2709                        uri, selection, selectionArgs, sortOrder);
2710            case GROUPS:
2711            case GROUPS_ID:
2712                String idString = null;
2713                if (match == GROUPS_ID) {
2714                    idString = uri.getPathSegments().get(1);
2715                }
2716
2717                return new GroupsEntityIterator(this, idString,
2718                        uri, selection, selectionArgs, sortOrder);
2719            default:
2720                throw new UnsupportedOperationException("Unknown uri: " + uri);
2721        }
2722    }
2723
2724    @Override
2725    public String getType(Uri uri) {
2726        final int match = sUriMatcher.match(uri);
2727        switch (match) {
2728            case CONTACTS: return Contacts.CONTENT_TYPE;
2729            case CONTACTS_ID: return Contacts.CONTENT_ITEM_TYPE;
2730            case RAW_CONTACTS: return RawContacts.CONTENT_TYPE;
2731            case RAW_CONTACTS_ID: return RawContacts.CONTENT_ITEM_TYPE;
2732            case DATA_ID:
2733                final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2734                long dataId = ContentUris.parseId(uri);
2735                return mOpenHelper.getDataMimeType(dataId);
2736            case AGGREGATION_EXCEPTIONS: return AggregationExceptions.CONTENT_TYPE;
2737            case AGGREGATION_EXCEPTION_ID: return AggregationExceptions.CONTENT_ITEM_TYPE;
2738            case AGGREGATION_SUGGESTIONS: return Contacts.CONTENT_TYPE;
2739            case SEARCH_SUGGESTIONS:
2740                return SearchManager.SUGGEST_MIME_TYPE;
2741            case SEARCH_SHORTCUT:
2742                return SearchManager.SHORTCUT_MIME_TYPE;
2743        }
2744        throw new UnsupportedOperationException("Unknown uri: " + uri);
2745    }
2746
2747    private void setDisplayName(long rawContactId, String displayName) {
2748        if (displayName != null) {
2749            mContactDisplayNameUpdate.bindString(1, displayName);
2750        } else {
2751            mContactDisplayNameUpdate.bindNull(1);
2752        }
2753        mContactDisplayNameUpdate.bindLong(2, rawContactId);
2754        mContactDisplayNameUpdate.execute();
2755    }
2756
2757    /**
2758     * Checks the {@link Data#MARK_AS_DIRTY} query parameter.
2759     *
2760     * Returns true if the parameter is missing or is either "true" or "1".
2761     */
2762    private boolean shouldMarkRawContactAsDirty(Uri uri) {
2763        if (mImportMode) {
2764            return false;
2765        }
2766
2767        String param = uri.getQueryParameter(Data.MARK_AS_DIRTY);
2768        return param == null || (!param.equalsIgnoreCase("false") && !param.equals("0"));
2769    }
2770
2771    /**
2772     * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
2773     */
2774    private void setRawContactDirty(long rawContactId) {
2775        mRawContactDirtyUpdate.bindLong(1, rawContactId);
2776        mRawContactDirtyUpdate.execute();
2777    }
2778
2779    /**
2780     * Checks the {@link Groups#MARK_AS_DIRTY} query parameter.
2781     *
2782     * Returns true if the parameter is missing or is either "true" or "1".
2783     */
2784    private boolean shouldMarkGroupAsDirty(Uri uri) {
2785        if (mImportMode) {
2786            return false;
2787        }
2788
2789        String param = uri.getQueryParameter(Groups.MARK_AS_DIRTY);
2790        return param == null || (!param.equalsIgnoreCase("false") && !param.equals("0"));
2791    }
2792
2793    /*
2794     * Sets the given dataId record in the "data" table to primary, and resets all data records of
2795     * the same mimetype and under the same contact to not be primary.
2796     *
2797     * @param dataId the id of the data record to be set to primary.
2798     */
2799    private void setIsPrimary(long dataId) {
2800        mSetPrimaryStatement.bindLong(1, dataId);
2801        mSetPrimaryStatement.bindLong(2, dataId);
2802        mSetPrimaryStatement.bindLong(3, dataId);
2803        mSetPrimaryStatement.execute();
2804    }
2805
2806    /*
2807     * Sets the given dataId record in the "data" table to "super primary", and resets all data
2808     * records of the same mimetype and under the same aggregate to not be "super primary".
2809     *
2810     * @param dataId the id of the data record to be set to primary.
2811     */
2812    private void setIsSuperPrimary(long dataId) {
2813        mSetSuperPrimaryStatement.bindLong(1, dataId);
2814        mSetSuperPrimaryStatement.bindLong(2, dataId);
2815        mSetSuperPrimaryStatement.bindLong(3, dataId);
2816        mSetSuperPrimaryStatement.execute();
2817    }
2818
2819    public String getRawContactsByFilterAsNestedQuery(String filterParam) {
2820        StringBuilder sb = new StringBuilder();
2821        appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
2822        return sb.toString();
2823    }
2824
2825    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
2826            String limit) {
2827        sb.append("(SELECT DISTINCT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '");
2828        sb.append(NameNormalizer.normalize(filterParam));
2829        sb.append("*'");
2830        if (limit != null) {
2831            sb.append(" LIMIT ").append(limit);
2832        }
2833        sb.append(")");
2834    }
2835
2836    /**
2837     * Inserts an argument at the beginning of the selection arg list.
2838     */
2839    private String[] insertSelectionArg(String[] selectionArgs, String arg) {
2840        if (selectionArgs == null) {
2841            return new String[] {arg};
2842        } else {
2843            int newLength = selectionArgs.length + 1;
2844            String[] newSelectionArgs = new String[newLength];
2845            newSelectionArgs[0] = arg;
2846            System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
2847            return newSelectionArgs;
2848        }
2849    }
2850
2851    protected Account getDefaultAccount() {
2852        AccountManager accountManager = AccountManager.get(getContext());
2853        try {
2854            Account[] accounts = accountManager.blockingGetAccountsWithTypeAndFeatures(
2855                    DEFAULT_ACCOUNT_TYPE, new String[] {FEATURE_APPS_FOR_DOMAIN});
2856            if (accounts != null && accounts.length > 0) {
2857                return accounts[0];
2858            }
2859        } catch (Throwable e) {
2860            throw new RuntimeException("Cannot determine the default account "
2861                    + "for contacts compatibility", e);
2862        }
2863        throw new RuntimeException("Cannot determine the default account "
2864                + "for contacts compatibility");
2865    }
2866}
2867