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