ContactsProvider2.java revision e80e514a6175ad2ee03ea6eff6201e0e47d5a710
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.providers.contacts.OpenHelper.AggregatesColumns;
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.PhoneColumns;
27import com.android.providers.contacts.OpenHelper.PhoneLookupColumns;
28import com.android.providers.contacts.OpenHelper.Tables;
29import com.android.internal.content.SyncStateContentProviderHelper;
30import com.google.android.collect.Lists;
31
32import android.accounts.Account;
33import android.content.pm.PackageManager;
34import android.content.ContentProvider;
35import android.content.UriMatcher;
36import android.content.Context;
37import android.content.ContentValues;
38import android.content.ContentUris;
39import android.content.EntityIterator;
40import android.content.Entity;
41import android.content.ContentProviderResult;
42import android.content.OperationApplicationException;
43import android.content.ContentProviderOperation;
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.Binder;
52import android.os.RemoteException;
53import android.provider.BaseColumns;
54import android.provider.ContactsContract;
55import android.provider.Contacts.ContactMethods;
56import android.provider.ContactsContract.Aggregates;
57import android.provider.ContactsContract.AggregationExceptions;
58import android.provider.ContactsContract.CommonDataKinds;
59import android.provider.ContactsContract.Contacts;
60import android.provider.ContactsContract.Data;
61import android.provider.ContactsContract.Groups;
62import android.provider.ContactsContract.Presence;
63import android.provider.ContactsContract.RestrictionExceptions;
64import android.provider.ContactsContract.Aggregates.AggregationSuggestions;
65import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
66import android.provider.ContactsContract.CommonDataKinds.Email;
67import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
68import android.provider.ContactsContract.CommonDataKinds.Im;
69import android.provider.ContactsContract.CommonDataKinds.Nickname;
70import android.provider.ContactsContract.CommonDataKinds.Organization;
71import android.provider.ContactsContract.CommonDataKinds.Phone;
72import android.provider.ContactsContract.CommonDataKinds.Postal;
73import android.provider.ContactsContract.CommonDataKinds.StructuredName;
74import android.telephony.PhoneNumberUtils;
75import android.text.TextUtils;
76import android.util.Log;
77
78import java.util.ArrayList;
79import java.util.HashMap;
80
81/**
82 * Contacts content provider. The contract between this provider and applications
83 * is defined in {@link ContactsContract}.
84 */
85public class ContactsProvider2 extends ContentProvider {
86    // TODO: clean up debug tag and rename this class
87    private static final String TAG = "ContactsProvider ~~~~";
88
89    // TODO: define broadcastreceiver to catch app uninstalls that should clear exceptions
90    // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
91    // TODO: check for restricted flag during insert(), update(), and delete() calls
92
93    /** Default for the maximum number of returned aggregation suggestions. */
94    private static final int DEFAULT_MAX_SUGGESTIONS = 5;
95
96    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
97
98    private static final String STREQUENT_ORDER_BY = Aggregates.STARRED + " DESC, "
99            + Aggregates.TIMES_CONTACTED + " DESC, "
100            + Aggregates.DISPLAY_NAME + " ASC";
101    private static final String STREQUENT_LIMIT =
102            "(SELECT COUNT(1) FROM " + Tables.AGGREGATES + " WHERE "
103            + Aggregates.STARRED + "=1) + 25";
104
105    private static final int AGGREGATES = 1000;
106    private static final int AGGREGATES_ID = 1001;
107    private static final int AGGREGATES_DATA = 1002;
108    private static final int AGGREGATES_SUMMARY = 1003;
109    private static final int AGGREGATES_SUMMARY_ID = 1004;
110    private static final int AGGREGATES_SUMMARY_FILTER = 1005;
111    private static final int AGGREGATES_SUMMARY_STREQUENT = 1006;
112    private static final int AGGREGATES_SUMMARY_STREQUENT_FILTER = 1007;
113    private static final int AGGREGATES_SUMMARY_GROUP = 1008;
114
115    private static final int CONTACTS = 2002;
116    private static final int CONTACTS_ID = 2003;
117    private static final int CONTACTS_DATA = 2004;
118    private static final int CONTACTS_FILTER_EMAIL = 2005;
119
120    private static final int DATA = 3000;
121    private static final int DATA_ID = 3001;
122    private static final int PHONES = 3002;
123    private static final int PHONES_FILTER = 3003;
124    private static final int POSTALS = 3004;
125
126    private static final int PHONE_LOOKUP = 4000;
127
128    private static final int AGGREGATION_EXCEPTIONS = 6000;
129    private static final int AGGREGATION_EXCEPTION_ID = 6001;
130
131    private static final int PRESENCE = 7000;
132    private static final int PRESENCE_ID = 7001;
133
134    private static final int AGGREGATION_SUGGESTIONS = 8000;
135
136    private static final int RESTRICTION_EXCEPTIONS = 9000;
137
138    private static final int GROUPS = 10000;
139    private static final int GROUPS_ID = 10001;
140    private static final int GROUPS_SUMMARY = 10003;
141
142    private static final int SYNCSTATE = 11000;
143
144    private interface Projections {
145        public static final String[] CONTACTS_ACCOUNT_AND_PACKAGE =
146                new String[]{Contacts.ACCOUNT_NAME, Contacts.ACCOUNT_TYPE,
147                        ContactsColumns.PACKAGE_ID};
148        public static final int CONTACTS_ACCOUNT_AND_PACKAGE_COL_ACCOUNT_NAME = 0;
149        public static final int CONTACTS_ACCOUNT_AND_PACKAGE_COL_ACCOUNT_TYPE = 1;
150        public static final int CONTACTS_ACCOUNT_AND_PACKAGE_COL_PACKAGE_ID = 2;
151
152        public static final String[] PROJ_CONTACTS = new String[] {
153            ContactsColumns.CONCRETE_ID,
154        };
155
156        public static final String[] PROJ_DATA_CONTACTS = new String[] {
157            ContactsColumns.CONCRETE_ID,
158            DataColumns.CONCRETE_ID,
159            Contacts.AGGREGATE_ID,
160            ContactsColumns.PACKAGE_ID,
161            Contacts.IS_RESTRICTED,
162            Data.MIMETYPE,
163        };
164
165        public static final int COL_CONTACT_ID = 0;
166        public static final int COL_DATA_ID = 1;
167        public static final int COL_AGGREGATE_ID = 2;
168        public static final int COL_PACKAGE_ID = 3;
169        public static final int COL_IS_RESTRICTED = 4;
170        public static final int COL_MIMETYPE = 5;
171
172        public static final String[] PROJ_DATA_AGGREGATES = new String[] {
173            ContactsColumns.CONCRETE_ID,
174            DataColumns.CONCRETE_ID,
175            AggregatesColumns.CONCRETE_ID,
176            MimetypesColumns.CONCRETE_ID,
177            Phone.NUMBER,
178            Email.DATA,
179            AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID,
180            AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID,
181            AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID,
182            AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID,
183        };
184
185        public static final int COL_MIMETYPE_ID = 3;
186        public static final int COL_PHONE_NUMBER = 4;
187        public static final int COL_EMAIL_DATA = 5;
188        public static final int COL_OPTIMAL_PHONE_ID = 6;
189        public static final int COL_FALLBACK_PHONE_ID = 7;
190        public static final int COL_OPTIMAL_EMAIL_ID = 8;
191        public static final int COL_FALLBACK_EMAIL_ID = 9;
192
193    }
194
195    private interface DisplayNameQuery {
196        public static final String TABLES = Tables.DATA_JOIN_MIMETYPES;
197
198        public static final String[] COLUMNS = new String[] {
199            MimetypesColumns.MIMETYPE,
200            Data.IS_PRIMARY,
201            Data.DATA2,
202            StructuredName.DISPLAY_NAME,
203        };
204
205        public static final int MIMETYPE = 0;
206        public static final int IS_PRIMARY = 1;
207        public static final int DATA2 = 2;
208        public static final int DISPLAY_NAME = 3;
209    }
210
211    private interface DataQuery {
212        public static final String TABLES = Tables.DATA_JOIN_MIMETYPES;
213
214        public static final String[] COLUMNS = new String[] {
215            DataColumns.CONCRETE_ID,
216            MimetypesColumns.MIMETYPE,
217            Data.CONTACT_ID,
218            Data.IS_PRIMARY,
219            Data.DATA1,
220            Data.DATA2,
221            Data.DATA3,
222            Data.DATA4,
223            Data.DATA5,
224            Data.DATA6,
225            Data.DATA7,
226            Data.DATA8,
227            Data.DATA9,
228            Data.DATA10,
229        };
230
231        public static final int ID = 0;
232        public static final int MIMETYPE = 1;
233        public static final int CONTACT_ID = 2;
234        public static final int IS_PRIMARY = 3;
235        public static final int DATA1 = 4;
236        public static final int DATA2 = 5;
237        public static final int DATA3 = 6;
238        public static final int DATA4 = 7;
239        public static final int DATA5 = 8;
240        public static final int DATA6 = 9;
241        public static final int DATA7 = 10;
242        public static final int DATA8 = 11;
243        public static final int DATA9 = 12;
244        public static final int DATA10 = 13;
245    }
246
247    // Higher number represents higher priority in choosing what data to use for the display name
248    private static final int DISPLAY_NAME_PRIORITY_EMAIL = 1;
249    private static final int DISPLAY_NAME_PRIORITY_PHONE = 2;
250    private static final int DISPLAY_NAME_PRIORITY_ORGANIZATION = 3;
251    private static final int DISPLAY_NAME_PRIORITY_STRUCTURED_NAME = 4;
252
253    private static final HashMap<String, Integer> sDisplayNamePriorities;
254    static {
255        sDisplayNamePriorities = new HashMap<String, Integer>();
256        sDisplayNamePriorities.put(StructuredName.CONTENT_ITEM_TYPE,
257                DISPLAY_NAME_PRIORITY_STRUCTURED_NAME);
258        sDisplayNamePriorities.put(Organization.CONTENT_ITEM_TYPE,
259                DISPLAY_NAME_PRIORITY_ORGANIZATION);
260        sDisplayNamePriorities.put(Phone.CONTENT_ITEM_TYPE,
261                DISPLAY_NAME_PRIORITY_PHONE);
262        sDisplayNamePriorities.put(Email.CONTENT_ITEM_TYPE,
263                DISPLAY_NAME_PRIORITY_EMAIL);
264    }
265
266    /** Contains just the contacts columns */
267    private static final HashMap<String, String> sAggregatesProjectionMap;
268    /** Contains the aggregate columns along with primary phone */
269    private static final HashMap<String, String> sAggregatesSummaryProjectionMap;
270    /** Contains the data, contacts, and aggregate columns, for joined tables. */
271    private static final HashMap<String, String> sDataContactsAggregateProjectionMap;
272    /** Contains the data, contacts, group sourceid and aggregate columns, for joined tables. */
273    private static final HashMap<String, String> sDataContactsGroupsAggregateProjectionMap;
274    /** Contains just the contacts columns */
275    private static final HashMap<String, String> sContactsProjectionMap;
276    /** Contains just the data columns */
277    private static final HashMap<String, String> sDataGroupsProjectionMap;
278    /** Contains the data and contacts columns, for joined tables */
279    private static final HashMap<String, String> sDataContactsGroupsProjectionMap;
280    /** Contains the data and contacts columns, for joined tables */
281    private static final HashMap<String, String> sDataContactsProjectionMap;
282    /** Contains the just the {@link Groups} columns */
283    private static final HashMap<String, String> sGroupsProjectionMap;
284    /** Contains {@link Groups} columns along with summary details */
285    private static final HashMap<String, String> sGroupsSummaryProjectionMap;
286    /** Contains the just the agg_exceptions columns */
287    private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
288    /** Contains the just the {@link RestrictionExceptions} columns */
289    private static final HashMap<String, String> sRestrictionExceptionsProjectionMap;
290
291    /** Sql select statement that returns the contact id associated with a data record. */
292    private static final String sNestedContactIdSelect;
293    /** Sql select statement that returns the mimetype id associated with a data record. */
294    private static final String sNestedMimetypeSelect;
295    /** Sql select statement that returns the aggregate id associated with a contact record. */
296    private static final String sNestedAggregateIdSelect;
297    /** Sql select statement that returns a list of contact ids associated with an aggregate record. */
298    private static final String sNestedContactIdListSelect;
299    /** Sql where statement used to match all the data records that need to be updated when a new
300     * "primary" is selected.*/
301    private static final String sSetPrimaryWhere;
302    /** Sql where statement used to match all the data records that need to be updated when a new
303     * "super primary" is selected.*/
304    private static final String sSetSuperPrimaryWhere;
305    /** Sql where statement for filtering on groups. */
306    private static final String sAggregatesInGroupSelect;
307    /** Precompiled sql statement for setting a data record to the primary. */
308    private SQLiteStatement mSetPrimaryStatement;
309    /** Precompiled sql statement for setting a data record to the super primary. */
310    private SQLiteStatement mSetSuperPrimaryStatement;
311    /** Precompiled sql statement for incrementing times contacted for an aggregate */
312    private SQLiteStatement mLastTimeContactedUpdate;
313    /** Precompiled sql statement for updating a contact display name */
314    private SQLiteStatement mContactDisplayNameUpdate;
315
316    private static final String GTALK_PROTOCOL_STRING = ContactMethods
317            .encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
318
319    static {
320        // Contacts URI matching table
321        final UriMatcher matcher = sUriMatcher;
322        matcher.addURI(ContactsContract.AUTHORITY, "aggregates", AGGREGATES);
323        matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#", AGGREGATES_ID);
324        matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#/data", AGGREGATES_DATA);
325        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary", AGGREGATES_SUMMARY);
326        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/#", AGGREGATES_SUMMARY_ID);
327        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/filter/*",
328                AGGREGATES_SUMMARY_FILTER);
329        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/strequent/",
330                AGGREGATES_SUMMARY_STREQUENT);
331        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/strequent/filter/*",
332                AGGREGATES_SUMMARY_STREQUENT_FILTER);
333        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/group/*",
334                AGGREGATES_SUMMARY_GROUP);
335        matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#/suggestions",
336                AGGREGATION_SUGGESTIONS);
337        matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
338        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
339        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
340        matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter_email/*",
341                CONTACTS_FILTER_EMAIL);
342
343        matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
344        matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
345        matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
346        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
347        matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
348
349        matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
350        matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
351        matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
352
353        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
354
355        matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
356        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
357                AGGREGATION_EXCEPTIONS);
358        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
359                AGGREGATION_EXCEPTION_ID);
360
361        matcher.addURI(ContactsContract.AUTHORITY, "presence", PRESENCE);
362        matcher.addURI(ContactsContract.AUTHORITY, "presence/#", PRESENCE_ID);
363
364        matcher.addURI(ContactsContract.AUTHORITY, "restriction_exceptions", RESTRICTION_EXCEPTIONS);
365
366        HashMap<String, String> columns;
367
368        // Aggregates projection map
369        columns = new HashMap<String, String>();
370        columns.put(Aggregates._ID, "aggregates._id AS _id");
371        columns.put(Aggregates.DISPLAY_NAME, Aggregates.DISPLAY_NAME);
372        columns.put(Aggregates.LAST_TIME_CONTACTED, Aggregates.LAST_TIME_CONTACTED);
373        columns.put(Aggregates.TIMES_CONTACTED, Aggregates.TIMES_CONTACTED);
374        columns.put(Aggregates.STARRED, Aggregates.STARRED);
375        columns.put(Aggregates.IN_VISIBLE_GROUP, Aggregates.IN_VISIBLE_GROUP);
376        columns.put(Aggregates.PHOTO_ID, Aggregates.PHOTO_ID);
377        columns.put(Aggregates.PRIMARY_PHONE_ID, Aggregates.PRIMARY_PHONE_ID);
378        columns.put(Aggregates.PRIMARY_EMAIL_ID, Aggregates.PRIMARY_EMAIL_ID);
379        columns.put(Aggregates.CUSTOM_RINGTONE, Aggregates.CUSTOM_RINGTONE);
380        columns.put(Aggregates.SEND_TO_VOICEMAIL, Aggregates.SEND_TO_VOICEMAIL);
381        columns.put(AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID,
382                AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID);
383        columns.put(AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID,
384                AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID);
385        sAggregatesProjectionMap = columns;
386
387        // Aggregates primaries projection map. The overall presence status is
388        // the most-present value, as indicated by the largest value.
389        columns = new HashMap<String, String>();
390        columns.putAll(sAggregatesProjectionMap);
391        columns.put(CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE);
392        columns.put(CommonDataKinds.Phone.LABEL, CommonDataKinds.Phone.LABEL);
393        columns.put(CommonDataKinds.Phone.NUMBER, CommonDataKinds.Phone.NUMBER);
394        columns.put(Presence.PRESENCE_STATUS, "MAX(" + Presence.PRESENCE_STATUS + ")");
395        sAggregatesSummaryProjectionMap = columns;
396
397        // Contacts projection map
398        columns = new HashMap<String, String>();
399        columns.put(Contacts._ID, "contacts._id AS _id");
400        columns.put(Contacts.PACKAGE, Contacts.PACKAGE);
401        columns.put(Contacts.AGGREGATE_ID, Contacts.AGGREGATE_ID);
402        columns.put(Contacts.ACCOUNT_NAME,
403                OpenHelper.ContactsColumns.CONCRETE_ACCOUNT_NAME + " as " + Contacts.ACCOUNT_NAME);
404        columns.put(Contacts.ACCOUNT_TYPE,
405                OpenHelper.ContactsColumns.CONCRETE_ACCOUNT_TYPE + " as " + Contacts.ACCOUNT_TYPE);
406        columns.put(Contacts.SOURCE_ID,
407                OpenHelper.ContactsColumns.CONCRETE_SOURCE_ID + " as " + Contacts.SOURCE_ID);
408        columns.put(Contacts.VERSION,
409                OpenHelper.ContactsColumns.CONCRETE_VERSION + " as " + Contacts.VERSION);
410        columns.put(Contacts.DIRTY,
411                OpenHelper.ContactsColumns.CONCRETE_DIRTY + " as " + Contacts.DIRTY);
412        sContactsProjectionMap = columns;
413
414        // Data projection map
415        columns = new HashMap<String, String>();
416        columns.put(Data._ID, "data._id AS _id");
417        columns.put(Data.CONTACT_ID, Data.CONTACT_ID);
418        columns.put(Data.MIMETYPE, Data.MIMETYPE);
419        columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
420        columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
421        columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
422        columns.put(Data.DATA1, "data.data1 as data1");
423        columns.put(Data.DATA2, "data.data2 as data2");
424        columns.put(Data.DATA3, "data.data3 as data3");
425        columns.put(Data.DATA4, "data.data4 as data4");
426        columns.put(Data.DATA5, "data.data5 as data5");
427        columns.put(Data.DATA6, "data.data6 as data6");
428        columns.put(Data.DATA7, "data.data7 as data7");
429        columns.put(Data.DATA8, "data.data8 as data8");
430        columns.put(Data.DATA9, "data.data9 as data9");
431        columns.put(Data.DATA10, "data.data10 as data10");
432        columns.put(GroupMembership.GROUP_SOURCE_ID, "groups.sourceid as " + GroupMembership.GROUP_SOURCE_ID);
433        // Mappings used for backwards compatibility.
434        columns.put("number", Phone.NUMBER);
435        sDataGroupsProjectionMap = columns;
436
437        // Data, groups and contacts projection map for joins. _id comes from the data table
438        columns = new HashMap<String, String>();
439        columns.putAll(sContactsProjectionMap);
440        columns.putAll(sDataGroupsProjectionMap); // _id will be replaced with the one from data
441        columns.put(Data.CONTACT_ID, DataColumns.CONCRETE_CONTACT_ID);
442        sDataContactsGroupsProjectionMap = columns;
443
444        // Data and contacts projection map for joins. _id comes from the data table
445        columns = new HashMap<String, String>();
446        columns.putAll(sDataContactsGroupsProjectionMap);
447        columns.remove(GroupMembership.GROUP_SOURCE_ID);
448        sDataContactsProjectionMap = columns;
449
450        // Data and contacts projection map for joins. _id comes from the data table
451        columns = new HashMap<String, String>();
452        columns.putAll(sAggregatesProjectionMap);
453        columns.putAll(sContactsProjectionMap); //
454        columns.putAll(sDataGroupsProjectionMap); // _id will be replaced with the one from data
455        columns.put(Data.CONTACT_ID, DataColumns.CONCRETE_CONTACT_ID);
456        sDataContactsGroupsAggregateProjectionMap = columns;
457
458        // Data and contacts projection map for joins. _id comes from the data table
459        columns = new HashMap<String, String>();
460        columns.putAll(sDataContactsGroupsAggregateProjectionMap);
461        columns.remove(GroupMembership.GROUP_SOURCE_ID);
462        sDataContactsAggregateProjectionMap = columns;
463
464        // Groups projection map
465        columns = new HashMap<String, String>();
466        columns.put(Groups._ID, "groups._id AS _id");
467        columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
468        columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
469        columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
470        columns.put(Groups.DIRTY, Groups.DIRTY);
471        columns.put(Groups.VERSION, Groups.VERSION);
472        columns.put(Groups.PACKAGE, Groups.PACKAGE);
473        columns.put(Groups.PACKAGE_ID, GroupsColumns.CONCRETE_PACKAGE_ID);
474        columns.put(Groups.TITLE, Groups.TITLE);
475        columns.put(Groups.TITLE_RESOURCE, Groups.TITLE_RESOURCE);
476        columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
477        sGroupsProjectionMap = columns;
478
479        // Contacts and groups projection map
480        columns = new HashMap<String, String>();
481        columns.putAll(sGroupsProjectionMap);
482
483        columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + AggregatesColumns.CONCRETE_ID
484                + ") FROM " + Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES + " WHERE "
485                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
486                + ") AS " + Groups.SUMMARY_COUNT);
487
488        columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
489                + AggregatesColumns.CONCRETE_ID + ") FROM "
490                + Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES + " WHERE "
491                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
492                + " AND " + Clauses.HAS_PRIMARY_PHONE + ") AS " + Groups.SUMMARY_WITH_PHONES);
493
494        sGroupsSummaryProjectionMap = columns;
495
496        // Aggregate exception projection map
497        columns = new HashMap<String, String>();
498        columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
499        columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
500        columns.put(AggregationExceptions.AGGREGATE_ID,
501                "contacts1." + Contacts.AGGREGATE_ID + " AS " + AggregationExceptions.AGGREGATE_ID);
502        columns.put(AggregationExceptions.CONTACT_ID, AggregationExceptionColumns.CONTACT_ID2);
503        sAggregationExceptionsProjectionMap = columns;
504
505        // Restriction exception projection map
506        columns = new HashMap<String, String>();
507        columns.put(RestrictionExceptions.PACKAGE_PROVIDER, RestrictionExceptions.PACKAGE_PROVIDER);
508        columns.put(RestrictionExceptions.PACKAGE_CLIENT, RestrictionExceptions.PACKAGE_CLIENT);
509        columns.put(RestrictionExceptions.ALLOW_ACCESS, "1"); // Access granted if row returned
510        sRestrictionExceptionsProjectionMap = columns;
511
512        sNestedContactIdSelect = "SELECT " + Data.CONTACT_ID + " FROM " + Tables.DATA + " WHERE "
513                + Data._ID + "=?";
514        sNestedMimetypeSelect = "SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA
515                + " WHERE " + Data._ID + "=?";
516        sNestedAggregateIdSelect = "SELECT " + Contacts.AGGREGATE_ID + " FROM " + Tables.CONTACTS
517                + " WHERE " + Contacts._ID + "=(" + sNestedContactIdSelect + ")";
518        sNestedContactIdListSelect = "SELECT " + Contacts._ID + " FROM " + Tables.CONTACTS
519                + " WHERE " + Contacts.AGGREGATE_ID + "=(" + sNestedAggregateIdSelect + ")";
520        sSetPrimaryWhere = Data.CONTACT_ID + "=(" + sNestedContactIdSelect + ") AND "
521                + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
522        sSetSuperPrimaryWhere = Data.CONTACT_ID + " IN (" + sNestedContactIdListSelect + ") AND "
523                + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
524        sAggregatesInGroupSelect = AggregatesColumns.CONCRETE_ID + " IN (SELECT "
525                + Contacts.AGGREGATE_ID + " FROM " + Tables.CONTACTS + " WHERE ("
526                + ContactsColumns.CONCRETE_ID + " IN (SELECT " + Tables.DATA + "."
527                + Data.CONTACT_ID + " FROM " + Tables.DATA_JOIN_MIMETYPES + " WHERE ("
528                + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND "
529                + GroupMembership.GROUP_ROW_ID + "=(SELECT " + Tables.GROUPS + "."
530                + Groups._ID + " FROM " + Tables.GROUPS + " WHERE " + Groups.TITLE + "=?)))))";
531    }
532
533    /**
534     * Handles inserts and update for a specific Data type.
535     */
536    private abstract class DataRowHandler {
537
538        protected final String mMimetype;
539
540        public DataRowHandler(String mimetype) {
541            mMimetype = mimetype;
542        }
543
544        /**
545         * Inserts a row into the {@link Data} table.
546         */
547        public long insert(SQLiteDatabase db, long contactId, ContentValues values) {
548            final long dataId = db.insert(Tables.DATA, null, values);
549
550            Integer primary = values.getAsInteger(Data.IS_PRIMARY);
551            if (primary != null && primary != 0) {
552                setIsPrimary(dataId);
553            }
554
555            fixContactDisplayName(db, contactId);
556            return dataId;
557        }
558
559        /**
560         * Validates data and updates a {@link Data} row using the cursor, which contains
561         * the current data.
562         */
563        public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
564            throw new UnsupportedOperationException();
565        }
566
567        public int delete(SQLiteDatabase db, Cursor c) {
568            long dataId = c.getLong(DataQuery.ID);
569            long contactId = c.getLong(DataQuery.CONTACT_ID);
570            boolean primary = c.getInt(DataQuery.IS_PRIMARY) != 0;
571            int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
572            if (count != 0 && primary) {
573                fixPrimary(db, contactId);
574                fixContactDisplayName(db, contactId);
575            }
576            return count;
577        }
578
579        private void fixPrimary(SQLiteDatabase db, long contactId) {
580            long newPrimaryId = findNewPrimaryDataId(db, contactId);
581            if (newPrimaryId != -1) {
582                ContactsProvider2.this.setIsPrimary(newPrimaryId);
583            }
584        }
585
586        protected long findNewPrimaryDataId(SQLiteDatabase db, long contactId) {
587            long primaryId = -1;
588            int primaryType = -1;
589            Cursor c = queryData(db, contactId);
590            try {
591                while (c.moveToNext()) {
592                    long dataId = c.getLong(DataQuery.ID);
593                    int type = c.getInt(DataQuery.DATA2);
594                    if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
595                        primaryId = dataId;
596                        primaryType = type;
597                    }
598                }
599            } finally {
600                c.close();
601            }
602            return primaryId;
603        }
604
605        /**
606         * Returns the rank of a specific record type to be used in determining the primary
607         * row. Lower number represents higher priority.
608         */
609        protected int getTypeRank(int type) {
610            return 0;
611        }
612
613        protected Cursor queryData(SQLiteDatabase db, long contactId) {
614            // TODO Lookup integer mimetype IDs' instead of joining for speed
615            return db.query(DataQuery.TABLES, DataQuery.COLUMNS, Data.CONTACT_ID + "="
616                    + contactId + " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'",
617                    null, null, null, null);
618        }
619
620        protected void fixContactDisplayName(SQLiteDatabase db, long contactId) {
621            if (!sDisplayNamePriorities.containsKey(mMimetype)) {
622                return;
623            }
624
625            String bestDisplayName = null;
626            Cursor c = db.query(DisplayNameQuery.TABLES, DisplayNameQuery.COLUMNS,
627                    Data.CONTACT_ID + "=" + contactId, null, null, null, null);
628            try {
629                int maxPriority = -1;
630                while (c.moveToNext()) {
631                    String mimeType = c.getString(DisplayNameQuery.MIMETYPE);
632                    boolean primary;
633                    String name;
634
635                    if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
636                        name = c.getString(DisplayNameQuery.DISPLAY_NAME);
637                        primary = true;
638                    } else {
639                        name = c.getString(DisplayNameQuery.DATA2);
640                        primary = (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0);
641                    }
642
643                    if (primary && name != null) {
644                        Integer priority = sDisplayNamePriorities.get(mimeType);
645                        if (priority != null && priority > maxPriority) {
646                            maxPriority = priority;
647                            bestDisplayName = name;
648                        }
649                    }
650                }
651
652            } finally {
653                c.close();
654            }
655
656            ContactsProvider2.this.setDisplayName(contactId, bestDisplayName);
657        }
658    }
659
660    public class CustomDataRowHandler extends DataRowHandler {
661
662        public CustomDataRowHandler(String mimetype) {
663            super(mimetype);
664        }
665    }
666
667    public class StructuredNameRowHandler extends DataRowHandler {
668
669        private final NameSplitter mNameSplitter;
670
671        public StructuredNameRowHandler(NameSplitter nameSplitter) {
672            super(StructuredName.CONTENT_ITEM_TYPE);
673            mNameSplitter = nameSplitter;
674        }
675
676        @Override
677        public long insert(SQLiteDatabase db, long contactId, ContentValues values) {
678            fixStructuredNameComponents(values);
679            return super.insert(db, contactId, values);
680        }
681
682        @Override
683        public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
684            // TODO Parse the full name if it has changed and replace pre-existing piece parts.
685        }
686
687        /**
688         * Parses the supplied display name, but only if the incoming values do not already contain
689         * structured name parts.  Also, if the display name is not provided, generate one by
690         * concatenating first name and last name
691         *
692         * TODO see if the order of first and last names needs to be conditionally reversed for
693         * some locales, e.g. China.
694         */
695        private void fixStructuredNameComponents(ContentValues values) {
696            String fullName = values.getAsString(StructuredName.DISPLAY_NAME);
697            if (!TextUtils.isEmpty(fullName)
698                    && TextUtils.isEmpty(values.getAsString(StructuredName.PREFIX))
699                    && TextUtils.isEmpty(values.getAsString(StructuredName.GIVEN_NAME))
700                    && TextUtils.isEmpty(values.getAsString(StructuredName.MIDDLE_NAME))
701                    && TextUtils.isEmpty(values.getAsString(StructuredName.FAMILY_NAME))
702                    && TextUtils.isEmpty(values.getAsString(StructuredName.SUFFIX))) {
703                NameSplitter.Name name = new NameSplitter.Name();
704                mNameSplitter.split(name, fullName);
705
706                values.put(StructuredName.PREFIX, name.getPrefix());
707                values.put(StructuredName.GIVEN_NAME, name.getGivenNames());
708                values.put(StructuredName.MIDDLE_NAME, name.getMiddleName());
709                values.put(StructuredName.FAMILY_NAME, name.getFamilyName());
710                values.put(StructuredName.SUFFIX, name.getSuffix());
711            }
712
713            if (TextUtils.isEmpty(fullName)) {
714                String givenName = values.getAsString(StructuredName.GIVEN_NAME);
715                String familyName = values.getAsString(StructuredName.FAMILY_NAME);
716                if (TextUtils.isEmpty(givenName)) {
717                    fullName = familyName;
718                } else if (TextUtils.isEmpty(familyName)) {
719                    fullName = givenName;
720                } else {
721                    fullName = givenName + " " + familyName;
722                }
723
724                if (!TextUtils.isEmpty(fullName)) {
725                    values.put(StructuredName.DISPLAY_NAME, fullName);
726                }
727            }
728        }
729    }
730
731    public class CommonDataRowHandler extends DataRowHandler {
732
733        private final String mTypeColumn;
734        private final String mLabelColumn;
735
736        public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) {
737            super(mimetype);
738            mTypeColumn = typeColumn;
739            mLabelColumn = labelColumn;
740        }
741
742        @Override
743        public long insert(SQLiteDatabase db, long contactId, ContentValues values) {
744            int type;
745            String label;
746            if (values.containsKey(mTypeColumn)) {
747                type = values.getAsInteger(mTypeColumn);
748            } else {
749                type = BaseTypes.TYPE_CUSTOM;
750            }
751            if (values.containsKey(mLabelColumn)) {
752                label = values.getAsString(mLabelColumn);
753            } else {
754                label = null;
755            }
756
757            if (type != BaseTypes.TYPE_CUSTOM && label != null) {
758                throw new RuntimeException(mLabelColumn + " value can only be specified with "
759                        + mTypeColumn + "=" + BaseTypes.TYPE_CUSTOM + "(custom)");
760            }
761
762            if (type == BaseTypes.TYPE_CUSTOM && label == null) {
763                throw new RuntimeException(mLabelColumn + " value must be specified when "
764                        + mTypeColumn + "=" + BaseTypes.TYPE_CUSTOM + "(custom)");
765            }
766
767            return super.insert(db, contactId, values);
768        }
769
770        @Override
771        public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
772            // TODO read the data and check the constraint
773        }
774    }
775
776    public class OrganizationDataRowHandler extends CommonDataRowHandler {
777
778        public OrganizationDataRowHandler() {
779            super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
780        }
781
782        @Override
783        public long insert(SQLiteDatabase db, long contactId, ContentValues values) {
784            long id = super.insert(db, contactId, values);
785            fixContactDisplayName(db, contactId);
786            return id;
787        }
788
789        @Override
790        protected int getTypeRank(int type) {
791            switch (type) {
792                case Organization.TYPE_WORK: return 0;
793                case Organization.TYPE_CUSTOM: return 1;
794                case Organization.TYPE_OTHER: return 2;
795                default: return 1000;
796            }
797        }
798    }
799
800    public class EmailDataRowHandler extends CommonDataRowHandler {
801
802        public EmailDataRowHandler() {
803            super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
804        }
805
806        @Override
807        public long insert(SQLiteDatabase db, long contactId, ContentValues values) {
808            long id = super.insert(db, contactId, values);
809            fixContactDisplayName(db, contactId);
810            return id;
811        }
812
813        @Override
814        protected int getTypeRank(int type) {
815            switch (type) {
816                case Email.TYPE_HOME: return 0;
817                case Email.TYPE_WORK: return 1;
818                case Email.TYPE_CUSTOM: return 2;
819                case Email.TYPE_OTHER: return 3;
820                default: return 1000;
821            }
822        }
823    }
824
825    public class PhoneDataRowHandler extends CommonDataRowHandler {
826
827        public PhoneDataRowHandler() {
828            super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
829        }
830
831        @Override
832        public long insert(SQLiteDatabase db, long contactId, ContentValues values) {
833            ContentValues phoneValues = new ContentValues();
834            String number = values.getAsString(Phone.NUMBER);
835            String normalizedNumber = null;
836            if (number != null) {
837                normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
838                values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
839            }
840
841            long id = super.insert(db, contactId, values);
842
843            if (number != null) {
844                phoneValues.put(PhoneLookupColumns.CONTACT_ID, contactId);
845                phoneValues.put(PhoneLookupColumns.DATA_ID, id);
846                phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
847                db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
848            }
849
850            return id;
851        }
852
853        @Override
854        protected int getTypeRank(int type) {
855            switch (type) {
856                case Phone.TYPE_MOBILE: return 0;
857                case Phone.TYPE_WORK: return 1;
858                case Phone.TYPE_HOME: return 2;
859                case Phone.TYPE_PAGER: return 3;
860                case Phone.TYPE_CUSTOM: return 4;
861                case Phone.TYPE_OTHER: return 5;
862                case Phone.TYPE_FAX_WORK: return 6;
863                case Phone.TYPE_FAX_HOME: return 7;
864                default: return 1000;
865            }
866        }
867    }
868
869    private HashMap<String, DataRowHandler> mDataRowHandlers;
870    private final ContactAggregationScheduler mAggregationScheduler;
871    private OpenHelper mOpenHelper;
872
873    private ContactAggregator mContactAggregator;
874    private NameSplitter mNameSplitter;
875    private LegacyApiSupport mLegacyApiSupport;
876
877    public ContactsProvider2() {
878        this(new ContactAggregationScheduler());
879    }
880
881    /**
882     * Constructor for testing.
883     */
884    /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
885        mAggregationScheduler = scheduler;
886    }
887
888    @Override
889    public boolean onCreate() {
890        final Context context = getContext();
891
892        mOpenHelper = getOpenHelper(context);
893        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
894
895        mContactAggregator = new ContactAggregator(context, mOpenHelper, mAggregationScheduler);
896
897        mSetPrimaryStatement = db.compileStatement(
898                "UPDATE " + Tables.DATA + " SET " + Data.IS_PRIMARY
899                + "=(_id=?) WHERE " + sSetPrimaryWhere);
900        mSetSuperPrimaryStatement = db.compileStatement(
901                "UPDATE " + Tables.DATA + " SET " + Data.IS_SUPER_PRIMARY
902                + "=(_id=?) WHERE " + sSetSuperPrimaryWhere);
903        mLastTimeContactedUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
904                + Contacts.TIMES_CONTACTED + "=" + Contacts.TIMES_CONTACTED + "+1,"
905                + Contacts.LAST_TIME_CONTACTED + "=? WHERE " + Contacts.AGGREGATE_ID + "=?");
906
907        mContactDisplayNameUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
908                + ContactsColumns.DISPLAY_NAME + "=? WHERE " + Contacts._ID + "=?");
909
910        mNameSplitter = new NameSplitter(
911                context.getString(com.android.internal.R.string.common_name_prefixes),
912                context.getString(com.android.internal.R.string.common_last_name_prefixes),
913                context.getString(com.android.internal.R.string.common_name_suffixes),
914                context.getString(com.android.internal.R.string.common_name_conjunctions));
915
916        mLegacyApiSupport = new LegacyApiSupport(context, mOpenHelper, this);
917
918        mDataRowHandlers = new HashMap<String, DataRowHandler>();
919
920        mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
921        mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
922                new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
923        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE,
924                new CommonDataRowHandler(Postal.CONTENT_ITEM_TYPE, Postal.TYPE, Postal.LABEL));
925        mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
926        mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
927        mDataRowHandlers.put(Postal.CONTENT_ITEM_TYPE,
928                new CommonDataRowHandler(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL));
929        mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
930                new StructuredNameRowHandler(mNameSplitter));
931
932        return (db != null);
933    }
934
935    /* Visible for testing */
936    protected OpenHelper getOpenHelper(final Context context) {
937        return OpenHelper.getInstance(context);
938    }
939
940    @Override
941    protected void finalize() throws Throwable {
942        if (mContactAggregator != null) {
943            mContactAggregator.quit();
944        }
945
946        super.finalize();
947    }
948
949    /**
950     * Wipes all data from the contacts database.
951     */
952    /* package */ void wipeData() {
953        mOpenHelper.wipeData();
954    }
955
956    /**
957     * Called when a change has been made.
958     *
959     * @param uri the uri that the change was made to
960     */
961    private void onChange(Uri uri) {
962        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
963    }
964
965    @Override
966    public boolean isTemporary() {
967        return false;
968    }
969
970    private DataRowHandler getDataRowHandler(final String mimeType) {
971        DataRowHandler handler = mDataRowHandlers.get(mimeType);
972        if (handler == null) {
973            handler = new CustomDataRowHandler(mimeType);
974            mDataRowHandlers.put(mimeType, handler);
975        }
976        return handler;
977    }
978
979    @Override
980    public Uri insert(Uri uri, ContentValues values) {
981        final int match = sUriMatcher.match(uri);
982        long id = 0;
983
984        switch (match) {
985            case SYNCSTATE:
986                id = mOpenHelper.getSyncState().insert(mOpenHelper.getWritableDatabase(), values);
987                break;
988
989            case AGGREGATES: {
990                insertAggregate(values);
991                break;
992            }
993
994            case CONTACTS: {
995                final Account account = readAccountFromQueryParams(uri);
996                id = insertContact(values, account);
997                break;
998            }
999
1000            case CONTACTS_DATA: {
1001                values.put(Data.CONTACT_ID, uri.getPathSegments().get(1));
1002                id = insertData(values);
1003                break;
1004            }
1005
1006            case DATA: {
1007                id = insertData(values);
1008                break;
1009            }
1010
1011            case GROUPS: {
1012                final Account account = readAccountFromQueryParams(uri);
1013                id = insertGroup(values, account);
1014                break;
1015            }
1016
1017            case PRESENCE: {
1018                id = insertPresence(values);
1019                break;
1020            }
1021
1022            default:
1023                return mLegacyApiSupport.insert(uri, values);
1024        }
1025
1026        if (id < 0) {
1027            return null;
1028        }
1029
1030        final Uri result = ContentUris.withAppendedId(uri, id);
1031        onChange(result);
1032        return result;
1033    }
1034
1035    /**
1036     * If account is non-null then store it in the values. If the account is already
1037     * specified in the values then it must be consistent with the account, if it is non-null.
1038     * @param values the ContentValues to read from and update
1039     * @param account the explicitly provided Account
1040     * @return false if the accounts are inconsistent
1041     */
1042    private boolean resolveAccount(ContentValues values, Account account) {
1043        // If either is specified then both must be specified.
1044        final String accountName = values.getAsString(Contacts.ACCOUNT_NAME);
1045        final String accountType = values.getAsString(Contacts.ACCOUNT_TYPE);
1046        if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
1047            final Account valuesAccount = new Account(accountName, accountType);
1048            if (account != null && !valuesAccount.equals(account)) {
1049                return false;
1050            }
1051            account = valuesAccount;
1052        }
1053        if (account != null) {
1054            values.put(Contacts.ACCOUNT_NAME, account.mName);
1055            values.put(Contacts.ACCOUNT_TYPE, account.mType);
1056        }
1057        return true;
1058    }
1059
1060    /**
1061     * Inserts an item in the aggregates table
1062     *
1063     * @param values the values for the new row
1064     * @return the row ID of the newly created row
1065     */
1066    private long insertAggregate(ContentValues values) {
1067        throw new UnsupportedOperationException("Aggregates are created automatically");
1068    }
1069
1070    /**
1071     * Inserts an item in the contacts table
1072     *
1073     * @param values the values for the new row
1074     * @param account the account this contact should be associated with. may be null.
1075     * @return the row ID of the newly created row
1076     */
1077    private long insertContact(ContentValues values, Account account) {
1078        /*
1079         * The contact record is inserted in the contacts table, but it needs to
1080         * be processed by the aggregator before it will be returned by the
1081         * "aggregates" queries.
1082         */
1083        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1084
1085        ContentValues overriddenValues = new ContentValues(values);
1086        overriddenValues.putNull(Contacts.AGGREGATE_ID);
1087        if (!resolveAccount(overriddenValues, account)) {
1088            return -1;
1089        }
1090
1091        // Replace package with internal mapping
1092        final String packageName = overriddenValues.getAsString(Contacts.PACKAGE);
1093        overriddenValues.put(ContactsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1094        overriddenValues.remove(Contacts.PACKAGE);
1095
1096        long contactId = db.insert(Tables.CONTACTS, Contacts.AGGREGATE_ID, overriddenValues);
1097
1098        int aggregationMode = Contacts.AGGREGATION_MODE_DEFAULT;
1099        if (values.containsKey(Contacts.AGGREGATION_MODE)) {
1100            aggregationMode = values.getAsInteger(Contacts.AGGREGATION_MODE);
1101        }
1102
1103        triggerAggregation(contactId, aggregationMode);
1104
1105        return contactId;
1106    }
1107
1108    /**
1109     * Inserts an item in the data table
1110     *
1111     * @param values the values for the new row
1112     * @return the row ID of the newly created row
1113     */
1114    private long insertData(ContentValues values) {
1115        int aggregationMode = Contacts.AGGREGATION_MODE_DISABLED;
1116
1117        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1118        long id = 0;
1119        db.beginTransaction();
1120        try {
1121            long contactId = values.getAsLong(Data.CONTACT_ID);
1122
1123            // Replace mimetype with internal mapping
1124            final String mimeType = values.getAsString(Data.MIMETYPE);
1125            values.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
1126            values.remove(Data.MIMETYPE);
1127
1128            if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
1129                boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
1130                boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
1131                if (containsGroupSourceId && containsGroupId) {
1132                    throw new IllegalArgumentException(
1133                            "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
1134                                    + "and GroupMembership.GROUP_ROW_ID");
1135                }
1136
1137                if (!containsGroupSourceId && !containsGroupId) {
1138                    throw new IllegalArgumentException(
1139                            "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
1140                                    + "and GroupMembership.GROUP_ROW_ID");
1141                }
1142
1143                if (containsGroupSourceId) {
1144                    final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
1145                    final long groupId = getOrMakeGroup(db, contactId, sourceId);
1146                    values.remove(GroupMembership.GROUP_SOURCE_ID);
1147                    values.put(GroupMembership.GROUP_ROW_ID, groupId);
1148                }
1149            }
1150
1151            id = getDataRowHandler(mimeType).insert(db, contactId, values);
1152
1153            aggregationMode = mContactAggregator.markContactForAggregation(contactId);
1154
1155            db.setTransactionSuccessful();
1156        } finally {
1157            db.endTransaction();
1158        }
1159
1160        triggerAggregation(id, aggregationMode);
1161        return id;
1162    }
1163
1164    private void triggerAggregation(long contactId, int aggregationMode) {
1165        switch (aggregationMode) {
1166            case Contacts.AGGREGATION_MODE_DEFAULT:
1167                mContactAggregator.schedule();
1168                break;
1169
1170            case Contacts.AGGREGATION_MODE_IMMEDITATE:
1171                mContactAggregator.aggregateContact(contactId);
1172                break;
1173
1174            case Contacts.AGGREGATION_MODE_DISABLED:
1175                // Do nothing
1176                break;
1177        }
1178    }
1179
1180    public int deleteData(long dataId, String[] allowedMimeTypes) {
1181        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1182        Cursor c = db.query(DataQuery.TABLES, DataQuery.COLUMNS,
1183                DataColumns.CONCRETE_ID + "=" + dataId, null, null, null, null);
1184        // TODO apply restrictions
1185        try {
1186            if (!c.moveToFirst()) {
1187                return 0;
1188            }
1189
1190            String mimeType = c.getString(DataQuery.MIMETYPE);
1191            boolean valid = false;
1192            for (int i = 0; i < allowedMimeTypes.length; i++) {
1193                if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
1194                    valid = true;
1195                    break;
1196                }
1197            }
1198
1199            if (!valid) {
1200                throw new RuntimeException("Data type mismatch: expected "
1201                        + Lists.newArrayList(allowedMimeTypes));
1202            }
1203
1204            return getDataRowHandler(mimeType).delete(db, c);
1205        } finally {
1206            c.close();
1207        }
1208    }
1209
1210    /**
1211     * Returns the group id of the group with sourceId and the same account as contactId.
1212     * If the group doesn't already exist then it is first created,
1213     * @param db SQLiteDatabase to use for this operation
1214     * @param contactId the contact this group is associated with
1215     * @param sourceId the sourceIf of the group to query or create
1216     * @return the group id of the existing or created group
1217     * @throws IllegalArgumentException if the contact is not associated with an account
1218     * @throws IllegalStateException if a group needs to be created but the creation failed
1219     */
1220    private long getOrMakeGroup(SQLiteDatabase db, long contactId, String sourceId) {
1221        Account account = null;
1222        long contactPackage = -1;
1223        Cursor c = db.query(Tables.CONTACTS,
1224                Projections.CONTACTS_ACCOUNT_AND_PACKAGE,
1225                Contacts._ID + "=" + contactId, null, null, null, null);
1226        try {
1227            if (c.moveToNext()) {
1228                final String accountName =
1229                        c.getString(Projections.CONTACTS_ACCOUNT_AND_PACKAGE_COL_ACCOUNT_NAME);
1230                final String accountType =
1231                        c.getString(Projections.CONTACTS_ACCOUNT_AND_PACKAGE_COL_ACCOUNT_TYPE);
1232                if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
1233                    account = new Account(accountName, accountType);
1234                }
1235                contactPackage = c.getLong(Projections.CONTACTS_ACCOUNT_AND_PACKAGE_COL_PACKAGE_ID);
1236            }
1237        } finally {
1238            c.close();
1239        }
1240        if (account == null) {
1241            throw new IllegalArgumentException("if the groupmembership only "
1242                    + "has a sourceid the the contact must be associate with "
1243                    + "an account");
1244        }
1245
1246        // look up the group that contains this sourceId and has the same account name and type
1247        // as the contact refered to by contactId
1248        c = db.query(Tables.GROUPS, new String[]{Contacts._ID},
1249                Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
1250                new String[]{sourceId, account.mName, account.mType}, null, null, null);
1251        try {
1252            if (c.moveToNext()) {
1253                return c.getLong(0);
1254            } else {
1255                ContentValues groupValues = new ContentValues();
1256                groupValues.put(Groups.PACKAGE_ID, contactPackage);
1257                groupValues.put(Groups.ACCOUNT_NAME, account.mName);
1258                groupValues.put(Groups.ACCOUNT_TYPE, account.mType);
1259                groupValues.put(Groups.SOURCE_ID, sourceId);
1260                long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
1261                if (groupId < 0) {
1262                    throw new IllegalStateException("unable to create a new group with "
1263                            + "this sourceid: " + groupValues);
1264                }
1265                return groupId;
1266            }
1267        } finally {
1268            c.close();
1269        }
1270    }
1271
1272    /**
1273     * Delete the given {@link Data} row, fixing up any {@link Aggregates}
1274     * primaries that reference it.
1275     */
1276    private int deleteData(long dataId) {
1277        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1278
1279        final long mimePhone = mOpenHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
1280        final long mimeEmail = mOpenHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
1281
1282        // Check to see if the data about to be deleted was a super-primary on
1283        // the parent aggregate, and set flags to fix-up once deleted.
1284        long aggId = -1;
1285        long mimeId = -1;
1286        String dataRaw = null;
1287        boolean fixOptimal = false;
1288        boolean fixFallback = false;
1289
1290        Cursor cursor = null;
1291        try {
1292            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES,
1293                    Projections.PROJ_DATA_AGGREGATES, DataColumns.CONCRETE_ID + "=" + dataId, null,
1294                    null, null, null);
1295            if (cursor.moveToFirst()) {
1296                aggId = cursor.getLong(Projections.COL_AGGREGATE_ID);
1297                mimeId = cursor.getLong(Projections.COL_MIMETYPE_ID);
1298                if (mimeId == mimePhone) {
1299                    dataRaw = cursor.getString(Projections.COL_PHONE_NUMBER);
1300                    fixOptimal = (cursor.getLong(Projections.COL_OPTIMAL_PHONE_ID) == dataId);
1301                    fixFallback = (cursor.getLong(Projections.COL_FALLBACK_PHONE_ID) == dataId);
1302                } else if (mimeId == mimeEmail) {
1303                    dataRaw = cursor.getString(Projections.COL_EMAIL_DATA);
1304                    fixOptimal = (cursor.getLong(Projections.COL_OPTIMAL_EMAIL_ID) == dataId);
1305                    fixFallback = (cursor.getLong(Projections.COL_FALLBACK_EMAIL_ID) == dataId);
1306                }
1307            }
1308        } finally {
1309            if (cursor != null) {
1310                cursor.close();
1311                cursor = null;
1312            }
1313        }
1314
1315        // Delete the requested data item.
1316        int dataDeleted = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
1317
1318        // Fix-up any super-primary values that are now invalid.
1319        if (fixOptimal || fixFallback) {
1320            final ContentValues values = new ContentValues();
1321            final StringBuilder scoreClause = new StringBuilder();
1322
1323            final String SCORE = "score";
1324
1325            // Build scoring clause that will first pick data items under the
1326            // same aggregate that have identical values, otherwise fall back to
1327            // normal primary scoring from the member contacts.
1328            scoreClause.append("(CASE WHEN ");
1329            if (mimeId == mimePhone) {
1330                scoreClause.append(Phone.NUMBER);
1331            } else if (mimeId == mimeEmail) {
1332                scoreClause.append(Email.DATA);
1333            }
1334            scoreClause.append("=");
1335            DatabaseUtils.appendEscapedSQLString(scoreClause, dataRaw);
1336            scoreClause.append(" THEN 2 ELSE " + Data.IS_PRIMARY + " END) AS " + SCORE);
1337
1338            final String[] PROJ_PRIMARY = new String[] {
1339                    DataColumns.CONCRETE_ID,
1340                    Contacts.IS_RESTRICTED,
1341                    ContactsColumns.PACKAGE_ID,
1342                    scoreClause.toString(),
1343            };
1344
1345            final int COL_DATA_ID = 0;
1346            final int COL_IS_RESTRICTED = 1;
1347            final int COL_PACKAGE_ID = 2;
1348            final int COL_SCORE = 3;
1349
1350            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES, PROJ_PRIMARY,
1351                    AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND " + DataColumns.MIMETYPE_ID
1352                            + "=" + mimeId, null, null, null, SCORE);
1353
1354            if (fixOptimal) {
1355                String colId = null;
1356                String colPackageId = null;
1357                if (mimeId == mimePhone) {
1358                    colId = AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID;
1359                    colPackageId = AggregatesColumns.OPTIMAL_PRIMARY_PHONE_PACKAGE_ID;
1360                } else if (mimeId == mimeEmail) {
1361                    colId = AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID;
1362                    colPackageId = AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_PACKAGE_ID;
1363                }
1364
1365                // Start by replacing with null, since fixOptimal told us that
1366                // the previous aggregate values are bad.
1367                values.putNull(colId);
1368                values.putNull(colPackageId);
1369
1370                // When finding a new optimal primary, we only care about the
1371                // highest scoring value, regardless of source.
1372                if (cursor.moveToFirst()) {
1373                    final long newOptimal = cursor.getLong(COL_DATA_ID);
1374                    final long newOptimalPackage = cursor.getLong(COL_PACKAGE_ID);
1375
1376                    if (newOptimal != 0) {
1377                        values.put(colId, newOptimal);
1378                    }
1379                    if (newOptimalPackage != 0) {
1380                        values.put(colPackageId, newOptimalPackage);
1381                    }
1382                }
1383            }
1384
1385            if (fixFallback) {
1386                String colId = null;
1387                if (mimeId == mimePhone) {
1388                    colId = AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID;
1389                } else if (mimeId == mimeEmail) {
1390                    colId = AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID;
1391                }
1392
1393                // Start by replacing with null, since fixFallback told us that
1394                // the previous aggregate values are bad.
1395                values.putNull(colId);
1396
1397                // The best fallback value is the highest scoring data item that
1398                // hasn't been restricted.
1399                cursor.moveToPosition(-1);
1400                while (cursor.moveToNext()) {
1401                    final boolean isRestricted = (cursor.getInt(COL_IS_RESTRICTED) == 1);
1402                    if (!isRestricted) {
1403                        values.put(colId, cursor.getLong(COL_DATA_ID));
1404                        break;
1405                    }
1406                }
1407            }
1408
1409            // Push through any aggregate updates we have
1410            if (values.size() > 0) {
1411                db.update(Tables.AGGREGATES, values, AggregatesColumns.CONCRETE_ID + "=" + aggId,
1412                        null);
1413            }
1414        }
1415
1416        return dataDeleted;
1417    }
1418
1419    /**
1420     * Inserts an item in the groups table
1421     */
1422    private long insertGroup(ContentValues values, Account account) {
1423        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1424
1425        ContentValues overriddenValues = new ContentValues(values);
1426        if (!resolveAccount(overriddenValues, account)) {
1427            return -1;
1428        }
1429
1430        // Replace package with internal mapping
1431        final String packageName = overriddenValues.getAsString(Groups.PACKAGE);
1432        overriddenValues.put(Groups.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1433        overriddenValues.remove(Groups.PACKAGE);
1434
1435        return db.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
1436    }
1437
1438    /**
1439     * Inserts a presence update.
1440     */
1441    private long insertPresence(ContentValues values) {
1442        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1443        final String handle = values.getAsString(Presence.IM_HANDLE);
1444        final String protocol = values.getAsString(Presence.IM_PROTOCOL);
1445        if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
1446            throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required");
1447        }
1448
1449        // TODO: generalize to allow other providers to match against email
1450        boolean matchEmail = GTALK_PROTOCOL_STRING.equals(protocol);
1451
1452        String selection;
1453        String[] selectionArgs;
1454        if (matchEmail) {
1455            selection = "(" + Clauses.WHERE_IM_MATCHES + ") OR (" + Clauses.WHERE_EMAIL_MATCHES + ")";
1456            selectionArgs = new String[] { protocol, handle, handle };
1457        } else {
1458            selection = Clauses.WHERE_IM_MATCHES;
1459            selectionArgs = new String[] { protocol, handle };
1460        }
1461
1462        long dataId = -1;
1463        long aggId = -1;
1464        Cursor cursor = null;
1465        try {
1466            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES,
1467                    Projections.PROJ_DATA_CONTACTS, selection, selectionArgs, null, null, null);
1468            if (cursor.moveToFirst()) {
1469                dataId = cursor.getLong(Projections.COL_DATA_ID);
1470                aggId = cursor.getLong(Projections.COL_AGGREGATE_ID);
1471            } else {
1472                // No contact found, return a null URI
1473                return -1;
1474            }
1475        } finally {
1476            if (cursor != null) {
1477                cursor.close();
1478            }
1479        }
1480
1481        values.put(Presence.DATA_ID, dataId);
1482        values.put(Presence.AGGREGATE_ID, aggId);
1483
1484        // Insert the presence update
1485        long presenceId = db.replace(Tables.PRESENCE, null, values);
1486        return presenceId;
1487    }
1488
1489    @Override
1490    public int delete(Uri uri, String selection, String[] selectionArgs) {
1491        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1492        final int match = sUriMatcher.match(uri);
1493        switch (match) {
1494            case SYNCSTATE:
1495                return mOpenHelper.getSyncState().delete(db, selection, selectionArgs);
1496
1497            case AGGREGATES_ID: {
1498                long aggregateId = ContentUris.parseId(uri);
1499
1500                // Remove references to the aggregate first
1501                ContentValues values = new ContentValues();
1502                values.putNull(Contacts.AGGREGATE_ID);
1503                db.update(Tables.CONTACTS, values, Contacts.AGGREGATE_ID + "=" + aggregateId, null);
1504
1505                return db.delete(Tables.AGGREGATES, BaseColumns._ID + "=" + aggregateId, null);
1506            }
1507
1508            case CONTACTS_ID: {
1509                long contactId = ContentUris.parseId(uri);
1510                int contactsDeleted = db.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
1511                int dataDeleted = db.delete(Tables.DATA, Data.CONTACT_ID + "=" + contactId, null);
1512                return contactsDeleted + dataDeleted;
1513            }
1514
1515            case DATA_ID: {
1516                long dataId = ContentUris.parseId(uri);
1517                return deleteData(dataId);
1518            }
1519
1520            case GROUPS_ID: {
1521                long groupId = ContentUris.parseId(uri);
1522                final long groupMembershipMimetypeId = mOpenHelper
1523                        .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
1524                int groupsDeleted = db.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
1525                int dataDeleted = db.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
1526                        + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
1527                        + groupId, null);
1528                mOpenHelper.updateAllVisible();
1529                return groupsDeleted + dataDeleted;
1530            }
1531
1532            case PRESENCE: {
1533                return db.delete(Tables.PRESENCE, null, null);
1534            }
1535
1536            default:
1537                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
1538        }
1539    }
1540
1541    private static Account readAccountFromQueryParams(Uri uri) {
1542        final String name = uri.getQueryParameter(Contacts.ACCOUNT_NAME);
1543        final String type = uri.getQueryParameter(Contacts.ACCOUNT_TYPE);
1544        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
1545            return null;
1546        }
1547        return new Account(name, type);
1548    }
1549
1550
1551    @Override
1552    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1553        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1554        int count = 0;
1555
1556        final int match = sUriMatcher.match(uri);
1557        switch(match) {
1558            case SYNCSTATE:
1559                return mOpenHelper.getSyncState().update(db, values, selection, selectionArgs);
1560
1561            // TODO(emillar): We will want to disallow editing the aggregates table at some point.
1562            case AGGREGATES: {
1563                count = db.update(Tables.AGGREGATES, values, selection, selectionArgs);
1564                break;
1565            }
1566
1567            case AGGREGATES_ID: {
1568                count = updateAggregateData(db, ContentUris.parseId(uri), values);
1569                break;
1570            }
1571
1572            case DATA_ID: {
1573                boolean containsIsSuperPrimary = values.containsKey(Data.IS_SUPER_PRIMARY);
1574                boolean containsIsPrimary = values.containsKey(Data.IS_PRIMARY);
1575                final long id = ContentUris.parseId(uri);
1576
1577                // Remove primary or super primary values being set to 0. This is disallowed by the
1578                // content provider.
1579                if (containsIsSuperPrimary && values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
1580                    containsIsSuperPrimary = false;
1581                    values.remove(Data.IS_SUPER_PRIMARY);
1582                }
1583                if (containsIsPrimary && values.getAsInteger(Data.IS_PRIMARY) == 0) {
1584                    containsIsPrimary = false;
1585                    values.remove(Data.IS_PRIMARY);
1586                }
1587
1588                if (containsIsSuperPrimary) {
1589                    setIsSuperPrimary(id);
1590                    setIsPrimary(id);
1591
1592                    // Now that we've taken care of setting these, remove them from "values".
1593                    values.remove(Data.IS_SUPER_PRIMARY);
1594                    if (containsIsPrimary) {
1595                        values.remove(Data.IS_PRIMARY);
1596                    }
1597                } else if (containsIsPrimary) {
1598                    setIsPrimary(id);
1599
1600                    // Now that we've taken care of setting this, remove it from "values".
1601                    values.remove(Data.IS_PRIMARY);
1602                }
1603
1604                if (values.size() > 0) {
1605                    String selectionWithId = (Data._ID + " = " + ContentUris.parseId(uri) + " ")
1606                            + (selection == null ? "" : " AND " + selection);
1607                    count = db.update(Tables.DATA, values, selectionWithId, selectionArgs);
1608                }
1609                break;
1610            }
1611
1612            case CONTACTS: {
1613                count = db.update(Tables.CONTACTS, values, selection, selectionArgs);
1614                break;
1615            }
1616
1617            case CONTACTS_ID: {
1618                String selectionWithId = (Contacts._ID + " = " + ContentUris.parseId(uri) + " ")
1619                        + (selection == null ? "" : " AND " + selection);
1620                count = db.update(Tables.CONTACTS, values, selectionWithId, selectionArgs);
1621                Log.i(TAG, "Selection is: " + selectionWithId);
1622                break;
1623            }
1624
1625            case DATA: {
1626                count = db.update(Tables.DATA, values, selection, selectionArgs);
1627                break;
1628            }
1629
1630            case GROUPS: {
1631                count = db.update(Tables.GROUPS, values, selection, selectionArgs);
1632                mOpenHelper.updateAllVisible();
1633                break;
1634            }
1635
1636            case GROUPS_ID: {
1637                long groupId = ContentUris.parseId(uri);
1638                String selectionWithId = (Groups._ID + "=" + groupId + " ")
1639                        + (selection == null ? "" : " AND " + selection);
1640                count = db.update(Tables.GROUPS, values, selectionWithId, selectionArgs);
1641
1642                // If changing visibility, then update aggregates
1643                if (values.containsKey(Groups.GROUP_VISIBLE)) {
1644                    mOpenHelper.updateAllVisible();
1645                }
1646
1647                break;
1648            }
1649
1650            case AGGREGATION_EXCEPTIONS: {
1651                count = updateAggregationException(db, values);
1652                break;
1653            }
1654
1655            case RESTRICTION_EXCEPTIONS: {
1656                // Enforce required fields
1657                boolean hasFields = values.containsKey(RestrictionExceptions.PACKAGE_PROVIDER)
1658                        && values.containsKey(RestrictionExceptions.PACKAGE_CLIENT)
1659                        && values.containsKey(RestrictionExceptions.ALLOW_ACCESS);
1660                if (!hasFields) {
1661                    throw new IllegalArgumentException("PACKAGE_PROVIDER, PACKAGE_CLIENT, and"
1662                            + "ALLOW_ACCESS are all required fields");
1663                }
1664
1665                final String packageProvider = values
1666                        .getAsString(RestrictionExceptions.PACKAGE_PROVIDER);
1667                final boolean allowAccess = (values
1668                        .getAsInteger(RestrictionExceptions.ALLOW_ACCESS) == 1);
1669
1670                final Context context = getContext();
1671                final PackageManager pm = context.getPackageManager();
1672
1673                // Enforce that caller has authority over the requested package
1674                // TODO: move back to Binder.getCallingUid() when we can stub-out test suite
1675                final int callingUid = OpenHelper
1676                        .getUidForPackageName(pm, context.getPackageName());
1677                final String[] ownedPackages = pm.getPackagesForUid(callingUid);
1678                if (!isContained(ownedPackages, packageProvider)) {
1679                    throw new RuntimeException(
1680                            "Requested PACKAGE_PROVIDER doesn't belong to calling UID.");
1681                }
1682
1683                // Add or remove exception using exception helper
1684                if (allowAccess) {
1685                    mOpenHelper.addRestrictionException(context, values);
1686                } else {
1687                    mOpenHelper.removeRestrictionException(context, values);
1688                }
1689
1690                break;
1691            }
1692
1693            default:
1694                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
1695        }
1696
1697        if (count > 0) {
1698            getContext().getContentResolver().notifyChange(uri, null);
1699        }
1700        return count;
1701    }
1702
1703    private int updateAggregateData(SQLiteDatabase db, long aggregateId, ContentValues values) {
1704
1705        // First update all constituent contacts
1706        ContentValues optionValues = new ContentValues(5);
1707        OpenHelper.copyStringValue(optionValues, Contacts.CUSTOM_RINGTONE,
1708                values, Aggregates.CUSTOM_RINGTONE);
1709        OpenHelper.copyLongValue(optionValues, Contacts.SEND_TO_VOICEMAIL,
1710                values, Aggregates.SEND_TO_VOICEMAIL);
1711        OpenHelper.copyLongValue(optionValues, Contacts.LAST_TIME_CONTACTED,
1712                values, Aggregates.LAST_TIME_CONTACTED);
1713        OpenHelper.copyLongValue(optionValues, Contacts.TIMES_CONTACTED,
1714                values, Aggregates.TIMES_CONTACTED);
1715        OpenHelper.copyLongValue(optionValues, Contacts.STARRED,
1716                values, Aggregates.STARRED);
1717
1718        // Nothing to update - just return
1719        if (optionValues.size() == 0) {
1720            return 0;
1721        }
1722
1723        db.update(Tables.CONTACTS, optionValues, Contacts.AGGREGATE_ID + "=" + aggregateId, null);
1724        return db.update(Tables.AGGREGATES, values, Aggregates._ID + "=" + aggregateId, null);
1725    }
1726
1727    public void updateContactTime(long aggregateId, long lastTimeContacted) {
1728        mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
1729        mLastTimeContactedUpdate.bindLong(2, aggregateId);
1730        mLastTimeContactedUpdate.execute();
1731    }
1732
1733    private static class ContactPair {
1734        final long contactId1;
1735        final long contactId2;
1736
1737        /**
1738         * Constructor that ensures that this.contactId1 &lt; this.contactId2
1739         */
1740        public ContactPair(long contactId1, long contactId2) {
1741            if (contactId1 < contactId2) {
1742                this.contactId1 = contactId1;
1743                this.contactId2 = contactId2;
1744            } else {
1745                this.contactId2 = contactId1;
1746                this.contactId1 = contactId2;
1747            }
1748        }
1749    }
1750
1751    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
1752        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
1753        long aggregateId = values.getAsInteger(AggregationExceptions.AGGREGATE_ID);
1754        long contactId = values.getAsInteger(AggregationExceptions.CONTACT_ID);
1755
1756        // First, we build a list of contactID-contactID pairs for the given aggregate and contact.
1757        ArrayList<ContactPair> pairs = new ArrayList<ContactPair>();
1758        Cursor c = db.query(Tables.CONTACTS, Projections.PROJ_CONTACTS,
1759                Contacts.AGGREGATE_ID + "=" + aggregateId,
1760                null, null, null, null);
1761        try {
1762            while (c.moveToNext()) {
1763                long aggregatedContactId = c.getLong(Projections.COL_CONTACT_ID);
1764                if (aggregatedContactId != contactId) {
1765                    pairs.add(new ContactPair(aggregatedContactId, contactId));
1766                }
1767            }
1768        } finally {
1769            c.close();
1770        }
1771
1772        // Now we iterate through all contact pairs to see if we need to insert/delete/update
1773        // the corresponding exception
1774        ContentValues exceptionValues = new ContentValues(3);
1775        exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
1776        for (ContactPair pair : pairs) {
1777            final String whereClause =
1778                    AggregationExceptionColumns.CONTACT_ID1 + "=" + pair.contactId1 + " AND "
1779                    + AggregationExceptionColumns.CONTACT_ID2 + "=" + pair.contactId2;
1780            if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
1781                db.delete(Tables.AGGREGATION_EXCEPTIONS, whereClause, null);
1782            } else {
1783                exceptionValues.put(AggregationExceptionColumns.CONTACT_ID1, pair.contactId1);
1784                exceptionValues.put(AggregationExceptionColumns.CONTACT_ID2, pair.contactId2);
1785                db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
1786                        exceptionValues);
1787            }
1788        }
1789
1790        int aggregationMode = mContactAggregator.markContactForAggregation(contactId);
1791        if (aggregationMode != Contacts.AGGREGATION_MODE_DISABLED) {
1792            mContactAggregator.aggregateContact(db, contactId);
1793            if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC
1794                    || exceptionType == AggregationExceptions.TYPE_KEEP_OUT) {
1795                mContactAggregator.updateAggregateData(aggregateId);
1796            }
1797        }
1798
1799        // The return value is fake - we just confirm that we made a change, not count actual
1800        // rows changed.
1801        return 1;
1802    }
1803
1804    /**
1805     * Test if a {@link String} value appears in the given list.
1806     */
1807    private boolean isContained(String[] array, String value) {
1808        if (array != null) {
1809            for (String test : array) {
1810                if (value.equals(test)) {
1811                    return true;
1812                }
1813            }
1814        }
1815        return false;
1816    }
1817
1818    /**
1819     * Test if a {@link String} value appears in the given list, and add to the
1820     * array if the value doesn't already appear.
1821     */
1822    private String[] assertContained(String[] array, String value) {
1823        if (array == null) {
1824            array = new String[] {value};
1825        } else if (!isContained(array, value)) {
1826            String[] newArray = new String[array.length + 1];
1827            System.arraycopy(array, 0, newArray, 0, array.length);
1828            newArray[array.length] = value;
1829            array = newArray;
1830        }
1831        return array;
1832    }
1833
1834    @Override
1835    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
1836            String sortOrder) {
1837        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1838
1839        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1840        String groupBy = null;
1841        String limit = null;
1842        String aggregateIdColName = Tables.AGGREGATES + "." + Aggregates._ID;
1843
1844        // TODO: Consider writing a test case for RestrictionExceptions when you
1845        // write a new query() block to make sure it protects restricted data.
1846        final int match = sUriMatcher.match(uri);
1847        switch (match) {
1848            case SYNCSTATE:
1849                return mOpenHelper.getSyncState().query(db, projection, selection,  selectionArgs,
1850                        sortOrder);
1851
1852            case AGGREGATES: {
1853                qb.setTables(Tables.AGGREGATES);
1854                applyAggregateRestrictionExceptions(qb);
1855                applyAggregatePrimaryRestrictionExceptions(sAggregatesProjectionMap);
1856                qb.setProjectionMap(sAggregatesProjectionMap);
1857                break;
1858            }
1859
1860            case AGGREGATES_ID: {
1861                long aggId = ContentUris.parseId(uri);
1862                qb.setTables(Tables.AGGREGATES);
1863                qb.appendWhere(AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND ");
1864                applyAggregateRestrictionExceptions(qb);
1865                applyAggregatePrimaryRestrictionExceptions(sAggregatesProjectionMap);
1866                qb.setProjectionMap(sAggregatesProjectionMap);
1867                break;
1868            }
1869
1870            case AGGREGATES_SUMMARY: {
1871                // TODO: join into social status tables
1872                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1873                applyAggregateRestrictionExceptions(qb);
1874                applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap);
1875                projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID);
1876                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1877                groupBy = aggregateIdColName;
1878                break;
1879            }
1880
1881            case AGGREGATES_SUMMARY_ID: {
1882                // TODO: join into social status tables
1883                long aggId = ContentUris.parseId(uri);
1884                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1885                qb.appendWhere(AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND ");
1886                applyAggregateRestrictionExceptions(qb);
1887                applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap);
1888                projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID);
1889                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1890                groupBy = aggregateIdColName;
1891                break;
1892            }
1893
1894            case AGGREGATES_SUMMARY_FILTER: {
1895                // TODO: filter query based on callingUid
1896                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1897                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1898                if (uri.getPathSegments().size() > 2) {
1899                    qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment()));
1900                }
1901                groupBy = aggregateIdColName;
1902                break;
1903            }
1904
1905            case AGGREGATES_SUMMARY_STREQUENT_FILTER:
1906            case AGGREGATES_SUMMARY_STREQUENT: {
1907                // Build the first query for starred
1908                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1909                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1910                if (match == AGGREGATES_SUMMARY_STREQUENT_FILTER
1911                        && uri.getPathSegments().size() > 3) {
1912                    qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment()));
1913                }
1914                final String starredQuery = qb.buildQuery(projection, Aggregates.STARRED + "=1",
1915                        null, aggregateIdColName, null, null,
1916                        null /* limit */);
1917
1918                // Build the second query for frequent
1919                qb = new SQLiteQueryBuilder();
1920                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1921                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1922                if (match == AGGREGATES_SUMMARY_STREQUENT_FILTER
1923                        && uri.getPathSegments().size() > 3) {
1924                    qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment()));
1925                }
1926                final String frequentQuery = qb.buildQuery(projection,
1927                        Aggregates.TIMES_CONTACTED + " > 0 AND (" + Aggregates.STARRED
1928                        + " = 0 OR " + Aggregates.STARRED + " IS NULL)",
1929                        null, aggregateIdColName, null, null, null);
1930
1931                // Put them together
1932                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
1933                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
1934                Cursor c = db.rawQueryWithFactory(null, query, null,
1935                        Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1936
1937                if ((c != null) && !isTemporary()) {
1938                    c.setNotificationUri(getContext().getContentResolver(),
1939                            ContactsContract.AUTHORITY_URI);
1940                }
1941                return c;
1942            }
1943
1944            case AGGREGATES_SUMMARY_GROUP: {
1945                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1946                applyAggregateRestrictionExceptions(qb);
1947                applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap);
1948                projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID);
1949                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1950                if (uri.getPathSegments().size() > 2) {
1951                    qb.appendWhere(" AND " + sAggregatesInGroupSelect);
1952                    selectionArgs = appendGroupArg(selectionArgs, uri.getLastPathSegment());
1953                }
1954                groupBy = aggregateIdColName;
1955                break;
1956            }
1957
1958            case AGGREGATES_DATA: {
1959                long aggId = Long.parseLong(uri.getPathSegments().get(1));
1960                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES_GROUPS);
1961                qb.setProjectionMap(sDataContactsGroupsAggregateProjectionMap);
1962                qb.appendWhere(Contacts.AGGREGATE_ID + "=" + aggId + " AND ");
1963                applyDataRestrictionExceptions(qb);
1964                break;
1965            }
1966
1967            case PHONES_FILTER: {
1968                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES);
1969                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1970                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
1971                if (uri.getPathSegments().size() > 2) {
1972                    qb.appendWhere(" AND " + buildAggregateLookupWhereClause(
1973                            uri.getLastPathSegment()));
1974                }
1975                break;
1976            }
1977
1978            case PHONES: {
1979                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES);
1980                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1981                qb.appendWhere(Data.MIMETYPE + " = \"" + Phone.CONTENT_ITEM_TYPE + "\"");
1982                break;
1983            }
1984
1985            case POSTALS: {
1986                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES);
1987                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1988                qb.appendWhere(Data.MIMETYPE + " = \"" + Postal.CONTENT_ITEM_TYPE + "\"");
1989                break;
1990            }
1991
1992            case CONTACTS: {
1993                qb.setTables(Tables.CONTACTS_JOIN_PACKAGES);
1994                qb.setProjectionMap(sContactsProjectionMap);
1995                applyContactsRestrictionExceptions(qb);
1996                break;
1997            }
1998
1999            case CONTACTS_ID: {
2000                long contactId = ContentUris.parseId(uri);
2001                qb.setTables(Tables.CONTACTS_JOIN_PACKAGES);
2002                qb.setProjectionMap(sContactsProjectionMap);
2003                qb.appendWhere(ContactsColumns.CONCRETE_ID + "=" + contactId + " AND ");
2004                applyContactsRestrictionExceptions(qb);
2005                break;
2006            }
2007
2008            case CONTACTS_DATA: {
2009                long contactId = Long.parseLong(uri.getPathSegments().get(1));
2010                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_GROUPS);
2011                qb.setProjectionMap(sDataContactsGroupsProjectionMap);
2012                qb.appendWhere(Data.CONTACT_ID + "=" + contactId + " AND ");
2013                applyDataRestrictionExceptions(qb);
2014                break;
2015            }
2016
2017            case CONTACTS_FILTER_EMAIL: {
2018                // TODO: filter query based on callingUid
2019                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES);
2020                qb.setProjectionMap(sDataContactsProjectionMap);
2021                qb.appendWhere(Data.MIMETYPE + "='" + CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'");
2022                qb.appendWhere(" AND " + CommonDataKinds.Email.DATA + "=");
2023                qb.appendWhereEscapeString(uri.getPathSegments().get(2));
2024                break;
2025            }
2026
2027            case DATA: {
2028                final String accountName = uri.getQueryParameter(Contacts.ACCOUNT_NAME);
2029                final String accountType = uri.getQueryParameter(Contacts.ACCOUNT_TYPE);
2030                if (!TextUtils.isEmpty(accountName)) {
2031                    qb.appendWhere(Contacts.ACCOUNT_NAME + "="
2032                            + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2033                            + Contacts.ACCOUNT_TYPE + "="
2034                            + DatabaseUtils.sqlEscapeString(accountType) + " AND ");
2035                }
2036                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_GROUPS);
2037                qb.setProjectionMap(sDataGroupsProjectionMap);
2038                applyDataRestrictionExceptions(qb);
2039                break;
2040            }
2041
2042            case DATA_ID: {
2043                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_GROUPS);
2044                qb.setProjectionMap(sDataGroupsProjectionMap);
2045                qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri) + " AND ");
2046                applyDataRestrictionExceptions(qb);
2047                break;
2048            }
2049
2050            case PHONE_LOOKUP: {
2051                // TODO: filter query based on callingUid
2052                if (TextUtils.isEmpty(sortOrder)) {
2053                    // Default the sort order to something reasonable so we get consistent
2054                    // results when callers don't request an ordering
2055                    sortOrder = Data.CONTACT_ID;
2056                }
2057
2058                final String number = uri.getLastPathSegment();
2059                OpenHelper.buildPhoneLookupQuery(qb, number);
2060                qb.setProjectionMap(sDataContactsProjectionMap);
2061                break;
2062            }
2063
2064            case GROUPS: {
2065                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2066                qb.setProjectionMap(sGroupsProjectionMap);
2067                break;
2068            }
2069
2070            case GROUPS_ID: {
2071                long groupId = ContentUris.parseId(uri);
2072                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2073                qb.setProjectionMap(sGroupsProjectionMap);
2074                qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId);
2075                break;
2076            }
2077
2078            case GROUPS_SUMMARY: {
2079                qb.setTables(Tables.GROUPS_JOIN_PACKAGES_DATA_CONTACTS_AGGREGATES);
2080                qb.setProjectionMap(sGroupsSummaryProjectionMap);
2081                groupBy = GroupsColumns.CONCRETE_ID;
2082                break;
2083            }
2084
2085            case AGGREGATION_EXCEPTIONS: {
2086                qb.setTables(Tables.AGGREGATION_EXCEPTIONS_JOIN_CONTACTS);
2087                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
2088                break;
2089            }
2090
2091            case AGGREGATION_SUGGESTIONS: {
2092                long aggregateId = Long.parseLong(uri.getPathSegments().get(1));
2093                final String maxSuggestionsParam =
2094                        uri.getQueryParameter(AggregationSuggestions.MAX_SUGGESTIONS);
2095
2096                final int maxSuggestions;
2097                if (maxSuggestionsParam != null) {
2098                    maxSuggestions = Integer.parseInt(maxSuggestionsParam);
2099                } else {
2100                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
2101                }
2102
2103                return mContactAggregator.queryAggregationSuggestions(aggregateId, projection,
2104                        sAggregatesProjectionMap, maxSuggestions);
2105            }
2106
2107            case RESTRICTION_EXCEPTIONS: {
2108                qb.setTables(Tables.RESTRICTION_EXCEPTIONS);
2109                qb.setProjectionMap(sRestrictionExceptionsProjectionMap);
2110                break;
2111            }
2112
2113            default:
2114                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
2115                        sortOrder);
2116        }
2117
2118        // Perform the query and set the notification uri
2119        final Cursor c = qb.query(db, projection, selection, selectionArgs,
2120                groupBy, null, sortOrder, limit);
2121        if (c != null) {
2122            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
2123        }
2124        return c;
2125    }
2126
2127    /**
2128     * Restrict selection of {@link Aggregates} to only public ones, or those
2129     * the caller has been granted a {@link RestrictionExceptions} to.
2130     */
2131    private void applyAggregateRestrictionExceptions(SQLiteQueryBuilder qb) {
2132        final int clientUid = OpenHelper.getUidForPackageName(getContext().getPackageManager(),
2133                getContext().getPackageName());
2134
2135        qb.appendWhere("(" + AggregatesColumns.SINGLE_RESTRICTED_PACKAGE_ID + " IS NULL");
2136        final String exceptionClause = mOpenHelper.getRestrictionExceptionClause(clientUid,
2137                AggregatesColumns.SINGLE_RESTRICTED_PACKAGE_ID);
2138        if (exceptionClause != null) {
2139            qb.appendWhere(" OR (" + exceptionClause + ")");
2140        }
2141        qb.appendWhere(")");
2142    }
2143
2144    /**
2145     * Find any exceptions that have been granted to the calling process, and
2146     * add projections to correctly select {@link Aggregates#PRIMARY_PHONE_ID}
2147     * and {@link Aggregates#PRIMARY_EMAIL_ID}.
2148     */
2149    private void applyAggregatePrimaryRestrictionExceptions(HashMap<String, String> projection) {
2150        // TODO: move back to Binder.getCallingUid() when we can stub-out test suite
2151        final int clientUid = OpenHelper.getUidForPackageName(getContext().getPackageManager(),
2152                getContext().getPackageName());
2153
2154        final String projectionPhone = "(CASE WHEN "
2155                + mOpenHelper.getRestrictionExceptionClause(clientUid,
2156                        AggregatesColumns.OPTIMAL_PRIMARY_PHONE_PACKAGE_ID) + " THEN "
2157                + AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID + " ELSE "
2158                + AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID + " END) AS "
2159                + Aggregates.PRIMARY_PHONE_ID;
2160        projection.remove(Aggregates.PRIMARY_PHONE_ID);
2161        projection.put(Aggregates.PRIMARY_PHONE_ID, projectionPhone);
2162
2163        final String projectionEmail = "(CASE WHEN "
2164            + mOpenHelper.getRestrictionExceptionClause(clientUid,
2165                    AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_PACKAGE_ID) + " THEN "
2166            + AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID + " ELSE "
2167            + AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID + " END) AS "
2168            + Aggregates.PRIMARY_EMAIL_ID;
2169        projection.remove(Aggregates.PRIMARY_EMAIL_ID);
2170        projection.put(Aggregates.PRIMARY_EMAIL_ID, projectionEmail);
2171    }
2172
2173    /**
2174     * Find any exceptions that have been granted to the
2175     * {@link Binder#getCallingUid()}, and add a limiting clause to the given
2176     * {@link SQLiteQueryBuilder} to hide restricted data.
2177     */
2178    private void applyContactsRestrictionExceptions(SQLiteQueryBuilder qb) {
2179        // TODO: move back to Binder.getCallingUid() when we can stub-out test suite
2180        final int clientUid = OpenHelper.getUidForPackageName(getContext().getPackageManager(),
2181                getContext().getPackageName());
2182
2183        qb.appendWhere("(" + Contacts.IS_RESTRICTED + "=0");
2184        final String exceptionClause = mOpenHelper.getRestrictionExceptionClause(clientUid,
2185                OpenHelper.ContactsColumns.CONCRETE_PACKAGE_ID);
2186        if (exceptionClause != null) {
2187            qb.appendWhere(" OR (" + exceptionClause + ")");
2188        }
2189        qb.appendWhere(")");
2190    }
2191
2192    /**
2193     * Find any exceptions that have been granted to the
2194     * {@link Binder#getCallingUid()}, and add a limiting clause to the given
2195     * {@link SQLiteQueryBuilder} to hide restricted data.
2196     */
2197    void applyDataRestrictionExceptions(SQLiteQueryBuilder qb) {
2198        applyContactsRestrictionExceptions(qb);
2199    }
2200
2201    /**
2202     * An implementation of EntityIterator that joins the contacts and data tables
2203     * and consumes all the data rows for a contact in order to build the Entity for a contact.
2204     */
2205    private static class ContactsEntityIterator implements EntityIterator {
2206        private final Cursor mEntityCursor;
2207        private volatile boolean mIsClosed;
2208
2209        private static final String[] DATA_KEYS = new String[]{
2210                "data1",
2211                "data2",
2212                "data3",
2213                "data4",
2214                "data5",
2215                "data6",
2216                "data7",
2217                "data8",
2218                "data9",
2219                "data10"};
2220
2221        private static final String[] PROJECTION = new String[]{
2222                Contacts.ACCOUNT_NAME,
2223                Contacts.ACCOUNT_TYPE,
2224                Contacts.SOURCE_ID,
2225                Contacts.VERSION,
2226                Contacts.DIRTY,
2227                Contacts.Data._ID,
2228                Contacts.Data.MIMETYPE,
2229                Contacts.Data.DATA1,
2230                Contacts.Data.DATA2,
2231                Contacts.Data.DATA3,
2232                Contacts.Data.DATA4,
2233                Contacts.Data.DATA5,
2234                Contacts.Data.DATA6,
2235                Contacts.Data.DATA7,
2236                Contacts.Data.DATA8,
2237                Contacts.Data.DATA9,
2238                Contacts.Data.DATA10,
2239                Contacts.Data.CONTACT_ID,
2240                Contacts.Data.IS_PRIMARY,
2241                Contacts.Data.DATA_VERSION,
2242                GroupMembership.GROUP_SOURCE_ID};
2243
2244        private static final int COLUMN_ACCOUNT_NAME = 0;
2245        private static final int COLUMN_ACCOUNT_TYPE = 1;
2246        private static final int COLUMN_SOURCE_ID = 2;
2247        private static final int COLUMN_VERSION = 3;
2248        private static final int COLUMN_DIRTY = 4;
2249        private static final int COLUMN_DATA_ID = 5;
2250        private static final int COLUMN_MIMETYPE = 6;
2251        private static final int COLUMN_DATA1 = 7;
2252        private static final int COLUMN_CONTACT_ID = 17;
2253        private static final int COLUMN_IS_PRIMARY = 18;
2254        private static final int COLUMN_DATA_VERSION = 19;
2255        private static final int COLUMN_GROUP_SOURCE_ID = 20;
2256
2257        public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri,
2258                String selection, String[] selectionArgs, String sortOrder) {
2259            mIsClosed = false;
2260
2261            final String updatedSortOrder = (sortOrder == null)
2262                    ? Contacts.Data.CONTACT_ID
2263                    : (Contacts.Data.CONTACT_ID + "," + sortOrder);
2264
2265            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
2266            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2267            qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_GROUPS);
2268            qb.setProjectionMap(sDataContactsGroupsProjectionMap);
2269            if (contactsIdString != null) {
2270                qb.appendWhere(Data.CONTACT_ID + "=" + contactsIdString);
2271            }
2272            final String accountName = uri.getQueryParameter(Contacts.ACCOUNT_NAME);
2273            final String accountType = uri.getQueryParameter(Contacts.ACCOUNT_TYPE);
2274            if (!TextUtils.isEmpty(accountName)) {
2275                qb.appendWhere(Contacts.ACCOUNT_NAME + "="
2276                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2277                        + Contacts.ACCOUNT_TYPE + "="
2278                        + DatabaseUtils.sqlEscapeString(accountType));
2279            }
2280            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
2281                    null, null, updatedSortOrder);
2282            mEntityCursor.moveToFirst();
2283        }
2284
2285        public void close() {
2286            if (mIsClosed) {
2287                throw new IllegalStateException("closing when already closed");
2288            }
2289            mIsClosed = true;
2290            mEntityCursor.close();
2291        }
2292
2293        public boolean hasNext() throws RemoteException {
2294            if (mIsClosed) {
2295                throw new IllegalStateException("calling hasNext() when the iterator is closed");
2296            }
2297
2298            return !mEntityCursor.isAfterLast();
2299        }
2300
2301        public Entity next() throws RemoteException {
2302            if (mIsClosed) {
2303                throw new IllegalStateException("calling next() when the iterator is closed");
2304            }
2305            if (!hasNext()) {
2306                throw new IllegalStateException("you may only call next() if hasNext() is true");
2307            }
2308
2309            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
2310
2311            final long contactId = c.getLong(COLUMN_CONTACT_ID);
2312
2313            // we expect the cursor is already at the row we need to read from
2314            ContentValues contactValues = new ContentValues();
2315            contactValues.put(Contacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
2316            contactValues.put(Contacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
2317            contactValues.put(Contacts._ID, contactId);
2318            contactValues.put(Contacts.DIRTY, c.getLong(COLUMN_DIRTY));
2319            contactValues.put(Contacts.VERSION, c.getLong(COLUMN_VERSION));
2320            contactValues.put(Contacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
2321            Entity contact = new Entity(contactValues);
2322
2323            // read data rows until the contact id changes
2324            do {
2325                if (contactId != c.getLong(COLUMN_CONTACT_ID)) {
2326                    break;
2327                }
2328                // add the data to to the contact
2329                ContentValues dataValues = new ContentValues();
2330                dataValues.put(Contacts.Data._ID, c.getString(COLUMN_DATA_ID));
2331                dataValues.put(Contacts.Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
2332                dataValues.put(Contacts.Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY));
2333                dataValues.put(Contacts.Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
2334                if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
2335                    dataValues.put(GroupMembership.GROUP_SOURCE_ID,
2336                            c.getString(COLUMN_GROUP_SOURCE_ID));
2337                }
2338                dataValues.put(Contacts.Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
2339                for (int i = 0; i < 10; i++) {
2340                    final int columnIndex = i + COLUMN_DATA1;
2341                    String key = DATA_KEYS[i];
2342                    if (c.isNull(columnIndex)) {
2343                        // don't put anything
2344                    } else if (c.isLong(columnIndex)) {
2345                        dataValues.put(key, c.getLong(columnIndex));
2346                    } else if (c.isFloat(columnIndex)) {
2347                        dataValues.put(key, c.getFloat(columnIndex));
2348                    } else if (c.isString(columnIndex)) {
2349                        dataValues.put(key, c.getString(columnIndex));
2350                    } else if (c.isBlob(columnIndex)) {
2351                        dataValues.put(key, c.getBlob(columnIndex));
2352                    }
2353                }
2354                contact.addSubValue(Data.CONTENT_URI, dataValues);
2355            } while (mEntityCursor.moveToNext());
2356
2357            return contact;
2358        }
2359    }
2360
2361    @Override
2362    public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
2363            String sortOrder) {
2364        final int match = sUriMatcher.match(uri);
2365        switch (match) {
2366            case CONTACTS:
2367            case CONTACTS_ID:
2368                String contactsIdString = null;
2369                if (match == CONTACTS_ID) {
2370                    contactsIdString = uri.getPathSegments().get(1);
2371                }
2372
2373                return new ContactsEntityIterator(this, contactsIdString,
2374                        uri, selection, selectionArgs, sortOrder);
2375            default:
2376                throw new UnsupportedOperationException("Unknown uri: " + uri);
2377        }
2378    }
2379
2380    @Override
2381    public String getType(Uri uri) {
2382        final int match = sUriMatcher.match(uri);
2383        switch (match) {
2384            case AGGREGATES: return Aggregates.CONTENT_TYPE;
2385            case AGGREGATES_ID: return Aggregates.CONTENT_ITEM_TYPE;
2386            case CONTACTS: return Contacts.CONTENT_TYPE;
2387            case CONTACTS_ID: return Contacts.CONTENT_ITEM_TYPE;
2388            case DATA_ID:
2389                final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2390                long dataId = ContentUris.parseId(uri);
2391                return mOpenHelper.getDataMimeType(dataId);
2392            case AGGREGATION_EXCEPTIONS: return AggregationExceptions.CONTENT_TYPE;
2393            case AGGREGATION_EXCEPTION_ID: return AggregationExceptions.CONTENT_ITEM_TYPE;
2394            case AGGREGATION_SUGGESTIONS: return Aggregates.CONTENT_TYPE;
2395        }
2396        throw new UnsupportedOperationException("Unknown uri: " + uri);
2397    }
2398
2399    @Override
2400    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
2401            throws OperationApplicationException {
2402
2403        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
2404        db.beginTransaction();
2405        try {
2406            ContentProviderResult[] results = super.applyBatch(operations);
2407            db.setTransactionSuccessful();
2408            return results;
2409        } finally {
2410            db.endTransaction();
2411        }
2412    }
2413
2414    private void setDisplayName(long contactId, String displayName) {
2415        if (displayName != null) {
2416            mContactDisplayNameUpdate.bindString(1, displayName);
2417        } else {
2418            mContactDisplayNameUpdate.bindNull(1);
2419        }
2420        mContactDisplayNameUpdate.bindLong(2, contactId);
2421        mContactDisplayNameUpdate.execute();
2422    }
2423
2424    /*
2425     * Sets the given dataId record in the "data" table to primary, and resets all data records of
2426     * the same mimetype and under the same contact to not be primary.
2427     *
2428     * @param dataId the id of the data record to be set to primary.
2429     */
2430    private void setIsPrimary(long dataId) {
2431        mSetPrimaryStatement.bindLong(1, dataId);
2432        mSetPrimaryStatement.bindLong(2, dataId);
2433        mSetPrimaryStatement.bindLong(3, dataId);
2434        mSetPrimaryStatement.execute();
2435    }
2436
2437    /*
2438     * Sets the given dataId record in the "data" table to "super primary", and resets all data
2439     * records of the same mimetype and under the same aggregate to not be "super primary".
2440     *
2441     * @param dataId the id of the data record to be set to primary.
2442     */
2443    private void setIsSuperPrimary(long dataId) {
2444        mSetSuperPrimaryStatement.bindLong(1, dataId);
2445        mSetSuperPrimaryStatement.bindLong(2, dataId);
2446        mSetSuperPrimaryStatement.bindLong(3, dataId);
2447        mSetSuperPrimaryStatement.execute();
2448
2449        // Find the parent aggregate and package for this new primary
2450        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
2451
2452        long aggId = -1;
2453        long packageId = -1;
2454        boolean isRestricted = false;
2455        String mimeType = null;
2456
2457        Cursor cursor = null;
2458        try {
2459            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES,
2460                    Projections.PROJ_DATA_CONTACTS, DataColumns.CONCRETE_ID + "=" + dataId, null,
2461                    null, null, null);
2462            if (cursor.moveToFirst()) {
2463                aggId = cursor.getLong(Projections.COL_AGGREGATE_ID);
2464                packageId = cursor.getLong(Projections.COL_PACKAGE_ID);
2465                isRestricted = (cursor.getInt(Projections.COL_IS_RESTRICTED) == 1);
2466                mimeType = cursor.getString(Projections.COL_MIMETYPE);
2467            }
2468        } finally {
2469            if (cursor != null) {
2470                cursor.close();
2471            }
2472        }
2473
2474        // Bypass aggregate update if no parent found, or if we don't keep track
2475        // of super-primary for this mimetype.
2476        if (aggId == -1) {
2477            return;
2478        }
2479
2480        boolean isPhone = CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType);
2481        boolean isEmail = CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType);
2482
2483        // Record this value as the new primary for the parent aggregate
2484        final ContentValues values = new ContentValues();
2485        if (isPhone) {
2486            values.put(AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID, dataId);
2487            values.put(AggregatesColumns.OPTIMAL_PRIMARY_PHONE_PACKAGE_ID, packageId);
2488        } else if (isEmail) {
2489            values.put(AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID, dataId);
2490            values.put(AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_PACKAGE_ID, packageId);
2491        }
2492
2493        // If this data is unrestricted, then also set as fallback
2494        if (!isRestricted && isPhone) {
2495            values.put(AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID, dataId);
2496        } else if (!isRestricted && isEmail) {
2497            values.put(AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID, dataId);
2498        }
2499
2500        // Push update into aggregates table, if needed
2501        if (values.size() > 0) {
2502            db.update(Tables.AGGREGATES, values, Aggregates._ID + "=" + aggId, null);
2503        }
2504
2505    }
2506
2507    private String buildAggregateLookupWhereClause(String filterParam) {
2508        StringBuilder filter = new StringBuilder();
2509        filter.append(Tables.AGGREGATES);
2510        filter.append(".");
2511        filter.append(Aggregates._ID);
2512        filter.append(" IN (SELECT ");
2513        filter.append(Contacts.AGGREGATE_ID);
2514        filter.append(" FROM ");
2515        filter.append(Tables.CONTACTS);
2516        filter.append(" WHERE ");
2517        filter.append(Contacts._ID);
2518        filter.append(" IN (SELECT  contact_id FROM name_lookup WHERE normalized_name GLOB '");
2519        // NOTE: Query parameters won't work here since the SQL compiler
2520        // needs to parse the actual string to know that it can use the
2521        // index to do a prefix scan.
2522        filter.append(NameNormalizer.normalize(filterParam) + "*");
2523        filter.append("'))");
2524        return filter.toString();
2525    }
2526
2527    private String[] appendGroupArg(String[] selectionArgs, String arg) {
2528        if (selectionArgs == null) {
2529            return new String[] {arg};
2530        } else {
2531            int newLength = selectionArgs.length + 1;
2532            String[] newSelectionArgs = new String[newLength];
2533            System.arraycopy(selectionArgs, 0, newSelectionArgs, 0, selectionArgs.length);
2534            newSelectionArgs[newLength - 1] = arg;
2535            return newSelectionArgs;
2536        }
2537    }
2538}
2539