ContactsProvider2.java revision 67dde51ab932dc84d95a203b113989b13437f13d
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_MIMETYPES_CONTACTS_AGGREGATES;
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
174    private interface DataAggregatesQuery {
175        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES;
176
177        public static final String[] PROJECTION = new String[] {
178            ContactsColumns.CONCRETE_ID,
179            DataColumns.CONCRETE_ID,
180            AggregatesColumns.CONCRETE_ID,
181            MimetypesColumns.CONCRETE_ID,
182            Phone.NUMBER,
183            Email.DATA,
184            AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID,
185            AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID,
186            AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID,
187            AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID,
188        };
189
190        public static final int CONTACT_ID = 0;
191        public static final int DATA_ID = 1;
192        public static final int AGGREGATE_ID = 2;
193        public static final int MIMETYPE_ID = 3;
194        public static final int PHONE_NUMBER = 4;
195        public static final int EMAIL_DATA = 5;
196        public static final int OPTIMAL_PHONE_ID = 6;
197        public static final int FALLBACK_PHONE_ID = 7;
198        public static final int OPTIMAL_EMAIL_ID = 8;
199        public static final int FALLBACK_EMAIL_ID = 9;
200
201    }
202
203    private interface DisplayNameQuery {
204        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
205
206        public static final String[] COLUMNS = new String[] {
207            MimetypesColumns.MIMETYPE,
208            Data.IS_PRIMARY,
209            Data.DATA2,
210            StructuredName.DISPLAY_NAME,
211        };
212
213        public static final int MIMETYPE = 0;
214        public static final int IS_PRIMARY = 1;
215        public static final int DATA2 = 2;
216        public static final int DISPLAY_NAME = 3;
217    }
218
219    private interface DataQuery {
220        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
221
222        public static final String[] COLUMNS = new String[] {
223            DataColumns.CONCRETE_ID,
224            MimetypesColumns.MIMETYPE,
225            Data.CONTACT_ID,
226            Data.IS_PRIMARY,
227            Data.DATA1,
228            Data.DATA2,
229            Data.DATA3,
230            Data.DATA4,
231            Data.DATA5,
232            Data.DATA6,
233            Data.DATA7,
234            Data.DATA8,
235            Data.DATA9,
236            Data.DATA10,
237            Data.DATA11,
238            Data.DATA12,
239            Data.DATA13,
240            Data.DATA14,
241            Data.DATA15,
242        };
243
244        public static final int ID = 0;
245        public static final int MIMETYPE = 1;
246        public static final int CONTACT_ID = 2;
247        public static final int IS_PRIMARY = 3;
248        public static final int DATA1 = 4;
249        public static final int DATA2 = 5;
250        public static final int DATA3 = 6;
251        public static final int DATA4 = 7;
252        public static final int DATA5 = 8;
253        public static final int DATA6 = 9;
254        public static final int DATA7 = 10;
255        public static final int DATA8 = 11;
256        public static final int DATA9 = 12;
257        public static final int DATA10 = 13;
258        public static final int DATA11 = 14;
259        public static final int DATA12 = 15;
260        public static final int DATA13 = 16;
261        public static final int DATA14 = 17;
262        public static final int DATA15 = 18;
263    }
264
265    // Higher number represents higher priority in choosing what data to use for the display name
266    private static final int DISPLAY_NAME_PRIORITY_EMAIL = 1;
267    private static final int DISPLAY_NAME_PRIORITY_PHONE = 2;
268    private static final int DISPLAY_NAME_PRIORITY_ORGANIZATION = 3;
269    private static final int DISPLAY_NAME_PRIORITY_STRUCTURED_NAME = 4;
270
271    private static final HashMap<String, Integer> sDisplayNamePriorities;
272    static {
273        sDisplayNamePriorities = new HashMap<String, Integer>();
274        sDisplayNamePriorities.put(StructuredName.CONTENT_ITEM_TYPE,
275                DISPLAY_NAME_PRIORITY_STRUCTURED_NAME);
276        sDisplayNamePriorities.put(Organization.CONTENT_ITEM_TYPE,
277                DISPLAY_NAME_PRIORITY_ORGANIZATION);
278        sDisplayNamePriorities.put(Phone.CONTENT_ITEM_TYPE,
279                DISPLAY_NAME_PRIORITY_PHONE);
280        sDisplayNamePriorities.put(Email.CONTENT_ITEM_TYPE,
281                DISPLAY_NAME_PRIORITY_EMAIL);
282    }
283
284    /** Contains just the contacts columns */
285    private static final HashMap<String, String> sAggregatesProjectionMap;
286    /** Contains the aggregate columns along with primary phone */
287    private static final HashMap<String, String> sAggregatesSummaryProjectionMap;
288    /** Contains the data, contacts, and aggregate columns, for joined tables. */
289    private static final HashMap<String, String> sDataContactsAggregateProjectionMap;
290    /** Contains the data, contacts, group sourceid and aggregate columns, for joined tables. */
291    private static final HashMap<String, String> sDataContactsGroupsAggregateProjectionMap;
292    /** Contains just the contacts columns */
293    private static final HashMap<String, String> sContactsProjectionMap;
294    /** Contains just the data columns */
295    private static final HashMap<String, String> sDataGroupsProjectionMap;
296    /** Contains the data and contacts columns, for joined tables */
297    private static final HashMap<String, String> sDataContactsGroupsProjectionMap;
298    /** Contains the data and contacts columns, for joined tables */
299    private static final HashMap<String, String> sDataContactsProjectionMap;
300    /** Contains the just the {@link Groups} columns */
301    private static final HashMap<String, String> sGroupsProjectionMap;
302    /** Contains {@link Groups} columns along with summary details */
303    private static final HashMap<String, String> sGroupsSummaryProjectionMap;
304    /** Contains the just the agg_exceptions columns */
305    private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
306
307    /** Sql select statement that returns the contact id associated with a data record. */
308    private static final String sNestedContactIdSelect;
309    /** Sql select statement that returns the mimetype id associated with a data record. */
310    private static final String sNestedMimetypeSelect;
311    /** Sql select statement that returns the aggregate id associated with a contact record. */
312    private static final String sNestedAggregateIdSelect;
313    /** Sql select statement that returns a list of contact ids associated with an aggregate record. */
314    private static final String sNestedContactIdListSelect;
315    /** Sql where statement used to match all the data records that need to be updated when a new
316     * "primary" is selected.*/
317    private static final String sSetPrimaryWhere;
318    /** Sql where statement used to match all the data records that need to be updated when a new
319     * "super primary" is selected.*/
320    private static final String sSetSuperPrimaryWhere;
321    /** Sql where statement for filtering on groups. */
322    private static final String sAggregatesInGroupSelect;
323    /** Precompiled sql statement for setting a data record to the primary. */
324    private SQLiteStatement mSetPrimaryStatement;
325    /** Precompiled sql statement for setting a data record to the super primary. */
326    private SQLiteStatement mSetSuperPrimaryStatement;
327    /** Precompiled sql statement for incrementing times contacted for an aggregate */
328    private SQLiteStatement mLastTimeContactedUpdate;
329    /** Precompiled sql statement for updating a contact display name */
330    private SQLiteStatement mContactDisplayNameUpdate;
331
332    private static final String GTALK_PROTOCOL_STRING = ContactMethods
333            .encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
334
335    static {
336        // Contacts URI matching table
337        final UriMatcher matcher = sUriMatcher;
338        matcher.addURI(ContactsContract.AUTHORITY, "aggregates", AGGREGATES);
339        matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#", AGGREGATES_ID);
340        matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#/data", AGGREGATES_DATA);
341        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary", AGGREGATES_SUMMARY);
342        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/#", AGGREGATES_SUMMARY_ID);
343        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/filter/*",
344                AGGREGATES_SUMMARY_FILTER);
345        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/strequent/",
346                AGGREGATES_SUMMARY_STREQUENT);
347        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/strequent/filter/*",
348                AGGREGATES_SUMMARY_STREQUENT_FILTER);
349        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/group/*",
350                AGGREGATES_SUMMARY_GROUP);
351        matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#/suggestions",
352                AGGREGATION_SUGGESTIONS);
353        matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
354        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
355        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
356        matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter_email/*",
357                CONTACTS_FILTER_EMAIL);
358
359        matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
360        matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
361        matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
362        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
363        matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
364
365        matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
366        matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
367        matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
368
369        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
370
371        matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
372        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
373                AGGREGATION_EXCEPTIONS);
374        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
375                AGGREGATION_EXCEPTION_ID);
376
377        matcher.addURI(ContactsContract.AUTHORITY, "presence", PRESENCE);
378        matcher.addURI(ContactsContract.AUTHORITY, "presence/#", PRESENCE_ID);
379
380        HashMap<String, String> columns;
381
382        // Aggregates projection map
383        columns = new HashMap<String, String>();
384        columns.put(Aggregates._ID, "aggregates._id AS _id");
385        columns.put(Aggregates.DISPLAY_NAME, AggregatesColumns.CONCRETE_DISPLAY_NAME + " AS "
386                + Aggregates.DISPLAY_NAME);
387        columns.put(Aggregates.LAST_TIME_CONTACTED, AggregatesColumns.CONCRETE_LAST_TIME_CONTACTED
388                + " AS " + Aggregates.LAST_TIME_CONTACTED);
389        columns.put(Aggregates.TIMES_CONTACTED, AggregatesColumns.CONCRETE_TIMES_CONTACTED + " AS "
390                + Aggregates.TIMES_CONTACTED);
391        columns.put(Aggregates.STARRED, AggregatesColumns.CONCRETE_STARRED + " AS "
392                + Aggregates.STARRED);
393        columns.put(Aggregates.IN_VISIBLE_GROUP, Aggregates.IN_VISIBLE_GROUP);
394        columns.put(Aggregates.PHOTO_ID, Aggregates.PHOTO_ID);
395        columns.put(Aggregates.PRIMARY_PHONE_ID, Aggregates.PRIMARY_PHONE_ID);
396        columns.put(Aggregates.PRIMARY_EMAIL_ID, Aggregates.PRIMARY_EMAIL_ID);
397        columns.put(Aggregates.CUSTOM_RINGTONE, AggregatesColumns.CONCRETE_CUSTOM_RINGTONE + " AS "
398                + Aggregates.CUSTOM_RINGTONE);
399        columns.put(Aggregates.SEND_TO_VOICEMAIL, AggregatesColumns.CONCRETE_SEND_TO_VOICEMAIL
400                + " AS " + Aggregates.SEND_TO_VOICEMAIL);
401        columns.put(AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID,
402                AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID);
403        columns.put(AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID,
404                AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID);
405        sAggregatesProjectionMap = columns;
406
407        // Aggregates primaries projection map. The overall presence status is
408        // the most-present value, as indicated by the largest value.
409        columns = new HashMap<String, String>();
410        columns.putAll(sAggregatesProjectionMap);
411        columns.put(CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE);
412        columns.put(CommonDataKinds.Phone.LABEL, CommonDataKinds.Phone.LABEL);
413        columns.put(CommonDataKinds.Phone.NUMBER, CommonDataKinds.Phone.NUMBER);
414        columns.put(Presence.PRESENCE_STATUS, "MAX(" + Presence.PRESENCE_STATUS + ")");
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        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
912
913        mContactAggregator = new ContactAggregator(context, mOpenHelper, mAggregationScheduler);
914
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        mLegacyApiSupport = new LegacyApiSupport(context, mOpenHelper, this);
935
936        mDataRowHandlers = new HashMap<String, DataRowHandler>();
937
938        mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
939        mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
940                new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
941        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
942                StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
943        mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
944        mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
945        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
946                Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL));
947        mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
948                new StructuredNameRowHandler(mNameSplitter));
949
950        return (db != null);
951    }
952
953    /* Visible for testing */
954    protected OpenHelper getOpenHelper(final Context context) {
955        return OpenHelper.getInstance(context);
956    }
957
958    @Override
959    protected void finalize() throws Throwable {
960        if (mContactAggregator != null) {
961            mContactAggregator.quit();
962        }
963
964        super.finalize();
965    }
966
967    /**
968     * Wipes all data from the contacts database.
969     */
970    /* package */ void wipeData() {
971        mOpenHelper.wipeData();
972    }
973
974    /**
975     * Called when a change has been made.
976     *
977     * @param uri the uri that the change was made to
978     */
979    private void onChange(Uri uri) {
980        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
981    }
982
983    @Override
984    public boolean isTemporary() {
985        return false;
986    }
987
988    private DataRowHandler getDataRowHandler(final String mimeType) {
989        DataRowHandler handler = mDataRowHandlers.get(mimeType);
990        if (handler == null) {
991            handler = new CustomDataRowHandler(mimeType);
992            mDataRowHandlers.put(mimeType, handler);
993        }
994        return handler;
995    }
996
997    @Override
998    public Uri insert(Uri uri, ContentValues values) {
999        final int match = sUriMatcher.match(uri);
1000        long id = 0;
1001
1002        switch (match) {
1003            case SYNCSTATE:
1004                id = mOpenHelper.getSyncState().insert(mOpenHelper.getWritableDatabase(), values);
1005                break;
1006
1007            case AGGREGATES: {
1008                insertAggregate(values);
1009                break;
1010            }
1011
1012            case CONTACTS: {
1013                final Account account = readAccountFromQueryParams(uri);
1014                id = insertContact(values, account);
1015                break;
1016            }
1017
1018            case CONTACTS_DATA: {
1019                values.put(Data.CONTACT_ID, uri.getPathSegments().get(1));
1020                id = insertData(values);
1021                break;
1022            }
1023
1024            case DATA: {
1025                id = insertData(values);
1026                break;
1027            }
1028
1029            case GROUPS: {
1030                final Account account = readAccountFromQueryParams(uri);
1031                id = insertGroup(values, account);
1032                break;
1033            }
1034
1035            case PRESENCE: {
1036                id = insertPresence(values);
1037                break;
1038            }
1039
1040            default:
1041                return mLegacyApiSupport.insert(uri, values);
1042        }
1043
1044        if (id < 0) {
1045            return null;
1046        }
1047
1048        final Uri result = ContentUris.withAppendedId(uri, id);
1049        onChange(result);
1050        return result;
1051    }
1052
1053    /**
1054     * If account is non-null then store it in the values. If the account is already
1055     * specified in the values then it must be consistent with the account, if it is non-null.
1056     * @param values the ContentValues to read from and update
1057     * @param account the explicitly provided Account
1058     * @return false if the accounts are inconsistent
1059     */
1060    private boolean resolveAccount(ContentValues values, Account account) {
1061        // If either is specified then both must be specified.
1062        final String accountName = values.getAsString(Contacts.ACCOUNT_NAME);
1063        final String accountType = values.getAsString(Contacts.ACCOUNT_TYPE);
1064        if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
1065            final Account valuesAccount = new Account(accountName, accountType);
1066            if (account != null && !valuesAccount.equals(account)) {
1067                return false;
1068            }
1069            account = valuesAccount;
1070        }
1071        if (account != null) {
1072            values.put(Contacts.ACCOUNT_NAME, account.mName);
1073            values.put(Contacts.ACCOUNT_TYPE, account.mType);
1074        }
1075        return true;
1076    }
1077
1078    /**
1079     * Inserts an item in the aggregates table
1080     *
1081     * @param values the values for the new row
1082     * @return the row ID of the newly created row
1083     */
1084    private long insertAggregate(ContentValues values) {
1085        throw new UnsupportedOperationException("Aggregates are created automatically");
1086    }
1087
1088    /**
1089     * Inserts an item in the contacts table
1090     *
1091     * @param values the values for the new row
1092     * @param account the account this contact should be associated with. may be null.
1093     * @return the row ID of the newly created row
1094     */
1095    private long insertContact(ContentValues values, Account account) {
1096        /*
1097         * The contact record is inserted in the contacts table, but it needs to
1098         * be processed by the aggregator before it will be returned by the
1099         * "aggregates" queries.
1100         */
1101        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1102
1103        ContentValues overriddenValues = new ContentValues(values);
1104        overriddenValues.putNull(Contacts.AGGREGATE_ID);
1105        if (!resolveAccount(overriddenValues, account)) {
1106            return -1;
1107        }
1108
1109        long contactId = db.insert(Tables.CONTACTS, Contacts.AGGREGATE_ID, overriddenValues);
1110
1111        int aggregationMode = Contacts.AGGREGATION_MODE_DEFAULT;
1112        if (values.containsKey(Contacts.AGGREGATION_MODE)) {
1113            aggregationMode = values.getAsInteger(Contacts.AGGREGATION_MODE);
1114        }
1115
1116        triggerAggregation(contactId, aggregationMode);
1117
1118        return contactId;
1119    }
1120
1121    /**
1122     * Inserts an item in the data table
1123     *
1124     * @param values the values for the new row
1125     * @return the row ID of the newly created row
1126     */
1127    private long insertData(ContentValues values) {
1128        int aggregationMode = Contacts.AGGREGATION_MODE_DISABLED;
1129
1130        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1131        long id = 0;
1132        db.beginTransaction();
1133        try {
1134            long contactId = values.getAsLong(Data.CONTACT_ID);
1135
1136            // Replace package with internal mapping
1137            final String packageName = values.getAsString(Data.RES_PACKAGE);
1138            if (packageName != null) {
1139                values.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1140            }
1141            values.remove(Data.RES_PACKAGE);
1142
1143            // Replace mimetype with internal mapping
1144            final String mimeType = values.getAsString(Data.MIMETYPE);
1145            values.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
1146            values.remove(Data.MIMETYPE);
1147
1148            if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
1149                boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
1150                boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
1151                if (containsGroupSourceId && containsGroupId) {
1152                    throw new IllegalArgumentException(
1153                            "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
1154                                    + "and GroupMembership.GROUP_ROW_ID");
1155                }
1156
1157                if (!containsGroupSourceId && !containsGroupId) {
1158                    throw new IllegalArgumentException(
1159                            "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
1160                                    + "and GroupMembership.GROUP_ROW_ID");
1161                }
1162
1163                if (containsGroupSourceId) {
1164                    final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
1165                    final long groupId = getOrMakeGroup(db, contactId, sourceId);
1166                    values.remove(GroupMembership.GROUP_SOURCE_ID);
1167                    values.put(GroupMembership.GROUP_ROW_ID, groupId);
1168                }
1169            }
1170
1171            id = getDataRowHandler(mimeType).insert(db, contactId, values);
1172
1173            aggregationMode = mContactAggregator.markContactForAggregation(contactId);
1174
1175            db.setTransactionSuccessful();
1176        } finally {
1177            db.endTransaction();
1178        }
1179
1180        triggerAggregation(id, aggregationMode);
1181        return id;
1182    }
1183
1184    private void triggerAggregation(long contactId, int aggregationMode) {
1185        switch (aggregationMode) {
1186            case Contacts.AGGREGATION_MODE_DEFAULT:
1187                mContactAggregator.schedule();
1188                break;
1189
1190            case Contacts.AGGREGATION_MODE_IMMEDITATE:
1191                mContactAggregator.aggregateContact(contactId);
1192                break;
1193
1194            case Contacts.AGGREGATION_MODE_DISABLED:
1195                // Do nothing
1196                break;
1197        }
1198    }
1199
1200    public int deleteData(long dataId, String[] allowedMimeTypes) {
1201        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1202        Cursor c = db.query(DataQuery.TABLE, DataQuery.COLUMNS,
1203                DataColumns.CONCRETE_ID + "=" + dataId, null, null, null, null);
1204        // TODO apply restrictions
1205        try {
1206            if (!c.moveToFirst()) {
1207                return 0;
1208            }
1209
1210            String mimeType = c.getString(DataQuery.MIMETYPE);
1211            boolean valid = false;
1212            for (int i = 0; i < allowedMimeTypes.length; i++) {
1213                if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
1214                    valid = true;
1215                    break;
1216                }
1217            }
1218
1219            if (!valid) {
1220                throw new RuntimeException("Data type mismatch: expected "
1221                        + Lists.newArrayList(allowedMimeTypes));
1222            }
1223
1224            return getDataRowHandler(mimeType).delete(db, c);
1225        } finally {
1226            c.close();
1227        }
1228    }
1229
1230    /**
1231     * Returns the group id of the group with sourceId and the same account as contactId.
1232     * If the group doesn't already exist then it is first created,
1233     * @param db SQLiteDatabase to use for this operation
1234     * @param contactId the contact this group is associated with
1235     * @param sourceId the sourceIf of the group to query or create
1236     * @return the group id of the existing or created group
1237     * @throws IllegalArgumentException if the contact is not associated with an account
1238     * @throws IllegalStateException if a group needs to be created but the creation failed
1239     */
1240    private long getOrMakeGroup(SQLiteDatabase db, long contactId, String sourceId) {
1241        Account account = null;
1242        Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, Contacts._ID + "="
1243                + contactId, null, null, null, null);
1244        try {
1245            if (c.moveToNext()) {
1246                final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
1247                final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
1248                if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
1249                    account = new Account(accountName, accountType);
1250                }
1251            }
1252        } finally {
1253            c.close();
1254        }
1255        if (account == null) {
1256            throw new IllegalArgumentException("if the groupmembership only "
1257                    + "has a sourceid the the contact must be associate with "
1258                    + "an account");
1259        }
1260
1261        // look up the group that contains this sourceId and has the same account name and type
1262        // as the contact refered to by contactId
1263        c = db.query(Tables.GROUPS, new String[]{Contacts._ID},
1264                Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
1265                new String[]{sourceId, account.mName, account.mType}, null, null, null);
1266        try {
1267            if (c.moveToNext()) {
1268                return c.getLong(0);
1269            } else {
1270                ContentValues groupValues = new ContentValues();
1271                groupValues.put(Groups.ACCOUNT_NAME, account.mName);
1272                groupValues.put(Groups.ACCOUNT_TYPE, account.mType);
1273                groupValues.put(Groups.SOURCE_ID, sourceId);
1274                long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
1275                if (groupId < 0) {
1276                    throw new IllegalStateException("unable to create a new group with "
1277                            + "this sourceid: " + groupValues);
1278                }
1279                return groupId;
1280            }
1281        } finally {
1282            c.close();
1283        }
1284    }
1285
1286    /**
1287     * Delete the given {@link Data} row, fixing up any {@link Aggregates}
1288     * primaries that reference it.
1289     */
1290    private int deleteData(long dataId) {
1291        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1292
1293        final long mimePhone = mOpenHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
1294        final long mimeEmail = mOpenHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
1295
1296        // Check to see if the data about to be deleted was a super-primary on
1297        // the parent aggregate, and set flags to fix-up once deleted.
1298        long aggId = -1;
1299        long mimeId = -1;
1300        String dataRaw = null;
1301        boolean fixOptimal = false;
1302        boolean fixFallback = false;
1303
1304        Cursor cursor = null;
1305        try {
1306            cursor = db.query(DataAggregatesQuery.TABLE, DataAggregatesQuery.PROJECTION,
1307                    DataColumns.CONCRETE_ID + "=" + dataId, null, null, null, null);
1308            if (cursor.moveToFirst()) {
1309                aggId = cursor.getLong(DataAggregatesQuery.AGGREGATE_ID);
1310                mimeId = cursor.getLong(DataAggregatesQuery.MIMETYPE_ID);
1311                if (mimeId == mimePhone) {
1312                    dataRaw = cursor.getString(DataAggregatesQuery.PHONE_NUMBER);
1313                    fixOptimal = (cursor.getLong(DataAggregatesQuery.OPTIMAL_PHONE_ID) == dataId);
1314                    fixFallback = (cursor.getLong(DataAggregatesQuery.FALLBACK_PHONE_ID) == dataId);
1315                } else if (mimeId == mimeEmail) {
1316                    dataRaw = cursor.getString(DataAggregatesQuery.EMAIL_DATA);
1317                    fixOptimal = (cursor.getLong(DataAggregatesQuery.OPTIMAL_EMAIL_ID) == dataId);
1318                    fixFallback = (cursor.getLong(DataAggregatesQuery.FALLBACK_EMAIL_ID) == dataId);
1319                }
1320            }
1321        } finally {
1322            if (cursor != null) {
1323                cursor.close();
1324                cursor = null;
1325            }
1326        }
1327
1328        // Delete the requested data item.
1329        int dataDeleted = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
1330
1331        // Fix-up any super-primary values that are now invalid.
1332        if (fixOptimal || fixFallback) {
1333            final ContentValues values = new ContentValues();
1334            final StringBuilder scoreClause = new StringBuilder();
1335
1336            final String SCORE = "score";
1337
1338            // Build scoring clause that will first pick data items under the
1339            // same aggregate that have identical values, otherwise fall back to
1340            // normal primary scoring from the member contacts.
1341            scoreClause.append("(CASE WHEN ");
1342            if (mimeId == mimePhone) {
1343                scoreClause.append(Phone.NUMBER);
1344            } else if (mimeId == mimeEmail) {
1345                scoreClause.append(Email.DATA);
1346            }
1347            scoreClause.append("=");
1348            DatabaseUtils.appendEscapedSQLString(scoreClause, dataRaw);
1349            scoreClause.append(" THEN 2 ELSE " + Data.IS_PRIMARY + " END) AS " + SCORE);
1350
1351            final String[] PROJ_PRIMARY = new String[] {
1352                    DataColumns.CONCRETE_ID,
1353                    Contacts.IS_RESTRICTED,
1354                    scoreClause.toString(),
1355            };
1356
1357            final int COL_DATA_ID = 0;
1358            final int COL_IS_RESTRICTED = 1;
1359            final int COL_SCORE = 2;
1360
1361            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES, PROJ_PRIMARY,
1362                    AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND " + DataColumns.MIMETYPE_ID
1363                            + "=" + mimeId, null, null, null, SCORE);
1364
1365            if (fixOptimal) {
1366                String colId = null;
1367                String colIsRestricted = null;
1368                if (mimeId == mimePhone) {
1369                    colId = AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID;
1370                    colIsRestricted = AggregatesColumns.OPTIMAL_PRIMARY_PHONE_IS_RESTRICTED;
1371                } else if (mimeId == mimeEmail) {
1372                    colId = AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID;
1373                    colIsRestricted = AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_IS_RESTRICTED;
1374                }
1375
1376                // Start by replacing with null, since fixOptimal told us that
1377                // the previous aggregate values are bad.
1378                values.putNull(colId);
1379                values.putNull(colIsRestricted);
1380
1381                // When finding a new optimal primary, we only care about the
1382                // highest scoring value, regardless of source.
1383                if (cursor.moveToFirst()) {
1384                    final long newOptimal = cursor.getLong(COL_DATA_ID);
1385                    final long newIsRestricted = cursor.getLong(COL_IS_RESTRICTED);
1386
1387                    if (newOptimal != 0) {
1388                        values.put(colId, newOptimal);
1389                    }
1390                    if (newIsRestricted != 0) {
1391                        values.put(colIsRestricted, newIsRestricted);
1392                    }
1393                }
1394            }
1395
1396            if (fixFallback) {
1397                String colId = null;
1398                if (mimeId == mimePhone) {
1399                    colId = AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID;
1400                } else if (mimeId == mimeEmail) {
1401                    colId = AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID;
1402                }
1403
1404                // Start by replacing with null, since fixFallback told us that
1405                // the previous aggregate values are bad.
1406                values.putNull(colId);
1407
1408                // The best fallback value is the highest scoring data item that
1409                // hasn't been restricted.
1410                cursor.moveToPosition(-1);
1411                while (cursor.moveToNext()) {
1412                    final boolean isRestricted = (cursor.getInt(COL_IS_RESTRICTED) == 1);
1413                    if (!isRestricted) {
1414                        values.put(colId, cursor.getLong(COL_DATA_ID));
1415                        break;
1416                    }
1417                }
1418            }
1419
1420            // Push through any aggregate updates we have
1421            if (values.size() > 0) {
1422                db.update(Tables.AGGREGATES, values, AggregatesColumns.CONCRETE_ID + "=" + aggId,
1423                        null);
1424            }
1425        }
1426
1427        return dataDeleted;
1428    }
1429
1430    /**
1431     * Inserts an item in the groups table
1432     */
1433    private long insertGroup(ContentValues values, Account account) {
1434        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1435
1436        ContentValues overriddenValues = new ContentValues(values);
1437        if (!resolveAccount(overriddenValues, account)) {
1438            return -1;
1439        }
1440
1441        // Replace package with internal mapping
1442        final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
1443        if (packageName != null) {
1444            overriddenValues.put(GroupsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
1445        }
1446        overriddenValues.remove(Groups.RES_PACKAGE);
1447
1448        return db.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
1449    }
1450
1451    /**
1452     * Inserts a presence update.
1453     */
1454    private long insertPresence(ContentValues values) {
1455        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1456        final String handle = values.getAsString(Presence.IM_HANDLE);
1457        final String protocol = values.getAsString(Presence.IM_PROTOCOL);
1458        if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
1459            throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required");
1460        }
1461
1462        // TODO: generalize to allow other providers to match against email
1463        boolean matchEmail = GTALK_PROTOCOL_STRING.equals(protocol);
1464
1465        String selection;
1466        String[] selectionArgs;
1467        if (matchEmail) {
1468            selection = "(" + Clauses.WHERE_IM_MATCHES + ") OR (" + Clauses.WHERE_EMAIL_MATCHES + ")";
1469            selectionArgs = new String[] { protocol, handle, handle };
1470        } else {
1471            selection = Clauses.WHERE_IM_MATCHES;
1472            selectionArgs = new String[] { protocol, handle };
1473        }
1474
1475        long dataId = -1;
1476        long aggId = -1;
1477        Cursor cursor = null;
1478        try {
1479            cursor = db.query(DataContactsQuery.TABLE,
1480                    DataContactsQuery.PROJECTION, selection, selectionArgs, null, null, null);
1481            if (cursor.moveToFirst()) {
1482                dataId = cursor.getLong(DataContactsQuery.DATA_ID);
1483                aggId = cursor.getLong(DataContactsQuery.AGGREGATE_ID);
1484            } else {
1485                // No contact found, return a null URI
1486                return -1;
1487            }
1488        } finally {
1489            if (cursor != null) {
1490                cursor.close();
1491            }
1492        }
1493
1494        values.put(Presence.DATA_ID, dataId);
1495        values.put(Presence.AGGREGATE_ID, aggId);
1496
1497        // Insert the presence update
1498        long presenceId = db.replace(Tables.PRESENCE, null, values);
1499        return presenceId;
1500    }
1501
1502    @Override
1503    public int delete(Uri uri, String selection, String[] selectionArgs) {
1504        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1505        final int match = sUriMatcher.match(uri);
1506        switch (match) {
1507            case SYNCSTATE:
1508                return mOpenHelper.getSyncState().delete(db, selection, selectionArgs);
1509
1510            case AGGREGATES_ID: {
1511                long aggregateId = ContentUris.parseId(uri);
1512
1513                // Remove references to the aggregate first
1514                ContentValues values = new ContentValues();
1515                values.putNull(Contacts.AGGREGATE_ID);
1516                db.update(Tables.CONTACTS, values, Contacts.AGGREGATE_ID + "=" + aggregateId, null);
1517
1518                return db.delete(Tables.AGGREGATES, BaseColumns._ID + "=" + aggregateId, null);
1519            }
1520
1521            case CONTACTS_ID: {
1522                long contactId = ContentUris.parseId(uri);
1523                int contactsDeleted = db.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
1524                int dataDeleted = db.delete(Tables.DATA, Data.CONTACT_ID + "=" + contactId, null);
1525                return contactsDeleted + dataDeleted;
1526            }
1527
1528            case DATA_ID: {
1529                long dataId = ContentUris.parseId(uri);
1530                return deleteData(dataId);
1531            }
1532
1533            case GROUPS_ID: {
1534                long groupId = ContentUris.parseId(uri);
1535                final long groupMembershipMimetypeId = mOpenHelper
1536                        .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
1537                int groupsDeleted = db.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
1538                int dataDeleted = db.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
1539                        + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
1540                        + groupId, null);
1541                mOpenHelper.updateAllVisible();
1542                return groupsDeleted + dataDeleted;
1543            }
1544
1545            case PRESENCE: {
1546                return db.delete(Tables.PRESENCE, null, null);
1547            }
1548
1549            default:
1550                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
1551        }
1552    }
1553
1554    private static Account readAccountFromQueryParams(Uri uri) {
1555        final String name = uri.getQueryParameter(Contacts.ACCOUNT_NAME);
1556        final String type = uri.getQueryParameter(Contacts.ACCOUNT_TYPE);
1557        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
1558            return null;
1559        }
1560        return new Account(name, type);
1561    }
1562
1563
1564    @Override
1565    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1566        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1567        int count = 0;
1568
1569        final int match = sUriMatcher.match(uri);
1570        switch(match) {
1571            case SYNCSTATE:
1572                return mOpenHelper.getSyncState().update(db, values, selection, selectionArgs);
1573
1574            // TODO(emillar): We will want to disallow editing the aggregates table at some point.
1575            case AGGREGATES: {
1576                count = db.update(Tables.AGGREGATES, values, selection, selectionArgs);
1577                break;
1578            }
1579
1580            case AGGREGATES_ID: {
1581                count = updateAggregateData(db, ContentUris.parseId(uri), values);
1582                break;
1583            }
1584
1585            case DATA_ID: {
1586                boolean containsIsSuperPrimary = values.containsKey(Data.IS_SUPER_PRIMARY);
1587                boolean containsIsPrimary = values.containsKey(Data.IS_PRIMARY);
1588                final long id = ContentUris.parseId(uri);
1589
1590                // Remove primary or super primary values being set to 0. This is disallowed by the
1591                // content provider.
1592                if (containsIsSuperPrimary && values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
1593                    containsIsSuperPrimary = false;
1594                    values.remove(Data.IS_SUPER_PRIMARY);
1595                }
1596                if (containsIsPrimary && values.getAsInteger(Data.IS_PRIMARY) == 0) {
1597                    containsIsPrimary = false;
1598                    values.remove(Data.IS_PRIMARY);
1599                }
1600
1601                if (containsIsSuperPrimary) {
1602                    setIsSuperPrimary(id);
1603                    setIsPrimary(id);
1604
1605                    // Now that we've taken care of setting these, remove them from "values".
1606                    values.remove(Data.IS_SUPER_PRIMARY);
1607                    if (containsIsPrimary) {
1608                        values.remove(Data.IS_PRIMARY);
1609                    }
1610                } else if (containsIsPrimary) {
1611                    setIsPrimary(id);
1612
1613                    // Now that we've taken care of setting this, remove it from "values".
1614                    values.remove(Data.IS_PRIMARY);
1615                }
1616
1617                if (values.size() > 0) {
1618                    String selectionWithId = (Data._ID + " = " + ContentUris.parseId(uri) + " ")
1619                            + (selection == null ? "" : " AND " + selection);
1620                    count = db.update(Tables.DATA, values, selectionWithId, selectionArgs);
1621                }
1622                break;
1623            }
1624
1625            case CONTACTS: {
1626                count = db.update(Tables.CONTACTS, values, selection, selectionArgs);
1627                break;
1628            }
1629
1630            case CONTACTS_ID: {
1631                String selectionWithId = (Contacts._ID + " = " + ContentUris.parseId(uri) + " ")
1632                        + (selection == null ? "" : " AND " + selection);
1633                count = db.update(Tables.CONTACTS, values, selectionWithId, selectionArgs);
1634                Log.i(TAG, "Selection is: " + selectionWithId);
1635                break;
1636            }
1637
1638            case DATA: {
1639                count = db.update(Tables.DATA, values, selection, selectionArgs);
1640                break;
1641            }
1642
1643            case GROUPS: {
1644                count = db.update(Tables.GROUPS, values, selection, selectionArgs);
1645                mOpenHelper.updateAllVisible();
1646                break;
1647            }
1648
1649            case GROUPS_ID: {
1650                long groupId = ContentUris.parseId(uri);
1651                String selectionWithId = (Groups._ID + "=" + groupId + " ")
1652                        + (selection == null ? "" : " AND " + selection);
1653                count = db.update(Tables.GROUPS, values, selectionWithId, selectionArgs);
1654
1655                // If changing visibility, then update aggregates
1656                if (values.containsKey(Groups.GROUP_VISIBLE)) {
1657                    mOpenHelper.updateAllVisible();
1658                }
1659
1660                break;
1661            }
1662
1663            case AGGREGATION_EXCEPTIONS: {
1664                count = updateAggregationException(db, values);
1665                break;
1666            }
1667
1668            default:
1669                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
1670        }
1671
1672        if (count > 0) {
1673            getContext().getContentResolver().notifyChange(uri, null);
1674        }
1675        return count;
1676    }
1677
1678    private int updateAggregateData(SQLiteDatabase db, long aggregateId, ContentValues values) {
1679
1680        // First update all constituent contacts
1681        ContentValues optionValues = new ContentValues(5);
1682        OpenHelper.copyStringValue(optionValues, Contacts.CUSTOM_RINGTONE,
1683                values, Aggregates.CUSTOM_RINGTONE);
1684        OpenHelper.copyLongValue(optionValues, Contacts.SEND_TO_VOICEMAIL,
1685                values, Aggregates.SEND_TO_VOICEMAIL);
1686        OpenHelper.copyLongValue(optionValues, Contacts.LAST_TIME_CONTACTED,
1687                values, Aggregates.LAST_TIME_CONTACTED);
1688        OpenHelper.copyLongValue(optionValues, Contacts.TIMES_CONTACTED,
1689                values, Aggregates.TIMES_CONTACTED);
1690        OpenHelper.copyLongValue(optionValues, Contacts.STARRED,
1691                values, Aggregates.STARRED);
1692
1693        // Nothing to update - just return
1694        if (optionValues.size() == 0) {
1695            return 0;
1696        }
1697
1698        db.update(Tables.CONTACTS, optionValues, Contacts.AGGREGATE_ID + "=" + aggregateId, null);
1699        return db.update(Tables.AGGREGATES, values, Aggregates._ID + "=" + aggregateId, null);
1700    }
1701
1702    public void updateContactTime(long aggregateId, long lastTimeContacted) {
1703        mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
1704        mLastTimeContactedUpdate.bindLong(2, aggregateId);
1705        mLastTimeContactedUpdate.execute();
1706    }
1707
1708    private static class ContactPair {
1709        final long contactId1;
1710        final long contactId2;
1711
1712        /**
1713         * Constructor that ensures that this.contactId1 &lt; this.contactId2
1714         */
1715        public ContactPair(long contactId1, long contactId2) {
1716            if (contactId1 < contactId2) {
1717                this.contactId1 = contactId1;
1718                this.contactId2 = contactId2;
1719            } else {
1720                this.contactId2 = contactId1;
1721                this.contactId1 = contactId2;
1722            }
1723        }
1724    }
1725
1726    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
1727        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
1728        long aggregateId = values.getAsInteger(AggregationExceptions.AGGREGATE_ID);
1729        long contactId = values.getAsInteger(AggregationExceptions.CONTACT_ID);
1730
1731        // First, we build a list of contactID-contactID pairs for the given aggregate and contact.
1732        ArrayList<ContactPair> pairs = new ArrayList<ContactPair>();
1733        Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, Contacts.AGGREGATE_ID
1734                + "=" + aggregateId, null, null, null, null);
1735        try {
1736            while (c.moveToNext()) {
1737                long aggregatedContactId = c.getLong(ContactsQuery.CONTACT_ID);
1738                if (aggregatedContactId != contactId) {
1739                    pairs.add(new ContactPair(aggregatedContactId, contactId));
1740                }
1741            }
1742        } finally {
1743            c.close();
1744        }
1745
1746        // Now we iterate through all contact pairs to see if we need to insert/delete/update
1747        // the corresponding exception
1748        ContentValues exceptionValues = new ContentValues(3);
1749        exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
1750        for (ContactPair pair : pairs) {
1751            final String whereClause =
1752                    AggregationExceptionColumns.CONTACT_ID1 + "=" + pair.contactId1 + " AND "
1753                    + AggregationExceptionColumns.CONTACT_ID2 + "=" + pair.contactId2;
1754            if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
1755                db.delete(Tables.AGGREGATION_EXCEPTIONS, whereClause, null);
1756            } else {
1757                exceptionValues.put(AggregationExceptionColumns.CONTACT_ID1, pair.contactId1);
1758                exceptionValues.put(AggregationExceptionColumns.CONTACT_ID2, pair.contactId2);
1759                db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
1760                        exceptionValues);
1761            }
1762        }
1763
1764        int aggregationMode = mContactAggregator.markContactForAggregation(contactId);
1765        if (aggregationMode != Contacts.AGGREGATION_MODE_DISABLED) {
1766            mContactAggregator.aggregateContact(db, contactId);
1767            if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC
1768                    || exceptionType == AggregationExceptions.TYPE_KEEP_OUT) {
1769                mContactAggregator.updateAggregateData(aggregateId);
1770            }
1771        }
1772
1773        // The return value is fake - we just confirm that we made a change, not count actual
1774        // rows changed.
1775        return 1;
1776    }
1777
1778    /**
1779     * Test if a {@link String} value appears in the given list.
1780     */
1781    private boolean isContained(String[] array, String value) {
1782        if (array != null) {
1783            for (String test : array) {
1784                if (value.equals(test)) {
1785                    return true;
1786                }
1787            }
1788        }
1789        return false;
1790    }
1791
1792    /**
1793     * Test if a {@link String} value appears in the given list, and add to the
1794     * array if the value doesn't already appear.
1795     */
1796    private String[] assertContained(String[] array, String value) {
1797        if (array == null) {
1798            array = new String[] {value};
1799        } else if (!isContained(array, value)) {
1800            String[] newArray = new String[array.length + 1];
1801            System.arraycopy(array, 0, newArray, 0, array.length);
1802            newArray[array.length] = value;
1803            array = newArray;
1804        }
1805        return array;
1806    }
1807
1808    @Override
1809    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
1810            String sortOrder) {
1811        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1812
1813        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1814        String groupBy = null;
1815        String limit = null;
1816        String aggregateIdColName = Tables.AGGREGATES + "." + Aggregates._ID;
1817
1818        // TODO: Consider writing a test case for RestrictionExceptions when you
1819        // write a new query() block to make sure it protects restricted data.
1820        final int match = sUriMatcher.match(uri);
1821        switch (match) {
1822            case SYNCSTATE:
1823                return mOpenHelper.getSyncState().query(db, projection, selection,  selectionArgs,
1824                        sortOrder);
1825
1826            case AGGREGATES: {
1827                qb.setTables(Tables.AGGREGATES);
1828                applyAggregateRestrictionExceptions(qb);
1829                applyAggregatePrimaryRestrictionExceptions(sAggregatesProjectionMap);
1830                qb.setProjectionMap(sAggregatesProjectionMap);
1831                break;
1832            }
1833
1834            case AGGREGATES_ID: {
1835                long aggId = ContentUris.parseId(uri);
1836                qb.setTables(Tables.AGGREGATES);
1837                qb.appendWhere(AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND ");
1838                applyAggregateRestrictionExceptions(qb);
1839                applyAggregatePrimaryRestrictionExceptions(sAggregatesProjectionMap);
1840                qb.setProjectionMap(sAggregatesProjectionMap);
1841                break;
1842            }
1843
1844            case AGGREGATES_SUMMARY: {
1845                // TODO: join into social status tables
1846                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1847                applyAggregateRestrictionExceptions(qb);
1848                applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap);
1849                projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID);
1850                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1851                groupBy = aggregateIdColName;
1852                break;
1853            }
1854
1855            case AGGREGATES_SUMMARY_ID: {
1856                // TODO: join into social status tables
1857                long aggId = ContentUris.parseId(uri);
1858                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1859                qb.appendWhere(AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND ");
1860                applyAggregateRestrictionExceptions(qb);
1861                applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap);
1862                projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID);
1863                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1864                groupBy = aggregateIdColName;
1865                break;
1866            }
1867
1868            case AGGREGATES_SUMMARY_FILTER: {
1869                // TODO: filter query based on callingUid
1870                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1871                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1872                if (uri.getPathSegments().size() > 2) {
1873                    qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment()));
1874                }
1875                groupBy = aggregateIdColName;
1876                break;
1877            }
1878
1879            case AGGREGATES_SUMMARY_STREQUENT_FILTER:
1880            case AGGREGATES_SUMMARY_STREQUENT: {
1881                // Build the first query for starred
1882                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1883                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1884                if (match == AGGREGATES_SUMMARY_STREQUENT_FILTER
1885                        && uri.getPathSegments().size() > 3) {
1886                    qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment()));
1887                }
1888                final String starredQuery = qb.buildQuery(projection, Aggregates.STARRED + "=1",
1889                        null, aggregateIdColName, null, null,
1890                        null /* limit */);
1891
1892                // Build the second query for frequent
1893                qb = new SQLiteQueryBuilder();
1894                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1895                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1896                if (match == AGGREGATES_SUMMARY_STREQUENT_FILTER
1897                        && uri.getPathSegments().size() > 3) {
1898                    qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment()));
1899                }
1900                final String frequentQuery = qb.buildQuery(projection,
1901                        Aggregates.TIMES_CONTACTED + " > 0 AND (" + Aggregates.STARRED
1902                        + " = 0 OR " + Aggregates.STARRED + " IS NULL)",
1903                        null, aggregateIdColName, null, null, null);
1904
1905                // Put them together
1906                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
1907                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
1908                Cursor c = db.rawQueryWithFactory(null, query, null,
1909                        Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1910
1911                if ((c != null) && !isTemporary()) {
1912                    c.setNotificationUri(getContext().getContentResolver(),
1913                            ContactsContract.AUTHORITY_URI);
1914                }
1915                return c;
1916            }
1917
1918            case AGGREGATES_SUMMARY_GROUP: {
1919                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1920                applyAggregateRestrictionExceptions(qb);
1921                applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap);
1922                projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID);
1923                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1924                if (uri.getPathSegments().size() > 2) {
1925                    qb.appendWhere(" AND " + sAggregatesInGroupSelect);
1926                    selectionArgs = appendGroupArg(selectionArgs, uri.getLastPathSegment());
1927                }
1928                groupBy = aggregateIdColName;
1929                break;
1930            }
1931
1932            case AGGREGATES_DATA: {
1933                long aggId = Long.parseLong(uri.getPathSegments().get(1));
1934                qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_CONTACTS_AGGREGATES_GROUPS);
1935                qb.setProjectionMap(sDataContactsGroupsAggregateProjectionMap);
1936                qb.appendWhere(Contacts.AGGREGATE_ID + "=" + aggId + " AND ");
1937                applyDataRestrictionExceptions(qb);
1938                break;
1939            }
1940
1941            case PHONES_FILTER: {
1942                qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_CONTACTS_AGGREGATES);
1943                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1944                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
1945                if (uri.getPathSegments().size() > 2) {
1946                    qb.appendWhere(" AND " + buildAggregateLookupWhereClause(
1947                            uri.getLastPathSegment()));
1948                }
1949                break;
1950            }
1951
1952            case PHONES: {
1953                qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_CONTACTS_AGGREGATES);
1954                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1955                qb.appendWhere(Data.MIMETYPE + " = \"" + Phone.CONTENT_ITEM_TYPE + "\"");
1956                break;
1957            }
1958
1959            case POSTALS: {
1960                qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_CONTACTS_AGGREGATES);
1961                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1962                qb.appendWhere(Data.MIMETYPE + " = \"" + StructuredPostal.CONTENT_ITEM_TYPE + "\"");
1963                break;
1964            }
1965
1966            case CONTACTS: {
1967                qb.setTables(Tables.CONTACTS);
1968                qb.setProjectionMap(sContactsProjectionMap);
1969                applyContactsRestrictionExceptions(qb);
1970                break;
1971            }
1972
1973            case CONTACTS_ID: {
1974                long contactId = ContentUris.parseId(uri);
1975                qb.setTables(Tables.CONTACTS);
1976                qb.setProjectionMap(sContactsProjectionMap);
1977                qb.appendWhere(ContactsColumns.CONCRETE_ID + "=" + contactId + " AND ");
1978                applyContactsRestrictionExceptions(qb);
1979                break;
1980            }
1981
1982            case CONTACTS_DATA: {
1983                long contactId = Long.parseLong(uri.getPathSegments().get(1));
1984                qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_CONTACTS_GROUPS);
1985                qb.setProjectionMap(sDataContactsGroupsProjectionMap);
1986                qb.appendWhere(Data.CONTACT_ID + "=" + contactId + " AND ");
1987                applyDataRestrictionExceptions(qb);
1988                break;
1989            }
1990
1991            case CONTACTS_FILTER_EMAIL: {
1992                // TODO: filter query based on callingUid
1993                qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_CONTACTS_AGGREGATES);
1994                qb.setProjectionMap(sDataContactsProjectionMap);
1995                qb.appendWhere(Data.MIMETYPE + "='" + CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'");
1996                qb.appendWhere(" AND " + CommonDataKinds.Email.DATA + "=");
1997                qb.appendWhereEscapeString(uri.getPathSegments().get(2));
1998                break;
1999            }
2000
2001            case DATA: {
2002                final String accountName = uri.getQueryParameter(Contacts.ACCOUNT_NAME);
2003                final String accountType = uri.getQueryParameter(Contacts.ACCOUNT_TYPE);
2004                if (!TextUtils.isEmpty(accountName)) {
2005                    qb.appendWhere(Contacts.ACCOUNT_NAME + "="
2006                            + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2007                            + Contacts.ACCOUNT_TYPE + "="
2008                            + DatabaseUtils.sqlEscapeString(accountType) + " AND ");
2009                }
2010                qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_CONTACTS_GROUPS);
2011                qb.setProjectionMap(sDataGroupsProjectionMap);
2012                applyDataRestrictionExceptions(qb);
2013                break;
2014            }
2015
2016            case DATA_ID: {
2017                qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_CONTACTS_GROUPS);
2018                qb.setProjectionMap(sDataGroupsProjectionMap);
2019                qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri) + " AND ");
2020                applyDataRestrictionExceptions(qb);
2021                break;
2022            }
2023
2024            case PHONE_LOOKUP: {
2025                // TODO: filter query based on callingUid
2026                if (TextUtils.isEmpty(sortOrder)) {
2027                    // Default the sort order to something reasonable so we get consistent
2028                    // results when callers don't request an ordering
2029                    sortOrder = Data.CONTACT_ID;
2030                }
2031
2032                final String number = uri.getLastPathSegment();
2033                OpenHelper.buildPhoneLookupQuery(qb, number);
2034                qb.setProjectionMap(sDataContactsProjectionMap);
2035                break;
2036            }
2037
2038            case GROUPS: {
2039                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2040                qb.setProjectionMap(sGroupsProjectionMap);
2041                break;
2042            }
2043
2044            case GROUPS_ID: {
2045                long groupId = ContentUris.parseId(uri);
2046                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
2047                qb.setProjectionMap(sGroupsProjectionMap);
2048                qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId);
2049                break;
2050            }
2051
2052            case GROUPS_SUMMARY: {
2053                qb.setTables(Tables.GROUPS_JOIN_PACKAGES_DATA_CONTACTS_AGGREGATES);
2054                qb.setProjectionMap(sGroupsSummaryProjectionMap);
2055                groupBy = GroupsColumns.CONCRETE_ID;
2056                break;
2057            }
2058
2059            case AGGREGATION_EXCEPTIONS: {
2060                qb.setTables(Tables.AGGREGATION_EXCEPTIONS_JOIN_CONTACTS);
2061                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
2062                break;
2063            }
2064
2065            case AGGREGATION_SUGGESTIONS: {
2066                long aggregateId = Long.parseLong(uri.getPathSegments().get(1));
2067                final String maxSuggestionsParam =
2068                        uri.getQueryParameter(AggregationSuggestions.MAX_SUGGESTIONS);
2069
2070                final int maxSuggestions;
2071                if (maxSuggestionsParam != null) {
2072                    maxSuggestions = Integer.parseInt(maxSuggestionsParam);
2073                } else {
2074                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
2075                }
2076
2077                return mContactAggregator.queryAggregationSuggestions(aggregateId, projection,
2078                        sAggregatesProjectionMap, maxSuggestions);
2079            }
2080
2081            default:
2082                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
2083                        sortOrder);
2084        }
2085
2086        // Perform the query and set the notification uri
2087        final Cursor c = qb.query(db, projection, selection, selectionArgs,
2088                groupBy, null, sortOrder, limit);
2089        if (c != null) {
2090            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
2091        }
2092        return c;
2093    }
2094
2095    /**
2096     * List of package names with access to {@link Contacts#IS_RESTRICTED} data.
2097     */
2098    private static final String[] sAllowedPackages = new String[] {
2099        "com.android.contacts",
2100        "com.facebook",
2101    };
2102
2103    /**
2104     * Check if {@link Binder#getCallingUid()} should be allowed access to
2105     * {@link Contacts#IS_RESTRICTED} data.
2106     */
2107    private boolean hasRestrictedAccess() {
2108        final PackageManager pm = getContext().getPackageManager();
2109        final String[] callerPackages = pm.getPackagesForUid(Binder.getCallingUid());
2110
2111        // Has restricted access if caller matches any packages
2112        for (String callerPackage : callerPackages) {
2113            for (String allowedPackage : sAllowedPackages) {
2114                if (allowedPackage.equals(callerPackage)) {
2115                    return true;
2116                }
2117            }
2118        }
2119        return false;
2120    }
2121
2122    /**
2123     * Restrict selection of {@link Aggregates} to only public ones, or those
2124     * the caller has been granted an exception to.
2125     */
2126    private void applyAggregateRestrictionExceptions(SQLiteQueryBuilder qb) {
2127        if (hasRestrictedAccess()) {
2128            qb.appendWhere("1");
2129        } else {
2130            qb.appendWhere(AggregatesColumns.SINGLE_IS_RESTRICTED + "=0");
2131        }
2132    }
2133
2134    /**
2135     * Find any exceptions that have been granted to the calling process, and
2136     * add projections to correctly select {@link Aggregates#PRIMARY_PHONE_ID}
2137     * and {@link Aggregates#PRIMARY_EMAIL_ID}.
2138     */
2139    private void applyAggregatePrimaryRestrictionExceptions(HashMap<String, String> projection) {
2140        String projectionPhone;
2141        String projectionEmail;
2142
2143        if (hasRestrictedAccess()) {
2144            // With restricted access, always give optimal values
2145            projectionPhone = AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID + " AS "
2146                    + Aggregates.PRIMARY_PHONE_ID;
2147            projectionEmail = AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID + " AS "
2148                    + Aggregates.PRIMARY_EMAIL_ID;
2149        } else {
2150            // With general access, always give fallback values
2151            projectionPhone = AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID + " AS "
2152                    + Aggregates.PRIMARY_PHONE_ID;
2153            projectionEmail = AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID + " AS "
2154                    + Aggregates.PRIMARY_EMAIL_ID;
2155        }
2156
2157        projection.remove(Aggregates.PRIMARY_PHONE_ID);
2158        projection.put(Aggregates.PRIMARY_PHONE_ID, projectionPhone);
2159
2160        projection.remove(Aggregates.PRIMARY_EMAIL_ID);
2161        projection.put(Aggregates.PRIMARY_EMAIL_ID, projectionEmail);
2162    }
2163
2164    /**
2165     * Find any exceptions that have been granted to the
2166     * {@link Binder#getCallingUid()}, and add a limiting clause to the given
2167     * {@link SQLiteQueryBuilder} to hide restricted data.
2168     */
2169    private void applyContactsRestrictionExceptions(SQLiteQueryBuilder qb) {
2170        if (hasRestrictedAccess()) {
2171            qb.appendWhere("1");
2172        } else {
2173            qb.appendWhere(Contacts.IS_RESTRICTED + "=0");
2174        }
2175    }
2176
2177    /**
2178     * Find any exceptions that have been granted to the
2179     * {@link Binder#getCallingUid()}, and add a limiting clause to the given
2180     * {@link SQLiteQueryBuilder} to hide restricted data.
2181     */
2182    void applyDataRestrictionExceptions(SQLiteQueryBuilder qb) {
2183        applyContactsRestrictionExceptions(qb);
2184    }
2185
2186    /**
2187     * An implementation of EntityIterator that joins the contacts and data tables
2188     * and consumes all the data rows for a contact in order to build the Entity for a contact.
2189     */
2190    private static class ContactsEntityIterator implements EntityIterator {
2191        private final Cursor mEntityCursor;
2192        private volatile boolean mIsClosed;
2193
2194        private static final String[] DATA_KEYS = new String[]{
2195                "data1",
2196                "data2",
2197                "data3",
2198                "data4",
2199                "data5",
2200                "data6",
2201                "data7",
2202                "data8",
2203                "data9",
2204                "data10",
2205                "data11",
2206                "data12",
2207                "data13",
2208                "data14",
2209                "data15"};
2210
2211        private static final String[] PROJECTION = new String[]{
2212                Contacts.ACCOUNT_NAME,
2213                Contacts.ACCOUNT_TYPE,
2214                Contacts.SOURCE_ID,
2215                Contacts.VERSION,
2216                Contacts.DIRTY,
2217                Contacts.Data._ID,
2218                Contacts.Data.RES_PACKAGE,
2219                Contacts.Data.MIMETYPE,
2220                Contacts.Data.DATA1,
2221                Contacts.Data.DATA2,
2222                Contacts.Data.DATA3,
2223                Contacts.Data.DATA4,
2224                Contacts.Data.DATA5,
2225                Contacts.Data.DATA6,
2226                Contacts.Data.DATA7,
2227                Contacts.Data.DATA8,
2228                Contacts.Data.DATA9,
2229                Contacts.Data.DATA10,
2230                Contacts.Data.DATA11,
2231                Contacts.Data.DATA12,
2232                Contacts.Data.DATA13,
2233                Contacts.Data.DATA14,
2234                Contacts.Data.DATA15,
2235                Contacts.Data.CONTACT_ID,
2236                Contacts.Data.IS_PRIMARY,
2237                Contacts.Data.DATA_VERSION,
2238                GroupMembership.GROUP_SOURCE_ID};
2239
2240        private static final int COLUMN_ACCOUNT_NAME = 0;
2241        private static final int COLUMN_ACCOUNT_TYPE = 1;
2242        private static final int COLUMN_SOURCE_ID = 2;
2243        private static final int COLUMN_VERSION = 3;
2244        private static final int COLUMN_DIRTY = 4;
2245        private static final int COLUMN_DATA_ID = 5;
2246        private static final int COLUMN_RES_PACKAGE = 6;
2247        private static final int COLUMN_MIMETYPE = 7;
2248        private static final int COLUMN_DATA1 = 8;
2249        private static final int COLUMN_CONTACT_ID = 23;
2250        private static final int COLUMN_IS_PRIMARY = 24;
2251        private static final int COLUMN_DATA_VERSION = 25;
2252        private static final int COLUMN_GROUP_SOURCE_ID = 26;
2253
2254        public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri,
2255                String selection, String[] selectionArgs, String sortOrder) {
2256            mIsClosed = false;
2257
2258            final String updatedSortOrder = (sortOrder == null)
2259                    ? Contacts.Data.CONTACT_ID
2260                    : (Contacts.Data.CONTACT_ID + "," + sortOrder);
2261
2262            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
2263            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2264            qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_CONTACTS_GROUPS);
2265            qb.setProjectionMap(sDataContactsGroupsProjectionMap);
2266            if (contactsIdString != null) {
2267                qb.appendWhere(Data.CONTACT_ID + "=" + contactsIdString);
2268            }
2269            final String accountName = uri.getQueryParameter(Contacts.ACCOUNT_NAME);
2270            final String accountType = uri.getQueryParameter(Contacts.ACCOUNT_TYPE);
2271            if (!TextUtils.isEmpty(accountName)) {
2272                qb.appendWhere(Contacts.ACCOUNT_NAME + "="
2273                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
2274                        + Contacts.ACCOUNT_TYPE + "="
2275                        + DatabaseUtils.sqlEscapeString(accountType));
2276            }
2277            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
2278                    null, null, updatedSortOrder);
2279            mEntityCursor.moveToFirst();
2280        }
2281
2282        public void close() {
2283            if (mIsClosed) {
2284                throw new IllegalStateException("closing when already closed");
2285            }
2286            mIsClosed = true;
2287            mEntityCursor.close();
2288        }
2289
2290        public boolean hasNext() throws RemoteException {
2291            if (mIsClosed) {
2292                throw new IllegalStateException("calling hasNext() when the iterator is closed");
2293            }
2294
2295            return !mEntityCursor.isAfterLast();
2296        }
2297
2298        public Entity next() throws RemoteException {
2299            if (mIsClosed) {
2300                throw new IllegalStateException("calling next() when the iterator is closed");
2301            }
2302            if (!hasNext()) {
2303                throw new IllegalStateException("you may only call next() if hasNext() is true");
2304            }
2305
2306            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
2307
2308            final long contactId = c.getLong(COLUMN_CONTACT_ID);
2309
2310            // we expect the cursor is already at the row we need to read from
2311            ContentValues contactValues = new ContentValues();
2312            contactValues.put(Contacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
2313            contactValues.put(Contacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
2314            contactValues.put(Contacts._ID, contactId);
2315            contactValues.put(Contacts.DIRTY, c.getLong(COLUMN_DIRTY));
2316            contactValues.put(Contacts.VERSION, c.getLong(COLUMN_VERSION));
2317            contactValues.put(Contacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
2318            Entity contact = new Entity(contactValues);
2319
2320            // read data rows until the contact id changes
2321            do {
2322                if (contactId != c.getLong(COLUMN_CONTACT_ID)) {
2323                    break;
2324                }
2325                // add the data to to the contact
2326                ContentValues dataValues = new ContentValues();
2327                dataValues.put(Contacts.Data._ID, c.getString(COLUMN_DATA_ID));
2328                dataValues.put(Contacts.Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
2329                dataValues.put(Contacts.Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
2330                dataValues.put(Contacts.Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY));
2331                dataValues.put(Contacts.Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
2332                if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
2333                    dataValues.put(GroupMembership.GROUP_SOURCE_ID,
2334                            c.getString(COLUMN_GROUP_SOURCE_ID));
2335                }
2336                dataValues.put(Contacts.Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
2337                for (int i = 0; i < 10; i++) {
2338                    final int columnIndex = i + COLUMN_DATA1;
2339                    String key = DATA_KEYS[i];
2340                    if (c.isNull(columnIndex)) {
2341                        // don't put anything
2342                    } else if (c.isLong(columnIndex)) {
2343                        dataValues.put(key, c.getLong(columnIndex));
2344                    } else if (c.isFloat(columnIndex)) {
2345                        dataValues.put(key, c.getFloat(columnIndex));
2346                    } else if (c.isString(columnIndex)) {
2347                        dataValues.put(key, c.getString(columnIndex));
2348                    } else if (c.isBlob(columnIndex)) {
2349                        dataValues.put(key, c.getBlob(columnIndex));
2350                    }
2351                }
2352                contact.addSubValue(Data.CONTENT_URI, dataValues);
2353            } while (mEntityCursor.moveToNext());
2354
2355            return contact;
2356        }
2357    }
2358
2359    @Override
2360    public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
2361            String sortOrder) {
2362        final int match = sUriMatcher.match(uri);
2363        switch (match) {
2364            case CONTACTS:
2365            case CONTACTS_ID:
2366                String contactsIdString = null;
2367                if (match == CONTACTS_ID) {
2368                    contactsIdString = uri.getPathSegments().get(1);
2369                }
2370
2371                return new ContactsEntityIterator(this, contactsIdString,
2372                        uri, selection, selectionArgs, sortOrder);
2373            default:
2374                throw new UnsupportedOperationException("Unknown uri: " + uri);
2375        }
2376    }
2377
2378    @Override
2379    public String getType(Uri uri) {
2380        final int match = sUriMatcher.match(uri);
2381        switch (match) {
2382            case AGGREGATES: return Aggregates.CONTENT_TYPE;
2383            case AGGREGATES_ID: return Aggregates.CONTENT_ITEM_TYPE;
2384            case CONTACTS: return Contacts.CONTENT_TYPE;
2385            case CONTACTS_ID: return Contacts.CONTENT_ITEM_TYPE;
2386            case DATA_ID:
2387                final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2388                long dataId = ContentUris.parseId(uri);
2389                return mOpenHelper.getDataMimeType(dataId);
2390            case AGGREGATION_EXCEPTIONS: return AggregationExceptions.CONTENT_TYPE;
2391            case AGGREGATION_EXCEPTION_ID: return AggregationExceptions.CONTENT_ITEM_TYPE;
2392            case AGGREGATION_SUGGESTIONS: return Aggregates.CONTENT_TYPE;
2393        }
2394        throw new UnsupportedOperationException("Unknown uri: " + uri);
2395    }
2396
2397    @Override
2398    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
2399            throws OperationApplicationException {
2400
2401        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
2402        db.beginTransaction();
2403        try {
2404            ContentProviderResult[] results = super.applyBatch(operations);
2405            db.setTransactionSuccessful();
2406            return results;
2407        } finally {
2408            db.endTransaction();
2409        }
2410    }
2411
2412    private void setDisplayName(long contactId, String displayName) {
2413        if (displayName != null) {
2414            mContactDisplayNameUpdate.bindString(1, displayName);
2415        } else {
2416            mContactDisplayNameUpdate.bindNull(1);
2417        }
2418        mContactDisplayNameUpdate.bindLong(2, contactId);
2419        mContactDisplayNameUpdate.execute();
2420    }
2421
2422    /*
2423     * Sets the given dataId record in the "data" table to primary, and resets all data records of
2424     * the same mimetype and under the same contact to not be primary.
2425     *
2426     * @param dataId the id of the data record to be set to primary.
2427     */
2428    private void setIsPrimary(long dataId) {
2429        mSetPrimaryStatement.bindLong(1, dataId);
2430        mSetPrimaryStatement.bindLong(2, dataId);
2431        mSetPrimaryStatement.bindLong(3, dataId);
2432        mSetPrimaryStatement.execute();
2433    }
2434
2435    /*
2436     * Sets the given dataId record in the "data" table to "super primary", and resets all data
2437     * records of the same mimetype and under the same aggregate to not be "super primary".
2438     *
2439     * @param dataId the id of the data record to be set to primary.
2440     */
2441    private void setIsSuperPrimary(long dataId) {
2442        mSetSuperPrimaryStatement.bindLong(1, dataId);
2443        mSetSuperPrimaryStatement.bindLong(2, dataId);
2444        mSetSuperPrimaryStatement.bindLong(3, dataId);
2445        mSetSuperPrimaryStatement.execute();
2446
2447        // Find the parent aggregate and package for this new primary
2448        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
2449
2450        long aggId = -1;
2451        boolean isRestricted = false;
2452        String mimeType = null;
2453
2454        Cursor cursor = null;
2455        try {
2456            cursor = db.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
2457                    DataColumns.CONCRETE_ID + "=" + dataId, null, null, null, null);
2458            if (cursor.moveToFirst()) {
2459                aggId = cursor.getLong(DataContactsQuery.AGGREGATE_ID);
2460                isRestricted = (cursor.getInt(DataContactsQuery.IS_RESTRICTED) == 1);
2461                mimeType = cursor.getString(DataContactsQuery.MIMETYPE);
2462            }
2463        } finally {
2464            if (cursor != null) {
2465                cursor.close();
2466            }
2467        }
2468
2469        // Bypass aggregate update if no parent found, or if we don't keep track
2470        // of super-primary for this mimetype.
2471        if (aggId == -1) {
2472            return;
2473        }
2474
2475        boolean isPhone = CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType);
2476        boolean isEmail = CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType);
2477
2478        // Record this value as the new primary for the parent aggregate
2479        final ContentValues values = new ContentValues();
2480        if (isPhone) {
2481            values.put(AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID, dataId);
2482            values.put(AggregatesColumns.OPTIMAL_PRIMARY_PHONE_IS_RESTRICTED, isRestricted);
2483        } else if (isEmail) {
2484            values.put(AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID, dataId);
2485            values.put(AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_IS_RESTRICTED, isRestricted);
2486        }
2487
2488        // If this data is unrestricted, then also set as fallback
2489        if (!isRestricted && isPhone) {
2490            values.put(AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID, dataId);
2491        } else if (!isRestricted && isEmail) {
2492            values.put(AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID, dataId);
2493        }
2494
2495        // Push update into aggregates table, if needed
2496        if (values.size() > 0) {
2497            db.update(Tables.AGGREGATES, values, Aggregates._ID + "=" + aggId, null);
2498        }
2499
2500    }
2501
2502    private String buildAggregateLookupWhereClause(String filterParam) {
2503        StringBuilder filter = new StringBuilder();
2504        filter.append(Tables.AGGREGATES);
2505        filter.append(".");
2506        filter.append(Aggregates._ID);
2507        filter.append(" IN (SELECT ");
2508        filter.append(Contacts.AGGREGATE_ID);
2509        filter.append(" FROM ");
2510        filter.append(Tables.CONTACTS);
2511        filter.append(" WHERE ");
2512        filter.append(Contacts._ID);
2513        filter.append(" IN (SELECT  contact_id FROM name_lookup WHERE normalized_name GLOB '");
2514        // NOTE: Query parameters won't work here since the SQL compiler
2515        // needs to parse the actual string to know that it can use the
2516        // index to do a prefix scan.
2517        filter.append(NameNormalizer.normalize(filterParam) + "*");
2518        filter.append("'))");
2519        return filter.toString();
2520    }
2521
2522    private String[] appendGroupArg(String[] selectionArgs, String arg) {
2523        if (selectionArgs == null) {
2524            return new String[] {arg};
2525        } else {
2526            int newLength = selectionArgs.length + 1;
2527            String[] newSelectionArgs = new String[newLength];
2528            System.arraycopy(selectionArgs, 0, newSelectionArgs, 0, selectionArgs.length);
2529            newSelectionArgs[newLength - 1] = arg;
2530            return newSelectionArgs;
2531        }
2532    }
2533}
2534