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