ContactsProvider2.java revision b67163a1088f09c59f324350662eb18772fac6b6
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.providers.contacts;
18
19import com.android.providers.contacts.OpenHelper.AggregatesColumns;
20import com.android.providers.contacts.OpenHelper.AggregationExceptionColumns;
21import com.android.providers.contacts.OpenHelper.Clauses;
22import com.android.providers.contacts.OpenHelper.ContactsColumns;
23import com.android.providers.contacts.OpenHelper.ContactOptionsColumns;
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.PhoneLookupColumns;
28import com.android.providers.contacts.OpenHelper.Tables;
29import com.android.internal.content.SyncStateContentProviderHelper;
30
31import android.accounts.Account;
32import android.content.pm.PackageManager;
33import android.content.ContentProvider;
34import android.content.UriMatcher;
35import android.content.Context;
36import android.content.ContentValues;
37import android.content.ContentUris;
38import android.content.EntityIterator;
39import android.content.Entity;
40import android.content.ContentProviderResult;
41import android.content.OperationApplicationException;
42import android.content.ContentProviderOperation;
43import android.database.Cursor;
44import android.database.DatabaseUtils;
45import android.database.sqlite.SQLiteCursor;
46import android.database.sqlite.SQLiteDatabase;
47import android.database.sqlite.SQLiteQueryBuilder;
48import android.database.sqlite.SQLiteStatement;
49import android.net.Uri;
50import android.os.Binder;
51import android.os.RemoteException;
52import android.provider.BaseColumns;
53import android.provider.ContactsContract;
54import android.provider.Contacts.ContactMethods;
55import android.provider.ContactsContract.Aggregates;
56import android.provider.ContactsContract.AggregationExceptions;
57import android.provider.ContactsContract.CommonDataKinds;
58import android.provider.ContactsContract.Contacts;
59import android.provider.ContactsContract.Data;
60import android.provider.ContactsContract.Groups;
61import android.provider.ContactsContract.Presence;
62import android.provider.ContactsContract.RestrictionExceptions;
63import android.provider.ContactsContract.Aggregates.AggregationSuggestions;
64import android.provider.ContactsContract.CommonDataKinds.Email;
65import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
66import android.provider.ContactsContract.CommonDataKinds.Phone;
67import android.provider.ContactsContract.CommonDataKinds.Postal;
68import android.provider.ContactsContract.CommonDataKinds.StructuredName;
69import android.telephony.PhoneNumberUtils;
70import android.text.TextUtils;
71import android.util.Log;
72
73import java.util.ArrayList;
74import java.util.HashMap;
75
76/**
77 * Contacts content provider. The contract between this provider and applications
78 * is defined in {@link ContactsContract}.
79 */
80public class ContactsProvider2 extends ContentProvider {
81    // TODO: clean up debug tag and rename this class
82    private static final String TAG = "ContactsProvider ~~~~";
83
84    // TODO: define broadcastreceiver to catch app uninstalls that should clear exceptions
85    // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
86    // TODO: check for restricted flag during insert(), update(), and delete() calls
87
88    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
89
90    private static final String STREQUENT_ORDER_BY = Aggregates.STARRED + " DESC, "
91            + Aggregates.TIMES_CONTACTED + " DESC, "
92            + Aggregates.DISPLAY_NAME + " ASC";
93    private static final String STREQUENT_LIMIT =
94            "(SELECT COUNT(1) FROM " + Tables.AGGREGATES + " WHERE "
95            + Aggregates.STARRED + "=1) + 25";
96
97    private static final int AGGREGATES = 1000;
98    private static final int AGGREGATES_ID = 1001;
99    private static final int AGGREGATES_DATA = 1002;
100    private static final int AGGREGATES_SUMMARY = 1003;
101    private static final int AGGREGATES_SUMMARY_ID = 1004;
102    private static final int AGGREGATES_SUMMARY_FILTER = 1005;
103    private static final int AGGREGATES_SUMMARY_STREQUENT = 1006;
104    private static final int AGGREGATES_SUMMARY_STREQUENT_FILTER = 1007;
105    private static final int AGGREGATES_SUMMARY_GROUP = 1008;
106
107    private static final int CONTACTS = 2002;
108    private static final int CONTACTS_ID = 2003;
109    private static final int CONTACTS_DATA = 2004;
110    private static final int CONTACTS_FILTER_EMAIL = 2005;
111
112    private static final int DATA = 3000;
113    private static final int DATA_ID = 3001;
114    private static final int PHONES = 3002;
115    private static final int PHONES_FILTER = 3003;
116    private static final int POSTALS = 3004;
117
118    private static final int PHONE_LOOKUP = 4000;
119
120    private static final int AGGREGATION_EXCEPTIONS = 6000;
121    private static final int AGGREGATION_EXCEPTION_ID = 6001;
122
123    private static final int PRESENCE = 7000;
124    private static final int PRESENCE_ID = 7001;
125
126    private static final int AGGREGATION_SUGGESTIONS = 8000;
127
128    private static final int RESTRICTION_EXCEPTIONS = 9000;
129
130    private static final int GROUPS = 10000;
131    private static final int GROUPS_ID = 10001;
132    private static final int GROUPS_SUMMARY = 10003;
133
134    private static final int SYNCSTATE = 11000;
135
136    private interface Projections {
137        public static final String[] PROJ_CONTACTS = new String[] {
138            ContactsColumns.CONCRETE_ID,
139        };
140
141        public static final String[] PROJ_DATA_CONTACTS = new String[] {
142                ContactsColumns.CONCRETE_ID,
143                DataColumns.CONCRETE_ID,
144                Contacts.AGGREGATE_ID,
145                ContactsColumns.PACKAGE_ID,
146                Contacts.IS_RESTRICTED,
147                Data.MIMETYPE,
148        };
149
150        public static final int COL_CONTACT_ID = 0;
151        public static final int COL_DATA_ID = 1;
152        public static final int COL_AGGREGATE_ID = 2;
153        public static final int COL_PACKAGE_ID = 3;
154        public static final int COL_IS_RESTRICTED = 4;
155        public static final int COL_MIMETYPE = 5;
156
157        public static final String[] PROJ_DATA_AGGREGATES = new String[] {
158            ContactsColumns.CONCRETE_ID,
159                DataColumns.CONCRETE_ID,
160                AggregatesColumns.CONCRETE_ID,
161                MimetypesColumns.CONCRETE_ID,
162                Phone.NUMBER,
163                Email.DATA,
164                AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID,
165                AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID,
166                AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID,
167                AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID,
168        };
169
170        public static final int COL_MIMETYPE_ID = 3;
171        public static final int COL_PHONE_NUMBER = 4;
172        public static final int COL_EMAIL_DATA = 5;
173        public static final int COL_OPTIMAL_PHONE_ID = 6;
174        public static final int COL_FALLBACK_PHONE_ID = 7;
175        public static final int COL_OPTIMAL_EMAIL_ID = 8;
176        public static final int COL_FALLBACK_EMAIL_ID = 9;
177
178    }
179
180    /** Default for the maximum number of returned aggregation suggestions. */
181    private static final int DEFAULT_MAX_SUGGESTIONS = 5;
182
183    /** Contains just the contacts columns */
184    private static final HashMap<String, String> sAggregatesProjectionMap;
185    /** Contains the aggregate columns along with primary phone */
186    private static final HashMap<String, String> sAggregatesSummaryProjectionMap;
187    /** Contains the data, contacts, and aggregate columns, for joined tables. */
188    private static final HashMap<String, String> sDataContactsAggregateProjectionMap;
189    /** Contains just the contacts columns */
190    private static final HashMap<String, String> sContactsProjectionMap;
191    /** Contains just the data columns */
192    private static final HashMap<String, String> sDataProjectionMap;
193    /** Contains the data and contacts columns, for joined tables */
194    private static final HashMap<String, String> sDataContactsProjectionMap;
195    /** Contains the just the {@link Groups} columns */
196    private static final HashMap<String, String> sGroupsProjectionMap;
197    /** Contains {@link Groups} columns along with summary details */
198    private static final HashMap<String, String> sGroupsSummaryProjectionMap;
199    /** Contains the just the agg_exceptions columns */
200    private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
201    /** Contains the just the {@link RestrictionExceptions} columns */
202    private static final HashMap<String, String> sRestrictionExceptionsProjectionMap;
203
204    /** Sql select statement that returns the contact id associated with a data record. */
205    private static final String sNestedContactIdSelect;
206    /** Sql select statement that returns the mimetype id associated with a data record. */
207    private static final String sNestedMimetypeSelect;
208    /** Sql select statement that returns the aggregate id associated with a contact record. */
209    private static final String sNestedAggregateIdSelect;
210    /** Sql select statement that returns a list of contact ids associated with an aggregate record. */
211    private static final String sNestedContactIdListSelect;
212    /** Sql where statement used to match all the data records that need to be updated when a new
213     * "primary" is selected.*/
214    private static final String sSetPrimaryWhere;
215    /** Sql where statement used to match all the data records that need to be updated when a new
216     * "super primary" is selected.*/
217    private static final String sSetSuperPrimaryWhere;
218    /** Sql where statement for filtering on groups. */
219    private static final String sAggregatesInGroupSelect;
220    /** Precompiled sql statement for setting a data record to the primary. */
221    private SQLiteStatement mSetPrimaryStatement;
222    /** Precomipled sql statement for setting a data record to the super primary. */
223    private SQLiteStatement mSetSuperPrimaryStatement;
224
225    private static final String GTALK_PROTOCOL_STRING = ContactMethods
226            .encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
227
228    static {
229        // Contacts URI matching table
230        final UriMatcher matcher = sUriMatcher;
231        matcher.addURI(ContactsContract.AUTHORITY, "aggregates", AGGREGATES);
232        matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#", AGGREGATES_ID);
233        matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#/data", AGGREGATES_DATA);
234        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary", AGGREGATES_SUMMARY);
235        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/#", AGGREGATES_SUMMARY_ID);
236        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/filter/*",
237                AGGREGATES_SUMMARY_FILTER);
238        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/strequent/",
239                AGGREGATES_SUMMARY_STREQUENT);
240        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/strequent/filter/*",
241                AGGREGATES_SUMMARY_STREQUENT_FILTER);
242        matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/group/*",
243                AGGREGATES_SUMMARY_GROUP);
244        matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#/suggestions",
245                AGGREGATION_SUGGESTIONS);
246        matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
247        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
248        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
249        matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter_email/*",
250                CONTACTS_FILTER_EMAIL);
251
252        matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
253        matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
254        matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
255        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
256        matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
257
258        matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
259        matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
260        matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
261
262        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
263
264        matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
265        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
266                AGGREGATION_EXCEPTIONS);
267        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
268                AGGREGATION_EXCEPTION_ID);
269
270        matcher.addURI(ContactsContract.AUTHORITY, "presence", PRESENCE);
271        matcher.addURI(ContactsContract.AUTHORITY, "presence/#", PRESENCE_ID);
272
273        matcher.addURI(ContactsContract.AUTHORITY, "restriction_exceptions", RESTRICTION_EXCEPTIONS);
274
275        HashMap<String, String> columns;
276
277        // Aggregates projection map
278        columns = new HashMap<String, String>();
279        columns.put(Aggregates._ID, "aggregates._id AS _id");
280        columns.put(Aggregates.DISPLAY_NAME, Aggregates.DISPLAY_NAME);
281        columns.put(Aggregates.LAST_TIME_CONTACTED, Aggregates.LAST_TIME_CONTACTED);
282        columns.put(Aggregates.TIMES_CONTACTED, Aggregates.TIMES_CONTACTED);
283        columns.put(Aggregates.STARRED, Aggregates.STARRED);
284        columns.put(Aggregates.IN_VISIBLE_GROUP, Aggregates.IN_VISIBLE_GROUP);
285        columns.put(Aggregates.PHOTO_ID, Aggregates.PHOTO_ID);
286        columns.put(Aggregates.PRIMARY_PHONE_ID, Aggregates.PRIMARY_PHONE_ID);
287        columns.put(Aggregates.PRIMARY_EMAIL_ID, Aggregates.PRIMARY_EMAIL_ID);
288        columns.put(Aggregates.CUSTOM_RINGTONE, Aggregates.CUSTOM_RINGTONE);
289        columns.put(Aggregates.SEND_TO_VOICEMAIL, Aggregates.SEND_TO_VOICEMAIL);
290        columns.put(AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID,
291                AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID);
292        columns.put(AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID,
293                AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID);
294        sAggregatesProjectionMap = columns;
295
296        // Aggregates primaries projection map. The overall presence status is
297        // the most-present value, as indicated by the largest value.
298        columns = new HashMap<String, String>();
299        columns.putAll(sAggregatesProjectionMap);
300        columns.put(CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE);
301        columns.put(CommonDataKinds.Phone.LABEL, CommonDataKinds.Phone.LABEL);
302        columns.put(CommonDataKinds.Phone.NUMBER, CommonDataKinds.Phone.NUMBER);
303        columns.put(Presence.PRESENCE_STATUS, "MAX(" + Presence.PRESENCE_STATUS + ")");
304        sAggregatesSummaryProjectionMap = columns;
305
306        // Contacts projection map
307        columns = new HashMap<String, String>();
308        columns.put(Contacts._ID, "contacts._id AS _id");
309        columns.put(Contacts.PACKAGE, Contacts.PACKAGE);
310        columns.put(Contacts.AGGREGATE_ID, Contacts.AGGREGATE_ID);
311        columns.put(Contacts.ACCOUNT_NAME, Contacts.ACCOUNT_NAME);
312        columns.put(Contacts.ACCOUNT_TYPE, Contacts.ACCOUNT_TYPE);
313        columns.put(Contacts.SOURCE_ID, Contacts.SOURCE_ID);
314        columns.put(Contacts.VERSION, Contacts.VERSION);
315        columns.put(Contacts.DIRTY, Contacts.DIRTY);
316        sContactsProjectionMap = columns;
317
318        // Data projection map
319        columns = new HashMap<String, String>();
320        columns.put(Data._ID, "data._id AS _id");
321        columns.put(Data.CONTACT_ID, Data.CONTACT_ID);
322        columns.put(Data.MIMETYPE, Data.MIMETYPE);
323        columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
324        columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
325        columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
326        columns.put(Data.DATA1, "data.data1 as data1");
327        columns.put(Data.DATA2, "data.data2 as data2");
328        columns.put(Data.DATA3, "data.data3 as data3");
329        columns.put(Data.DATA4, "data.data4 as data4");
330        columns.put(Data.DATA5, "data.data5 as data5");
331        columns.put(Data.DATA6, "data.data6 as data6");
332        columns.put(Data.DATA7, "data.data7 as data7");
333        columns.put(Data.DATA8, "data.data8 as data8");
334        columns.put(Data.DATA9, "data.data9 as data9");
335        columns.put(Data.DATA10, "data.data10 as data10");
336        // Mappings used for backwards compatibility.
337        columns.put("number", Phone.NUMBER);
338        sDataProjectionMap = columns;
339
340        // Data and contacts projection map for joins. _id comes from the data table
341        columns = new HashMap<String, String>();
342        columns.putAll(sContactsProjectionMap);
343        columns.putAll(sDataProjectionMap); // _id will be replaced with the one from data
344        columns.put(Data.CONTACT_ID, DataColumns.CONCRETE_CONTACT_ID);
345        sDataContactsProjectionMap = columns;
346
347        // Data and contacts projection map for joins. _id comes from the data table
348        columns = new HashMap<String, String>();
349        columns.putAll(sAggregatesProjectionMap);
350        columns.putAll(sContactsProjectionMap); //
351        columns.putAll(sDataProjectionMap); // _id will be replaced with the one from data
352        columns.put(Data.CONTACT_ID, DataColumns.CONCRETE_CONTACT_ID);
353        sDataContactsAggregateProjectionMap = columns;
354
355        // Groups projection map
356        columns = new HashMap<String, String>();
357        columns.put(Groups._ID, "groups._id AS _id");
358        columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
359        columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
360        columns.put(Groups.PACKAGE, Groups.PACKAGE);
361        columns.put(Groups.PACKAGE_ID, GroupsColumns.CONCRETE_PACKAGE_ID);
362        columns.put(Groups.TITLE, Groups.TITLE);
363        columns.put(Groups.TITLE_RESOURCE, Groups.TITLE_RESOURCE);
364        columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
365        sGroupsProjectionMap = columns;
366
367        // Contacts and groups projection map
368        columns = new HashMap<String, String>();
369        columns.putAll(sGroupsProjectionMap);
370
371        columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + AggregatesColumns.CONCRETE_ID
372                + ") FROM " + Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES + " WHERE "
373                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
374                + ") AS " + Groups.SUMMARY_COUNT);
375
376        columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
377                + AggregatesColumns.CONCRETE_ID + ") FROM "
378                + Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES + " WHERE "
379                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
380                + " AND " + Clauses.HAS_PRIMARY_PHONE + ") AS " + Groups.SUMMARY_WITH_PHONES);
381
382        sGroupsSummaryProjectionMap = columns;
383
384        // Aggregate exception projection map
385        columns = new HashMap<String, String>();
386        columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
387        columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
388        columns.put(AggregationExceptions.AGGREGATE_ID,
389                "contacts1." + Contacts.AGGREGATE_ID + " AS " + AggregationExceptions.AGGREGATE_ID);
390        columns.put(AggregationExceptions.CONTACT_ID, AggregationExceptionColumns.CONTACT_ID2);
391        sAggregationExceptionsProjectionMap = columns;
392
393        // Restriction exception projection map
394        columns = new HashMap<String, String>();
395        columns.put(RestrictionExceptions.PACKAGE_PROVIDER, RestrictionExceptions.PACKAGE_PROVIDER);
396        columns.put(RestrictionExceptions.PACKAGE_CLIENT, RestrictionExceptions.PACKAGE_CLIENT);
397        columns.put(RestrictionExceptions.ALLOW_ACCESS, "1"); // Access granted if row returned
398        sRestrictionExceptionsProjectionMap = columns;
399
400        sNestedContactIdSelect = "SELECT " + Data.CONTACT_ID + " FROM " + Tables.DATA + " WHERE "
401                + Data._ID + "=?";
402        sNestedMimetypeSelect = "SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA
403                + " WHERE " + Data._ID + "=?";
404        sNestedAggregateIdSelect = "SELECT " + Contacts.AGGREGATE_ID + " FROM " + Tables.CONTACTS
405                + " WHERE " + Contacts._ID + "=(" + sNestedContactIdSelect + ")";
406        sNestedContactIdListSelect = "SELECT " + Contacts._ID + " FROM " + Tables.CONTACTS
407                + " WHERE " + Contacts.AGGREGATE_ID + "=(" + sNestedAggregateIdSelect + ")";
408        sSetPrimaryWhere = Data.CONTACT_ID + "=(" + sNestedContactIdSelect + ") AND "
409                + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
410        sSetSuperPrimaryWhere = Data.CONTACT_ID + " IN (" + sNestedContactIdListSelect + ") AND "
411                + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
412        sAggregatesInGroupSelect = AggregatesColumns.CONCRETE_ID + " IN (SELECT "
413                + Contacts.AGGREGATE_ID + " FROM " + Tables.CONTACTS + " WHERE ("
414                + ContactsColumns.CONCRETE_ID + " IN (SELECT " + Tables.DATA + "."
415                + Data.CONTACT_ID + " FROM " + Tables.DATA_JOIN_MIMETYPES + " WHERE ("
416                + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND "
417                + GroupMembership.GROUP_ROW_ID + "=(SELECT " + Tables.GROUPS + "."
418                + Groups._ID + " FROM " + Tables.GROUPS + " WHERE " + Groups.TITLE + "=?)))))";
419    }
420
421    private final ContactAggregationScheduler mAggregationScheduler;
422    private OpenHelper mOpenHelper;
423
424    private ContactAggregator mContactAggregator;
425    private NameSplitter mNameSplitter;
426
427    public ContactsProvider2() {
428        this(new ContactAggregationScheduler());
429    }
430
431    /**
432     * Constructor for testing.
433     */
434    /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
435        mAggregationScheduler = scheduler;
436    }
437
438    @Override
439    public boolean onCreate() {
440        final Context context = getContext();
441
442        mOpenHelper = getOpenHelper(context);
443        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
444
445        mContactAggregator = new ContactAggregator(context, mOpenHelper, mAggregationScheduler);
446
447        mSetPrimaryStatement = db.compileStatement(
448                "UPDATE " + Tables.DATA + " SET " + Data.IS_PRIMARY
449                + "=(_id=?) WHERE " + sSetPrimaryWhere);
450        mSetSuperPrimaryStatement = db.compileStatement(
451                "UPDATE " + Tables.DATA + " SET " + Data.IS_SUPER_PRIMARY
452                + "=(_id=?) WHERE " + sSetSuperPrimaryWhere);
453
454        mNameSplitter = new NameSplitter(
455                context.getString(com.android.internal.R.string.common_name_prefixes),
456                context.getString(com.android.internal.R.string.common_last_name_prefixes),
457                context.getString(com.android.internal.R.string.common_name_suffixes),
458                context.getString(com.android.internal.R.string.common_name_conjunctions));
459
460        return (db != null);
461    }
462
463    /* Visible for testing */
464    protected OpenHelper getOpenHelper(final Context context) {
465        return OpenHelper.getInstance(context);
466    }
467
468    @Override
469    protected void finalize() throws Throwable {
470        if (mContactAggregator != null) {
471            mContactAggregator.quit();
472        }
473
474        super.finalize();
475    }
476
477    /**
478     * Wipes all data from the contacts database.
479     */
480    /* package */ void wipeData() {
481        mOpenHelper.wipeData();
482    }
483
484    /**
485     * Called when a change has been made.
486     *
487     * @param uri the uri that the change was made to
488     */
489    private void onChange(Uri uri) {
490        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
491    }
492
493    @Override
494    public boolean isTemporary() {
495        return false;
496    }
497
498    @Override
499    public Uri insert(Uri uri, ContentValues values) {
500        final int match = sUriMatcher.match(uri);
501        long id = 0;
502
503        switch (match) {
504            case SYNCSTATE:
505                id = mOpenHelper.getSyncState().insert(mOpenHelper.getWritableDatabase(), values);
506                break;
507
508            case AGGREGATES: {
509                id = insertAggregate(values);
510                break;
511            }
512
513            case CONTACTS: {
514                final Account account = readAccountFromQueryParams(uri);
515                id = insertContact(values, account);
516                break;
517            }
518
519            case CONTACTS_DATA: {
520                values.put(Data.CONTACT_ID, uri.getPathSegments().get(1));
521                id = insertData(values);
522                break;
523            }
524
525            case DATA: {
526                id = insertData(values);
527                break;
528            }
529
530            case GROUPS: {
531                final Account account = readAccountFromQueryParams(uri);
532                id = insertGroup(values, account);
533                break;
534            }
535
536            case PRESENCE: {
537                id = insertPresence(values);
538                break;
539            }
540
541            default:
542                throw new UnsupportedOperationException("Unknown uri: " + uri);
543        }
544
545        if (id < 0) {
546            return null;
547        }
548
549        final Uri result = ContentUris.withAppendedId(uri, id);
550        onChange(result);
551        return result;
552    }
553
554    /**
555     * If account is non-null then store it in the values. If the account is already
556     * specified in the values then it must be consistent with the account, if it is non-null.
557     * @param values the ContentValues to read from and update
558     * @param account the explicitly provided Account
559     * @return false if the accounts are inconsistent
560     */
561    private boolean resolveAccount(ContentValues values, Account account) {
562        // If either is specified then both must be specified.
563        final String accountName = values.getAsString(Contacts.ACCOUNT_NAME);
564        final String accountType = values.getAsString(Contacts.ACCOUNT_TYPE);
565        if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
566            final Account valuesAccount = new Account(accountName, accountType);
567            if (account != null && !valuesAccount.equals(account)) {
568                return false;
569            }
570            account = valuesAccount;
571        }
572        if (account != null) {
573            values.put(Contacts.ACCOUNT_NAME, account.mName);
574            values.put(Contacts.ACCOUNT_TYPE, account.mType);
575        }
576        return true;
577    }
578
579    /**
580     * Inserts an item in the aggregates table
581     *
582     * @param values the values for the new row
583     * @return the row ID of the newly created row
584     */
585    private long insertAggregate(ContentValues values) {
586        throw new UnsupportedOperationException("Aggregates are created automatically");
587    }
588
589    /**
590     * Inserts an item in the contacts table
591     *
592     * @param values the values for the new row
593     * @param account the account this contact should be associated with. may be null.
594     * @return the row ID of the newly created row
595     */
596    private long insertContact(ContentValues values, Account account) {
597        /*
598         * The contact record is inserted in the contacts table, but it needs to
599         * be processed by the aggregator before it will be returned by the
600         * "aggregates" queries.
601         */
602        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
603
604        ContentValues overriddenValues = new ContentValues(values);
605        overriddenValues.putNull(Contacts.AGGREGATE_ID);
606        if (!resolveAccount(overriddenValues, account)) {
607            return -1;
608        }
609
610        // Replace package with internal mapping
611        final String packageName = overriddenValues.getAsString(Contacts.PACKAGE);
612        overriddenValues.put(ContactsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
613        overriddenValues.remove(Contacts.PACKAGE);
614
615        long rowId = db.insert(Tables.CONTACTS, Contacts.AGGREGATE_ID, overriddenValues);
616
617        mContactAggregator.schedule();
618
619        return rowId;
620    }
621
622    /**
623     * Inserts an item in the data table
624     *
625     * @param values the values for the new row
626     * @return the row ID of the newly created row
627     */
628    private long insertData(ContentValues values) {
629        boolean success = false;
630
631        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
632        long id = 0;
633        db.beginTransaction();
634        try {
635            long contactId = values.getAsLong(Data.CONTACT_ID);
636
637            // Replace mimetype with internal mapping
638            final String mimeType = values.getAsString(Data.MIMETYPE);
639            values.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
640            values.remove(Data.MIMETYPE);
641
642            if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
643                parseStructuredName(values);
644            }
645
646            // Insert the data row itself
647            id = db.insert(Tables.DATA, Data.DATA1, values);
648
649            // If it's a phone number add the normalized version to the lookup table
650            if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
651                final ContentValues phoneValues = new ContentValues();
652                final String number = values.getAsString(Phone.NUMBER);
653                phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER,
654                        PhoneNumberUtils.getStrippedReversed(number));
655                phoneValues.put(PhoneLookupColumns.DATA_ID, id);
656                phoneValues.put(PhoneLookupColumns.CONTACT_ID, contactId);
657                db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
658            }
659
660            mContactAggregator.markContactForAggregation(contactId);
661
662            db.setTransactionSuccessful();
663            success = true;
664        } finally {
665            db.endTransaction();
666        }
667
668        if (success) {
669            mContactAggregator.schedule();
670        }
671
672        return id;
673    }
674
675    /**
676     * Delete the given {@link Data} row, fixing up any {@link Aggregates}
677     * primaries that reference it.
678     */
679    private int deleteData(long dataId) {
680        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
681
682        final long mimePhone = mOpenHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
683        final long mimeEmail = mOpenHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
684
685        // Check to see if the data about to be deleted was a super-primary on
686        // the parent aggregate, and set flags to fix-up once deleted.
687        long aggId = -1;
688        long mimeId = -1;
689        String dataRaw = null;
690        boolean fixOptimal = false;
691        boolean fixFallback = false;
692
693        Cursor cursor = null;
694        try {
695            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES,
696                    Projections.PROJ_DATA_AGGREGATES, DataColumns.CONCRETE_ID + "=" + dataId, null,
697                    null, null, null);
698            if (cursor.moveToFirst()) {
699                aggId = cursor.getLong(Projections.COL_AGGREGATE_ID);
700                mimeId = cursor.getLong(Projections.COL_MIMETYPE_ID);
701                if (mimeId == mimePhone) {
702                    dataRaw = cursor.getString(Projections.COL_PHONE_NUMBER);
703                    fixOptimal = (cursor.getLong(Projections.COL_OPTIMAL_PHONE_ID) == dataId);
704                    fixFallback = (cursor.getLong(Projections.COL_FALLBACK_PHONE_ID) == dataId);
705                } else if (mimeId == mimeEmail) {
706                    dataRaw = cursor.getString(Projections.COL_EMAIL_DATA);
707                    fixOptimal = (cursor.getLong(Projections.COL_OPTIMAL_EMAIL_ID) == dataId);
708                    fixFallback = (cursor.getLong(Projections.COL_FALLBACK_EMAIL_ID) == dataId);
709                }
710            }
711        } finally {
712            if (cursor != null) {
713                cursor.close();
714                cursor = null;
715            }
716        }
717
718        // Delete the requested data item.
719        int dataDeleted = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
720
721        // Fix-up any super-primary values that are now invalid.
722        if (fixOptimal || fixFallback) {
723            final ContentValues values = new ContentValues();
724            final StringBuilder scoreClause = new StringBuilder();
725
726            final String SCORE = "score";
727
728            // Build scoring clause that will first pick data items under the
729            // same aggregate that have identical values, otherwise fall back to
730            // normal primary scoring from the member contacts.
731            scoreClause.append("(CASE WHEN ");
732            if (mimeId == mimePhone) {
733                scoreClause.append(Phone.NUMBER);
734            } else if (mimeId == mimeEmail) {
735                scoreClause.append(Email.DATA);
736            }
737            scoreClause.append("=");
738            DatabaseUtils.appendEscapedSQLString(scoreClause, dataRaw);
739            scoreClause.append(" THEN 2 ELSE " + Data.IS_PRIMARY + " END) AS " + SCORE);
740
741            final String[] PROJ_PRIMARY = new String[] {
742                    DataColumns.CONCRETE_ID,
743                    Contacts.IS_RESTRICTED,
744                    ContactsColumns.PACKAGE_ID,
745                    scoreClause.toString(),
746            };
747
748            final int COL_DATA_ID = 0;
749            final int COL_IS_RESTRICTED = 1;
750            final int COL_PACKAGE_ID = 2;
751            final int COL_SCORE = 3;
752
753            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES, PROJ_PRIMARY,
754                    AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND " + DataColumns.MIMETYPE_ID
755                            + "=" + mimeId, null, null, null, SCORE);
756
757            if (fixOptimal) {
758                String colId = null;
759                String colPackageId = null;
760                if (mimeId == mimePhone) {
761                    colId = AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID;
762                    colPackageId = AggregatesColumns.OPTIMAL_PRIMARY_PHONE_PACKAGE_ID;
763                } else if (mimeId == mimeEmail) {
764                    colId = AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID;
765                    colPackageId = AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_PACKAGE_ID;
766                }
767
768                // Start by replacing with null, since fixOptimal told us that
769                // the previous aggregate values are bad.
770                values.putNull(colId);
771                values.putNull(colPackageId);
772
773                // When finding a new optimal primary, we only care about the
774                // highest scoring value, regardless of source.
775                if (cursor.moveToFirst()) {
776                    final long newOptimal = cursor.getLong(COL_DATA_ID);
777                    final long newOptimalPackage = cursor.getLong(COL_PACKAGE_ID);
778
779                    if (newOptimal != 0) {
780                        values.put(colId, newOptimal);
781                    }
782                    if (newOptimalPackage != 0) {
783                        values.put(colPackageId, newOptimalPackage);
784                    }
785                }
786            }
787
788            if (fixFallback) {
789                String colId = null;
790                if (mimeId == mimePhone) {
791                    colId = AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID;
792                } else if (mimeId == mimeEmail) {
793                    colId = AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID;
794                }
795
796                // Start by replacing with null, since fixFallback told us that
797                // the previous aggregate values are bad.
798                values.putNull(colId);
799
800                // The best fallback value is the highest scoring data item that
801                // hasn't been restricted.
802                cursor.moveToPosition(-1);
803                while (cursor.moveToNext()) {
804                    final boolean isRestricted = (cursor.getInt(COL_IS_RESTRICTED) == 1);
805                    if (!isRestricted) {
806                        values.put(colId, cursor.getLong(COL_DATA_ID));
807                        break;
808                    }
809                }
810            }
811
812            // Push through any aggregate updates we have
813            if (values.size() > 0) {
814                db.update(Tables.AGGREGATES, values, AggregatesColumns.CONCRETE_ID + "=" + aggId,
815                        null);
816            }
817        }
818
819        return dataDeleted;
820    }
821
822    /**
823     * Parse the supplied display name, but only if the incoming values do not already contain
824     * structured name parts.
825     */
826    private void parseStructuredName(ContentValues values) {
827        final String fullName = values.getAsString(StructuredName.DISPLAY_NAME);
828        if (TextUtils.isEmpty(fullName)
829                || !TextUtils.isEmpty(values.getAsString(StructuredName.PREFIX))
830                || !TextUtils.isEmpty(values.getAsString(StructuredName.GIVEN_NAME))
831                || !TextUtils.isEmpty(values.getAsString(StructuredName.MIDDLE_NAME))
832                || !TextUtils.isEmpty(values.getAsString(StructuredName.FAMILY_NAME))
833                || !TextUtils.isEmpty(values.getAsString(StructuredName.SUFFIX))) {
834            return;
835        }
836
837        NameSplitter.Name name = new NameSplitter.Name();
838        mNameSplitter.split(name, fullName);
839
840        values.put(StructuredName.PREFIX, name.getPrefix());
841        values.put(StructuredName.GIVEN_NAME, name.getGivenNames());
842        values.put(StructuredName.MIDDLE_NAME, name.getMiddleName());
843        values.put(StructuredName.FAMILY_NAME, name.getFamilyName());
844        values.put(StructuredName.SUFFIX, name.getSuffix());
845    }
846
847    /**
848     * Inserts an item in the groups table
849     */
850    private long insertGroup(ContentValues values, Account account) {
851        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
852
853        ContentValues overriddenValues = new ContentValues(values);
854        if (!resolveAccount(overriddenValues, account)) {
855            return -1;
856        }
857
858        // Replace package with internal mapping
859        final String packageName = overriddenValues.getAsString(Groups.PACKAGE);
860        overriddenValues.put(Groups.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
861        overriddenValues.remove(Groups.PACKAGE);
862
863        return db.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
864    }
865
866    /**
867     * Inserts a presence update.
868     */
869    private long insertPresence(ContentValues values) {
870        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
871        final String handle = values.getAsString(Presence.IM_HANDLE);
872        final String protocol = values.getAsString(Presence.IM_PROTOCOL);
873        if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
874            throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required");
875        }
876
877        // TODO: generalize to allow other providers to match against email
878        boolean matchEmail = GTALK_PROTOCOL_STRING.equals(protocol);
879
880        String selection;
881        String[] selectionArgs;
882        if (matchEmail) {
883            selection = "(" + Clauses.WHERE_IM_MATCHES + ") OR (" + Clauses.WHERE_EMAIL_MATCHES + ")";
884            selectionArgs = new String[] { protocol, handle, handle };
885        } else {
886            selection = Clauses.WHERE_IM_MATCHES;
887            selectionArgs = new String[] { protocol, handle };
888        }
889
890        long dataId = -1;
891        long aggId = -1;
892        Cursor cursor = null;
893        try {
894            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES,
895                    Projections.PROJ_DATA_CONTACTS, selection, selectionArgs, null, null, null);
896            if (cursor.moveToFirst()) {
897                dataId = cursor.getLong(Projections.COL_DATA_ID);
898                aggId = cursor.getLong(Projections.COL_AGGREGATE_ID);
899            } else {
900                // No contact found, return a null URI
901                return -1;
902            }
903        } finally {
904            if (cursor != null) {
905                cursor.close();
906            }
907        }
908
909        values.put(Presence.DATA_ID, dataId);
910        values.put(Presence.AGGREGATE_ID, aggId);
911
912        // Insert the presence update
913        long presenceId = db.replace(Tables.PRESENCE, null, values);
914        return presenceId;
915    }
916
917    @Override
918    public int delete(Uri uri, String selection, String[] selectionArgs) {
919        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
920        final int match = sUriMatcher.match(uri);
921        switch (match) {
922            case SYNCSTATE:
923                return mOpenHelper.getSyncState().delete(db, selection, selectionArgs);
924
925            case AGGREGATES_ID: {
926                long aggregateId = ContentUris.parseId(uri);
927
928                // Remove references to the aggregate first
929                ContentValues values = new ContentValues();
930                values.putNull(Contacts.AGGREGATE_ID);
931                db.update(Tables.CONTACTS, values, Contacts.AGGREGATE_ID + "=" + aggregateId, null);
932
933                return db.delete(Tables.AGGREGATES, BaseColumns._ID + "=" + aggregateId, null);
934            }
935
936            case CONTACTS_ID: {
937                long contactId = ContentUris.parseId(uri);
938                int contactsDeleted = db.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
939                int dataDeleted = db.delete(Tables.DATA, Data.CONTACT_ID + "=" + contactId, null);
940                return contactsDeleted + dataDeleted;
941            }
942
943            case DATA_ID: {
944                long dataId = ContentUris.parseId(uri);
945                return deleteData(dataId);
946            }
947
948            case GROUPS_ID: {
949                long groupId = ContentUris.parseId(uri);
950                final long groupMembershipMimetypeId = mOpenHelper
951                        .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
952                int groupsDeleted = db.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
953                int dataDeleted = db.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
954                        + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
955                        + groupId, null);
956                mOpenHelper.updateAllVisible();
957                return groupsDeleted + dataDeleted;
958            }
959
960            case PRESENCE: {
961                return db.delete(Tables.PRESENCE, null, null);
962            }
963
964            default:
965                throw new UnsupportedOperationException("Unknown uri: " + uri);
966        }
967    }
968
969    private static Account readAccountFromQueryParams(Uri uri) {
970        final String name = uri.getQueryParameter(Contacts.ACCOUNT_NAME);
971        final String type = uri.getQueryParameter(Contacts.ACCOUNT_TYPE);
972        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
973            return null;
974        }
975        return new Account(name, type);
976    }
977
978
979    @Override
980    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
981        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
982        int count = 0;
983
984        final int match = sUriMatcher.match(uri);
985        switch(match) {
986            case SYNCSTATE:
987                return mOpenHelper.getSyncState().update(db, values, selection, selectionArgs);
988
989            // TODO(emillar): We will want to disallow editing the aggregates table at some point.
990            case AGGREGATES: {
991                count = db.update(Tables.AGGREGATES, values, selection, selectionArgs);
992                break;
993            }
994
995            case AGGREGATES_ID: {
996                count = updateAggregateData(db, ContentUris.parseId(uri), values);
997                break;
998            }
999
1000            case DATA_ID: {
1001                boolean containsIsSuperPrimary = values.containsKey(Data.IS_SUPER_PRIMARY);
1002                boolean containsIsPrimary = values.containsKey(Data.IS_PRIMARY);
1003                final long id = ContentUris.parseId(uri);
1004
1005                // Remove primary or super primary values being set to 0. This is disallowed by the
1006                // content provider.
1007                if (containsIsSuperPrimary && values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
1008                    containsIsSuperPrimary = false;
1009                    values.remove(Data.IS_SUPER_PRIMARY);
1010                }
1011                if (containsIsPrimary && values.getAsInteger(Data.IS_PRIMARY) == 0) {
1012                    containsIsPrimary = false;
1013                    values.remove(Data.IS_PRIMARY);
1014                }
1015
1016                if (containsIsSuperPrimary) {
1017                    setIsSuperPrimary(id);
1018                    setIsPrimary(id);
1019
1020                    // Now that we've taken care of setting these, remove them from "values".
1021                    values.remove(Data.IS_SUPER_PRIMARY);
1022                    if (containsIsPrimary) {
1023                        values.remove(Data.IS_PRIMARY);
1024                    }
1025                } else if (containsIsPrimary) {
1026                    setIsPrimary(id);
1027
1028                    // Now that we've taken care of setting this, remove it from "values".
1029                    values.remove(Data.IS_PRIMARY);
1030                }
1031
1032                if (values.size() > 0) {
1033                    String selectionWithId = (Data._ID + " = " + ContentUris.parseId(uri) + " ")
1034                            + (selection == null ? "" : " AND " + selection);
1035                    count = db.update(Tables.DATA, values, selectionWithId, selectionArgs);
1036                }
1037                break;
1038            }
1039
1040            case CONTACTS: {
1041                count = db.update(Tables.CONTACTS, values, selection, selectionArgs);
1042                break;
1043            }
1044
1045            case CONTACTS_ID: {
1046                String selectionWithId = (Contacts._ID + " = " + ContentUris.parseId(uri) + " ")
1047                        + (selection == null ? "" : " AND " + selection);
1048                count = db.update(Tables.CONTACTS, values, selectionWithId, selectionArgs);
1049                Log.i(TAG, "Selection is: " + selectionWithId);
1050                break;
1051            }
1052
1053            case DATA: {
1054                count = db.update(Tables.DATA, values, selection, selectionArgs);
1055                break;
1056            }
1057
1058            case GROUPS: {
1059                count = db.update(Tables.GROUPS, values, selection, selectionArgs);
1060                mOpenHelper.updateAllVisible();
1061                break;
1062            }
1063
1064            case GROUPS_ID: {
1065                long groupId = ContentUris.parseId(uri);
1066                String selectionWithId = (Groups._ID + "=" + groupId + " ")
1067                        + (selection == null ? "" : " AND " + selection);
1068                count = db.update(Tables.GROUPS, values, selectionWithId, selectionArgs);
1069
1070                // If changing visibility, then update aggregates
1071                if (values.containsKey(Groups.GROUP_VISIBLE)) {
1072                    mOpenHelper.updateAllVisible();
1073                }
1074
1075                break;
1076            }
1077
1078            case AGGREGATION_EXCEPTIONS: {
1079                count = updateAggregationException(db, values);
1080                break;
1081            }
1082
1083            case RESTRICTION_EXCEPTIONS: {
1084                // Enforce required fields
1085                boolean hasFields = values.containsKey(RestrictionExceptions.PACKAGE_PROVIDER)
1086                        && values.containsKey(RestrictionExceptions.PACKAGE_CLIENT)
1087                        && values.containsKey(RestrictionExceptions.ALLOW_ACCESS);
1088                if (!hasFields) {
1089                    throw new IllegalArgumentException("PACKAGE_PROVIDER, PACKAGE_CLIENT, and"
1090                            + "ALLOW_ACCESS are all required fields");
1091                }
1092
1093                final String packageProvider = values
1094                        .getAsString(RestrictionExceptions.PACKAGE_PROVIDER);
1095                final boolean allowAccess = (values
1096                        .getAsInteger(RestrictionExceptions.ALLOW_ACCESS) == 1);
1097
1098                final Context context = getContext();
1099                final PackageManager pm = context.getPackageManager();
1100
1101                // Enforce that caller has authority over the requested package
1102                // TODO: move back to Binder.getCallingUid() when we can stub-out test suite
1103                final int callingUid = OpenHelper
1104                        .getUidForPackageName(pm, context.getPackageName());
1105                final String[] ownedPackages = pm.getPackagesForUid(callingUid);
1106                if (!isContained(ownedPackages, packageProvider)) {
1107                    throw new RuntimeException(
1108                            "Requested PACKAGE_PROVIDER doesn't belong to calling UID.");
1109                }
1110
1111                // Add or remove exception using exception helper
1112                if (allowAccess) {
1113                    mOpenHelper.addRestrictionException(context, values);
1114                } else {
1115                    mOpenHelper.removeRestrictionException(context, values);
1116                }
1117
1118                break;
1119            }
1120
1121            default:
1122                throw new UnsupportedOperationException("Unknown uri: " + uri);
1123        }
1124
1125        if (count > 0) {
1126            getContext().getContentResolver().notifyChange(uri, null);
1127        }
1128        return count;
1129    }
1130
1131    private int updateAggregateData(SQLiteDatabase db, long aggregateId, ContentValues values) {
1132
1133        // First update all constituent contacts
1134        ContentValues optionValues = new ContentValues(3);
1135        if (values.containsKey(Aggregates.CUSTOM_RINGTONE)) {
1136            optionValues.put(ContactOptionsColumns.CUSTOM_RINGTONE,
1137                    values.getAsString(Aggregates.CUSTOM_RINGTONE));
1138        }
1139        if (values.containsKey(Aggregates.SEND_TO_VOICEMAIL)) {
1140            optionValues.put(ContactOptionsColumns.SEND_TO_VOICEMAIL,
1141                    values.getAsBoolean(Aggregates.SEND_TO_VOICEMAIL));
1142        }
1143
1144        // Nothing to update - just return
1145        if (optionValues.size() == 0) {
1146            return 0;
1147        }
1148
1149        Cursor c = db.query(Tables.CONTACTS, Projections.PROJ_CONTACTS, Contacts.AGGREGATE_ID + "="
1150                + aggregateId, null, null, null, null);
1151        try {
1152            while (c.moveToNext()) {
1153                long contactId = c.getLong(Projections.COL_CONTACT_ID);
1154
1155                optionValues.put(ContactOptionsColumns._ID, contactId);
1156                db.replace(Tables.CONTACT_OPTIONS, null, optionValues);
1157            }
1158        } finally {
1159            c.close();
1160        }
1161
1162        // Now update the aggregate itself.  Ignore all supplied fields except rington and
1163        // send_to_voicemail
1164        optionValues.clear();
1165        if (values.containsKey(Aggregates.CUSTOM_RINGTONE)) {
1166            optionValues.put(Aggregates.CUSTOM_RINGTONE,
1167                    values.getAsString(Aggregates.CUSTOM_RINGTONE));
1168        }
1169        if (values.containsKey(Aggregates.SEND_TO_VOICEMAIL)) {
1170            optionValues.put(Aggregates.SEND_TO_VOICEMAIL,
1171                    values.getAsBoolean(Aggregates.SEND_TO_VOICEMAIL));
1172        }
1173
1174        return db.update(Tables.AGGREGATES, optionValues, Aggregates._ID + "=" + aggregateId, null);
1175    }
1176
1177    private static class ContactPair {
1178        final long contactId1;
1179        final long contactId2;
1180
1181        /**
1182         * Constructor that ensures that this.contactId1 &lt; this.contactId2
1183         */
1184        public ContactPair(long contactId1, long contactId2) {
1185            if (contactId1 < contactId2) {
1186                this.contactId1 = contactId1;
1187                this.contactId2 = contactId2;
1188            } else {
1189                this.contactId2 = contactId1;
1190                this.contactId1 = contactId2;
1191            }
1192        }
1193    }
1194
1195    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
1196        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
1197        long aggregateId = values.getAsInteger(AggregationExceptions.AGGREGATE_ID);
1198        long contactId = values.getAsInteger(AggregationExceptions.CONTACT_ID);
1199
1200        // First, we build a list of contactID-contactID pairs for the given aggregate and contact.
1201        ArrayList<ContactPair> pairs = new ArrayList<ContactPair>();
1202        Cursor c = db.query(Tables.CONTACTS, Projections.PROJ_CONTACTS,
1203                Contacts.AGGREGATE_ID + "=" + aggregateId,
1204                null, null, null, null);
1205        try {
1206            while (c.moveToNext()) {
1207                long aggregatedContactId = c.getLong(Projections.COL_CONTACT_ID);
1208                if (aggregatedContactId != contactId) {
1209                    pairs.add(new ContactPair(aggregatedContactId, contactId));
1210                }
1211            }
1212        } finally {
1213            c.close();
1214        }
1215
1216        // Now we iterate through all contact pairs to see if we need to insert/delete/update
1217        // the corresponding exception
1218        ContentValues exceptionValues = new ContentValues(3);
1219        exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
1220        for (ContactPair pair : pairs) {
1221            final String whereClause =
1222                    AggregationExceptionColumns.CONTACT_ID1 + "=" + pair.contactId1 + " AND "
1223                    + AggregationExceptionColumns.CONTACT_ID2 + "=" + pair.contactId2;
1224            if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
1225                db.delete(Tables.AGGREGATION_EXCEPTIONS, whereClause, null);
1226            } else {
1227                exceptionValues.put(AggregationExceptionColumns.CONTACT_ID1, pair.contactId1);
1228                exceptionValues.put(AggregationExceptionColumns.CONTACT_ID2, pair.contactId2);
1229                db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
1230                        exceptionValues);
1231            }
1232        }
1233
1234        mContactAggregator.markContactForAggregation(contactId);
1235        mContactAggregator.aggregateContact(contactId);
1236        if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC
1237                || exceptionType == AggregationExceptions.TYPE_KEEP_OUT) {
1238            mContactAggregator.updateAggregateData(aggregateId);
1239        }
1240
1241        // The return value is fake - we just confirm that we made a change, not count actual
1242        // rows changed.
1243        return 1;
1244    }
1245
1246    /**
1247     * Test if a {@link String} value appears in the given list.
1248     */
1249    private boolean isContained(String[] array, String value) {
1250        if (array != null) {
1251            for (String test : array) {
1252                if (value.equals(test)) {
1253                    return true;
1254                }
1255            }
1256        }
1257        return false;
1258    }
1259
1260    /**
1261     * Test if a {@link String} value appears in the given list, and add to the
1262     * array if the value doesn't already appear.
1263     */
1264    private String[] assertContained(String[] array, String value) {
1265        if (array == null) {
1266            array = new String[] {value};
1267        } else if (!isContained(array, value)) {
1268            String[] newArray = new String[array.length + 1];
1269            System.arraycopy(array, 0, newArray, 0, array.length);
1270            newArray[array.length] = value;
1271            array = newArray;
1272        }
1273        return array;
1274    }
1275
1276    @Override
1277    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
1278            String sortOrder) {
1279        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1280
1281        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1282        String groupBy = null;
1283        String limit = null;
1284        String aggregateIdColName = Tables.AGGREGATES + "." + Aggregates._ID;
1285
1286        // TODO: Consider writing a test case for RestrictionExceptions when you
1287        // write a new query() block to make sure it protects restricted data.
1288        final int match = sUriMatcher.match(uri);
1289        switch (match) {
1290            case SYNCSTATE:
1291                return mOpenHelper.getSyncState().query(db, projection, selection,  selectionArgs,
1292                        sortOrder);
1293
1294            case AGGREGATES: {
1295                qb.setTables(Tables.AGGREGATES);
1296                applyAggregateRestrictionExceptions(qb);
1297                applyAggregatePrimaryRestrictionExceptions(sAggregatesProjectionMap);
1298                qb.setProjectionMap(sAggregatesProjectionMap);
1299                break;
1300            }
1301
1302            case AGGREGATES_ID: {
1303                long aggId = ContentUris.parseId(uri);
1304                qb.setTables(Tables.AGGREGATES);
1305                qb.appendWhere(AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND ");
1306                applyAggregateRestrictionExceptions(qb);
1307                applyAggregatePrimaryRestrictionExceptions(sAggregatesProjectionMap);
1308                qb.setProjectionMap(sAggregatesProjectionMap);
1309                break;
1310            }
1311
1312            case AGGREGATES_SUMMARY: {
1313                // TODO: join into social status tables
1314                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1315                applyAggregateRestrictionExceptions(qb);
1316                applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap);
1317                projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID);
1318                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1319                groupBy = aggregateIdColName;
1320                break;
1321            }
1322
1323            case AGGREGATES_SUMMARY_ID: {
1324                // TODO: join into social status tables
1325                long aggId = ContentUris.parseId(uri);
1326                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1327                qb.appendWhere(AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND ");
1328                applyAggregateRestrictionExceptions(qb);
1329                applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap);
1330                projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID);
1331                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1332                groupBy = aggregateIdColName;
1333                break;
1334            }
1335
1336            case AGGREGATES_SUMMARY_FILTER: {
1337                // TODO: filter query based on callingUid
1338                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1339                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1340                if (uri.getPathSegments().size() > 2) {
1341                    qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment()));
1342                }
1343                groupBy = aggregateIdColName;
1344                break;
1345            }
1346
1347            case AGGREGATES_SUMMARY_STREQUENT_FILTER:
1348            case AGGREGATES_SUMMARY_STREQUENT: {
1349                // Build the first query for starred
1350                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1351                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1352                if (match == AGGREGATES_SUMMARY_STREQUENT_FILTER
1353                        && uri.getPathSegments().size() > 3) {
1354                    qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment()));
1355                }
1356                final String starredQuery = qb.buildQuery(projection, Aggregates.STARRED + "=1",
1357                        null, aggregateIdColName, null, null,
1358                        null /* limit */);
1359
1360                // Build the second query for frequent
1361                qb = new SQLiteQueryBuilder();
1362                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1363                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1364                if (match == AGGREGATES_SUMMARY_STREQUENT_FILTER
1365                        && uri.getPathSegments().size() > 3) {
1366                    qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment()));
1367                }
1368                final String frequentQuery = qb.buildQuery(projection,
1369                        Aggregates.TIMES_CONTACTED + " > 0 AND (" + Aggregates.STARRED
1370                        + " = 0 OR " + Aggregates.STARRED + " IS NULL)",
1371                        null, aggregateIdColName, null, null, null);
1372
1373                // Put them together
1374                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
1375                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
1376                Cursor c = db.rawQueryWithFactory(null, query, null,
1377                        Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1378
1379                if ((c != null) && !isTemporary()) {
1380                    c.setNotificationUri(getContext().getContentResolver(),
1381                            ContactsContract.AUTHORITY_URI);
1382                }
1383                return c;
1384            }
1385
1386            case AGGREGATES_SUMMARY_GROUP: {
1387                qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE);
1388                applyAggregateRestrictionExceptions(qb);
1389                applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap);
1390                projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID);
1391                qb.setProjectionMap(sAggregatesSummaryProjectionMap);
1392                if (uri.getPathSegments().size() > 2) {
1393                    qb.appendWhere(" AND " + sAggregatesInGroupSelect);
1394                    selectionArgs = appendGroupArg(selectionArgs, uri.getLastPathSegment());
1395                }
1396                groupBy = aggregateIdColName;
1397                break;
1398            }
1399
1400            case AGGREGATES_DATA: {
1401                long aggId = Long.parseLong(uri.getPathSegments().get(1));
1402                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES);
1403                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1404                qb.appendWhere(Contacts.AGGREGATE_ID + "=" + aggId + " AND ");
1405                applyDataRestrictionExceptions(qb);
1406                break;
1407            }
1408
1409            case PHONES_FILTER: {
1410                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES);
1411                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1412                qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
1413                if (uri.getPathSegments().size() > 2) {
1414                    qb.appendWhere(" AND " + buildAggregateLookupWhereClause(
1415                            uri.getLastPathSegment()));
1416                }
1417                break;
1418            }
1419
1420            case PHONES: {
1421                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES);
1422                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1423                qb.appendWhere(Data.MIMETYPE + " = \"" + Phone.CONTENT_ITEM_TYPE + "\"");
1424                break;
1425            }
1426
1427            case POSTALS: {
1428                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES);
1429                qb.setProjectionMap(sDataContactsAggregateProjectionMap);
1430                qb.appendWhere(Data.MIMETYPE + " = \"" + Postal.CONTENT_ITEM_TYPE + "\"");
1431                break;
1432            }
1433
1434            case CONTACTS: {
1435                qb.setTables(Tables.CONTACTS_JOIN_PACKAGES);
1436                qb.setProjectionMap(sContactsProjectionMap);
1437                applyContactsRestrictionExceptions(qb);
1438                break;
1439            }
1440
1441            case CONTACTS_ID: {
1442                long contactId = ContentUris.parseId(uri);
1443                qb.setTables(Tables.CONTACTS_JOIN_PACKAGES);
1444                qb.setProjectionMap(sContactsProjectionMap);
1445                qb.appendWhere(ContactsColumns.CONCRETE_ID + "=" + contactId + " AND ");
1446                applyContactsRestrictionExceptions(qb);
1447                break;
1448            }
1449
1450            case CONTACTS_DATA: {
1451                long contactId = Long.parseLong(uri.getPathSegments().get(1));
1452                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES);
1453                qb.setProjectionMap(sDataContactsProjectionMap);
1454                qb.appendWhere(Data.CONTACT_ID + "=" + contactId + " AND ");
1455                applyDataRestrictionExceptions(qb);
1456                break;
1457            }
1458
1459            case CONTACTS_FILTER_EMAIL: {
1460                // TODO: filter query based on callingUid
1461                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES);
1462                qb.setProjectionMap(sDataContactsProjectionMap);
1463                qb.appendWhere(Data.MIMETYPE + "='" + CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'");
1464                qb.appendWhere(" AND " + CommonDataKinds.Email.DATA + "=");
1465                qb.appendWhereEscapeString(uri.getPathSegments().get(2));
1466                break;
1467            }
1468
1469            case DATA: {
1470                final String accountName = uri.getQueryParameter(Contacts.ACCOUNT_NAME);
1471                final String accountType = uri.getQueryParameter(Contacts.ACCOUNT_TYPE);
1472                if (!TextUtils.isEmpty(accountName)) {
1473                    qb.appendWhere(Contacts.ACCOUNT_NAME + "="
1474                            + DatabaseUtils.sqlEscapeString(accountName) + " AND "
1475                            + Contacts.ACCOUNT_TYPE + "="
1476                            + DatabaseUtils.sqlEscapeString(accountType) + " AND ");
1477                }
1478                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES);
1479                qb.setProjectionMap(sDataProjectionMap);
1480                applyDataRestrictionExceptions(qb);
1481                break;
1482            }
1483
1484            case DATA_ID: {
1485                qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES);
1486                qb.setProjectionMap(sDataProjectionMap);
1487                qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri) + " AND ");
1488                applyDataRestrictionExceptions(qb);
1489                break;
1490            }
1491
1492            case PHONE_LOOKUP: {
1493                // TODO: filter query based on callingUid
1494                if (TextUtils.isEmpty(sortOrder)) {
1495                    // Default the sort order to something reasonable so we get consistent
1496                    // results when callers don't request an ordering
1497                    sortOrder = Data.CONTACT_ID;
1498                }
1499
1500                final String number = uri.getLastPathSegment();
1501                OpenHelper.buildPhoneLookupQuery(qb, number);
1502                qb.setProjectionMap(sDataContactsProjectionMap);
1503                break;
1504            }
1505
1506            case GROUPS: {
1507                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
1508                qb.setProjectionMap(sGroupsProjectionMap);
1509                break;
1510            }
1511
1512            case GROUPS_ID: {
1513                long groupId = ContentUris.parseId(uri);
1514                qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
1515                qb.setProjectionMap(sGroupsProjectionMap);
1516                qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId);
1517                break;
1518            }
1519
1520            case GROUPS_SUMMARY: {
1521                qb.setTables(Tables.GROUPS_JOIN_PACKAGES_DATA_CONTACTS_AGGREGATES);
1522                qb.setProjectionMap(sGroupsSummaryProjectionMap);
1523                groupBy = GroupsColumns.CONCRETE_ID;
1524                break;
1525            }
1526
1527            case AGGREGATION_EXCEPTIONS: {
1528                qb.setTables(Tables.AGGREGATION_EXCEPTIONS_JOIN_CONTACTS);
1529                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
1530                break;
1531            }
1532
1533            case AGGREGATION_SUGGESTIONS: {
1534                long aggregateId = Long.parseLong(uri.getPathSegments().get(1));
1535                final String maxSuggestionsParam =
1536                        uri.getQueryParameter(AggregationSuggestions.MAX_SUGGESTIONS);
1537
1538                final int maxSuggestions;
1539                if (maxSuggestionsParam != null) {
1540                    maxSuggestions = Integer.parseInt(maxSuggestionsParam);
1541                } else {
1542                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
1543                }
1544
1545                return mContactAggregator.queryAggregationSuggestions(aggregateId, projection,
1546                        sAggregatesProjectionMap, maxSuggestions);
1547            }
1548
1549            case RESTRICTION_EXCEPTIONS: {
1550                qb.setTables(Tables.RESTRICTION_EXCEPTIONS);
1551                qb.setProjectionMap(sRestrictionExceptionsProjectionMap);
1552                break;
1553            }
1554
1555            default:
1556                throw new UnsupportedOperationException("Unknown uri: " + uri);
1557        }
1558
1559        // Perform the query and set the notification uri
1560        final Cursor c = qb.query(db, projection, selection, selectionArgs,
1561                groupBy, null, sortOrder, limit);
1562        if (c != null) {
1563            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
1564        }
1565        return c;
1566    }
1567
1568    /**
1569     * Restrict selection of {@link Aggregates} to only public ones, or those
1570     * the caller has been granted a {@link RestrictionExceptions} to.
1571     */
1572    private void applyAggregateRestrictionExceptions(SQLiteQueryBuilder qb) {
1573        final int clientUid = OpenHelper.getUidForPackageName(getContext().getPackageManager(),
1574                getContext().getPackageName());
1575
1576        qb.appendWhere("(" + AggregatesColumns.SINGLE_RESTRICTED_PACKAGE_ID + " IS NULL");
1577        final String exceptionClause = mOpenHelper.getRestrictionExceptionClause(clientUid,
1578                AggregatesColumns.SINGLE_RESTRICTED_PACKAGE_ID);
1579        if (exceptionClause != null) {
1580            qb.appendWhere(" OR (" + exceptionClause + ")");
1581        }
1582        qb.appendWhere(")");
1583    }
1584
1585    /**
1586     * Find any exceptions that have been granted to the calling process, and
1587     * add projections to correctly select {@link Aggregates#PRIMARY_PHONE_ID}
1588     * and {@link Aggregates#PRIMARY_EMAIL_ID}.
1589     */
1590    private void applyAggregatePrimaryRestrictionExceptions(HashMap<String, String> projection) {
1591        // TODO: move back to Binder.getCallingUid() when we can stub-out test suite
1592        final int clientUid = OpenHelper.getUidForPackageName(getContext().getPackageManager(),
1593                getContext().getPackageName());
1594
1595        final String projectionPhone = "(CASE WHEN "
1596                + mOpenHelper.getRestrictionExceptionClause(clientUid,
1597                        AggregatesColumns.OPTIMAL_PRIMARY_PHONE_PACKAGE_ID) + " THEN "
1598                + AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID + " ELSE "
1599                + AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID + " END) AS "
1600                + Aggregates.PRIMARY_PHONE_ID;
1601        projection.remove(Aggregates.PRIMARY_PHONE_ID);
1602        projection.put(Aggregates.PRIMARY_PHONE_ID, projectionPhone);
1603
1604        final String projectionEmail = "(CASE WHEN "
1605            + mOpenHelper.getRestrictionExceptionClause(clientUid,
1606                    AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_PACKAGE_ID) + " THEN "
1607            + AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID + " ELSE "
1608            + AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID + " END) AS "
1609            + Aggregates.PRIMARY_EMAIL_ID;
1610        projection.remove(Aggregates.PRIMARY_EMAIL_ID);
1611        projection.put(Aggregates.PRIMARY_EMAIL_ID, projectionEmail);
1612    }
1613
1614    /**
1615     * Find any exceptions that have been granted to the
1616     * {@link Binder#getCallingUid()}, and add a limiting clause to the given
1617     * {@link SQLiteQueryBuilder} to hide restricted data.
1618     */
1619    private void applyContactsRestrictionExceptions(SQLiteQueryBuilder qb) {
1620        // TODO: move back to Binder.getCallingUid() when we can stub-out test suite
1621        final int clientUid = OpenHelper.getUidForPackageName(getContext().getPackageManager(),
1622                getContext().getPackageName());
1623
1624        qb.appendWhere("(" + Contacts.IS_RESTRICTED + "=0");
1625        final String exceptionClause = mOpenHelper.getRestrictionExceptionClause(clientUid,
1626                ContactsColumns.PACKAGE_ID);
1627        if (exceptionClause != null) {
1628            qb.appendWhere(" OR (" + exceptionClause + ")");
1629        }
1630        qb.appendWhere(")");
1631    }
1632
1633    /**
1634     * Find any exceptions that have been granted to the
1635     * {@link Binder#getCallingUid()}, and add a limiting clause to the given
1636     * {@link SQLiteQueryBuilder} to hide restricted data.
1637     */
1638    private void applyDataRestrictionExceptions(SQLiteQueryBuilder qb) {
1639        applyContactsRestrictionExceptions(qb);
1640    }
1641
1642    /**
1643     * An implementation of EntityIterator that joins the contacts and data tables
1644     * and consumes all the data rows for a contact in order to build the Entity for a contact.
1645     */
1646    private static class ContactsEntityIterator implements EntityIterator {
1647        private final Cursor mEntityCursor;
1648        private volatile boolean mIsClosed;
1649
1650        private static final String[] DATA_KEYS = new String[]{
1651                "data1",
1652                "data2",
1653                "data3",
1654                "data4",
1655                "data5",
1656                "data6",
1657                "data7",
1658                "data8",
1659                "data9",
1660                "data10"};
1661
1662        private static final String[] PROJECTION = new String[]{
1663                Contacts.ACCOUNT_NAME,
1664                Contacts.ACCOUNT_TYPE,
1665                Contacts.SOURCE_ID,
1666                Contacts.VERSION,
1667                Contacts.DIRTY,
1668                Contacts.Data._ID,
1669                Contacts.Data.MIMETYPE,
1670                Contacts.Data.DATA1,
1671                Contacts.Data.DATA2,
1672                Contacts.Data.DATA3,
1673                Contacts.Data.DATA4,
1674                Contacts.Data.DATA5,
1675                Contacts.Data.DATA6,
1676                Contacts.Data.DATA7,
1677                Contacts.Data.DATA8,
1678                Contacts.Data.DATA9,
1679                Contacts.Data.DATA10,
1680                Contacts.Data.CONTACT_ID,
1681                Contacts.Data.IS_PRIMARY,
1682                Contacts.Data.DATA_VERSION};
1683
1684        private static final int COLUMN_ACCOUNT_NAME = 0;
1685        private static final int COLUMN_ACCOUNT_TYPE = 1;
1686        private static final int COLUMN_SOURCE_ID = 2;
1687        private static final int COLUMN_VERSION = 3;
1688        private static final int COLUMN_DIRTY = 4;
1689        private static final int COLUMN_DATA_ID = 5;
1690        private static final int COLUMN_MIMETYPE = 6;
1691        private static final int COLUMN_DATA1 = 7;
1692        private static final int COLUMN_CONTACT_ID = 17;
1693        private static final int COLUMN_IS_PRIMARY = 18;
1694        private static final int COLUMN_DATA_VERSION = 19;
1695
1696        public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri,
1697                String selection, String[] selectionArgs, String sortOrder) {
1698            mIsClosed = false;
1699
1700            final String updatedSortOrder = (sortOrder == null)
1701                    ? Contacts.Data.CONTACT_ID
1702                    : (Contacts.Data.CONTACT_ID + "," + sortOrder);
1703
1704            final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
1705            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1706            qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES);
1707            qb.setProjectionMap(sDataContactsProjectionMap);
1708            if (contactsIdString != null) {
1709                qb.appendWhere(Data.CONTACT_ID + "=" + contactsIdString);
1710            }
1711            final String accountName = uri.getQueryParameter(Contacts.ACCOUNT_NAME);
1712            final String accountType = uri.getQueryParameter(Contacts.ACCOUNT_TYPE);
1713            if (!TextUtils.isEmpty(accountName)) {
1714                qb.appendWhere(Contacts.ACCOUNT_NAME + "="
1715                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
1716                        + Contacts.ACCOUNT_TYPE + "="
1717                        + DatabaseUtils.sqlEscapeString(accountType));
1718            }
1719            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
1720                    null, null, updatedSortOrder);
1721            mEntityCursor.moveToFirst();
1722        }
1723
1724        public void close() {
1725            if (mIsClosed) {
1726                throw new IllegalStateException("closing when already closed");
1727            }
1728            mIsClosed = true;
1729            mEntityCursor.close();
1730        }
1731
1732        public boolean hasNext() throws RemoteException {
1733            if (mIsClosed) {
1734                throw new IllegalStateException("calling hasNext() when the iterator is closed");
1735            }
1736
1737            return !mEntityCursor.isAfterLast();
1738        }
1739
1740        public Entity next() throws RemoteException {
1741            if (mIsClosed) {
1742                throw new IllegalStateException("calling next() when the iterator is closed");
1743            }
1744            if (!hasNext()) {
1745                throw new IllegalStateException("you may only call next() if hasNext() is true");
1746            }
1747
1748            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
1749
1750            final long contactId = c.getLong(COLUMN_CONTACT_ID);
1751
1752            // we expect the cursor is already at the row we need to read from
1753            ContentValues contactValues = new ContentValues();
1754            contactValues.put(Contacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
1755            contactValues.put(Contacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
1756            contactValues.put(Contacts._ID, contactId);
1757            contactValues.put(Contacts.DIRTY, c.getLong(COLUMN_DIRTY));
1758            contactValues.put(Contacts.VERSION, c.getLong(COLUMN_VERSION));
1759            contactValues.put(Contacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
1760            Entity contact = new Entity(contactValues);
1761
1762            // read data rows until the contact id changes
1763            do {
1764                if (contactId != c.getLong(COLUMN_CONTACT_ID)) {
1765                    break;
1766                }
1767                // add the data to to the contact
1768                ContentValues dataValues = new ContentValues();
1769                dataValues.put(Contacts.Data._ID, c.getString(COLUMN_DATA_ID));
1770                dataValues.put(Contacts.Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
1771                dataValues.put(Contacts.Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY));
1772                dataValues.put(Contacts.Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
1773                for (int i = 0; i < 10; i++) {
1774                    final int columnIndex = i + COLUMN_DATA1;
1775                    String key = DATA_KEYS[i];
1776                    if (c.isNull(columnIndex)) {
1777                        // don't put anything
1778                    } else if (c.isLong(columnIndex)) {
1779                        dataValues.put(key, c.getLong(columnIndex));
1780                    } else if (c.isFloat(columnIndex)) {
1781                        dataValues.put(key, c.getFloat(columnIndex));
1782                    } else if (c.isString(columnIndex)) {
1783                        dataValues.put(key, c.getString(columnIndex));
1784                    } else if (c.isBlob(columnIndex)) {
1785                        dataValues.put(key, c.getBlob(columnIndex));
1786                    }
1787                }
1788                contact.addSubValue(Data.CONTENT_URI, dataValues);
1789            } while (mEntityCursor.moveToNext());
1790
1791            return contact;
1792        }
1793    }
1794
1795    @Override
1796    public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
1797            String sortOrder) {
1798        final int match = sUriMatcher.match(uri);
1799        switch (match) {
1800            case CONTACTS:
1801            case CONTACTS_ID:
1802                String contactsIdString = null;
1803                if (match == CONTACTS_ID) {
1804                    contactsIdString = uri.getPathSegments().get(1);
1805                }
1806
1807                return new ContactsEntityIterator(this, contactsIdString,
1808                        uri, selection, selectionArgs, sortOrder);
1809            default:
1810                throw new UnsupportedOperationException("Unknown uri: " + uri);
1811        }
1812    }
1813
1814    @Override
1815    public String getType(Uri uri) {
1816        final int match = sUriMatcher.match(uri);
1817        switch (match) {
1818            case AGGREGATES: return Aggregates.CONTENT_TYPE;
1819            case AGGREGATES_ID: return Aggregates.CONTENT_ITEM_TYPE;
1820            case CONTACTS: return Contacts.CONTENT_TYPE;
1821            case CONTACTS_ID: return Contacts.CONTENT_ITEM_TYPE;
1822            case DATA_ID:
1823                final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1824                long dataId = ContentUris.parseId(uri);
1825                return mOpenHelper.getDataMimeType(dataId);
1826            case AGGREGATION_EXCEPTIONS: return AggregationExceptions.CONTENT_TYPE;
1827            case AGGREGATION_EXCEPTION_ID: return AggregationExceptions.CONTENT_ITEM_TYPE;
1828            case AGGREGATION_SUGGESTIONS: return Aggregates.CONTENT_TYPE;
1829        }
1830        throw new UnsupportedOperationException("Unknown uri: " + uri);
1831    }
1832
1833    @Override
1834    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
1835            throws OperationApplicationException {
1836
1837        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1838        db.beginTransaction();
1839        try {
1840            ContentProviderResult[] results = super.applyBatch(operations);
1841            db.setTransactionSuccessful();
1842            return results;
1843        } finally {
1844            db.endTransaction();
1845        }
1846    }
1847
1848    /*
1849     * Sets the given dataId record in the "data" table to primary, and resets all data records of
1850     * the same mimetype and under the same contact to not be primary.
1851     *
1852     * @param dataId the id of the data record to be set to primary.
1853     */
1854    private void setIsPrimary(long dataId) {
1855        mSetPrimaryStatement.bindLong(1, dataId);
1856        mSetPrimaryStatement.bindLong(2, dataId);
1857        mSetPrimaryStatement.bindLong(3, dataId);
1858        mSetPrimaryStatement.execute();
1859    }
1860
1861    /*
1862     * Sets the given dataId record in the "data" table to "super primary", and resets all data
1863     * records of the same mimetype and under the same aggregate to not be "super primary".
1864     *
1865     * @param dataId the id of the data record to be set to primary.
1866     */
1867    private void setIsSuperPrimary(long dataId) {
1868        mSetSuperPrimaryStatement.bindLong(1, dataId);
1869        mSetSuperPrimaryStatement.bindLong(2, dataId);
1870        mSetSuperPrimaryStatement.bindLong(3, dataId);
1871        mSetSuperPrimaryStatement.execute();
1872
1873        // Find the parent aggregate and package for this new primary
1874        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1875
1876        long aggId = -1;
1877        long packageId = -1;
1878        boolean isRestricted = false;
1879        String mimeType = null;
1880
1881        Cursor cursor = null;
1882        try {
1883            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES,
1884                    Projections.PROJ_DATA_CONTACTS, DataColumns.CONCRETE_ID + "=" + dataId, null,
1885                    null, null, null);
1886            if (cursor.moveToFirst()) {
1887                aggId = cursor.getLong(Projections.COL_AGGREGATE_ID);
1888                packageId = cursor.getLong(Projections.COL_PACKAGE_ID);
1889                isRestricted = (cursor.getInt(Projections.COL_IS_RESTRICTED) == 1);
1890                mimeType = cursor.getString(Projections.COL_MIMETYPE);
1891            }
1892        } finally {
1893            if (cursor != null) {
1894                cursor.close();
1895            }
1896        }
1897
1898        // Bypass aggregate update if no parent found, or if we don't keep track
1899        // of super-primary for this mimetype.
1900        if (aggId == -1) {
1901            return;
1902        }
1903
1904        boolean isPhone = CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType);
1905        boolean isEmail = CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType);
1906
1907        // Record this value as the new primary for the parent aggregate
1908        final ContentValues values = new ContentValues();
1909        if (isPhone) {
1910            values.put(AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID, dataId);
1911            values.put(AggregatesColumns.OPTIMAL_PRIMARY_PHONE_PACKAGE_ID, packageId);
1912        } else if (isEmail) {
1913            values.put(AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID, dataId);
1914            values.put(AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_PACKAGE_ID, packageId);
1915        }
1916
1917        // If this data is unrestricted, then also set as fallback
1918        if (!isRestricted && isPhone) {
1919            values.put(AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID, dataId);
1920        } else if (!isRestricted && isEmail) {
1921            values.put(AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID, dataId);
1922        }
1923
1924        // Push update into aggregates table, if needed
1925        if (values.size() > 0) {
1926            db.update(Tables.AGGREGATES, values, Aggregates._ID + "=" + aggId, null);
1927        }
1928
1929    }
1930
1931    private String buildAggregateLookupWhereClause(String filterParam) {
1932        StringBuilder filter = new StringBuilder();
1933        filter.append(Tables.AGGREGATES);
1934        filter.append(".");
1935        filter.append(Aggregates._ID);
1936        filter.append(" IN (SELECT ");
1937        filter.append(Contacts.AGGREGATE_ID);
1938        filter.append(" FROM ");
1939        filter.append(Tables.CONTACTS);
1940        filter.append(" WHERE ");
1941        filter.append(Contacts._ID);
1942        filter.append(" IN (SELECT  contact_id FROM name_lookup WHERE normalized_name GLOB '");
1943        // NOTE: Query parameters won't work here since the SQL compiler
1944        // needs to parse the actual string to know that it can use the
1945        // index to do a prefix scan.
1946        filter.append(NameNormalizer.normalize(filterParam) + "*");
1947        filter.append("'))");
1948        return filter.toString();
1949    }
1950
1951    private String[] appendGroupArg(String[] selectionArgs, String arg) {
1952        if (selectionArgs == null) {
1953            return new String[] {arg};
1954        } else {
1955            int newLength = selectionArgs.length + 1;
1956            String[] newSelectionArgs = new String[newLength];
1957            System.arraycopy(selectionArgs, 0, newSelectionArgs, 0, selectionArgs.length);
1958            newSelectionArgs[newLength - 1] = arg;
1959            return newSelectionArgs;
1960        }
1961    }
1962}
1963