ContactsProvider2.java revision 2149ab82f021c204618d0d3644e261fd7a8d8490
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.providers.contacts;
18
19import com.android.internal.content.SyncStateContentProviderHelper;
20import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
21import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
22import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
23import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
24import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
25import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
26import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
27import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
28import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
29import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
30import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
31import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
32import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
33import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
34import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
35import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
36import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
37import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
38import com.google.android.collect.Lists;
39import com.google.android.collect.Maps;
40import com.google.android.collect.Sets;
41
42import android.accounts.Account;
43import android.accounts.AccountManager;
44import android.accounts.OnAccountsUpdateListener;
45import android.app.Notification;
46import android.app.NotificationManager;
47import android.app.PendingIntent;
48import android.app.SearchManager;
49import android.content.ContentProviderOperation;
50import android.content.ContentProviderResult;
51import android.content.ContentResolver;
52import android.content.ContentUris;
53import android.content.ContentValues;
54import android.content.Context;
55import android.content.IContentService;
56import android.content.Intent;
57import android.content.OperationApplicationException;
58import android.content.SharedPreferences;
59import android.content.SyncAdapterType;
60import android.content.UriMatcher;
61import android.content.res.AssetFileDescriptor;
62import android.content.res.Configuration;
63import android.database.CharArrayBuffer;
64import android.database.Cursor;
65import android.database.CursorWrapper;
66import android.database.DatabaseUtils;
67import android.database.MatrixCursor;
68import android.database.MatrixCursor.RowBuilder;
69import android.database.sqlite.SQLiteConstraintException;
70import android.database.sqlite.SQLiteContentHelper;
71import android.database.sqlite.SQLiteDatabase;
72import android.database.sqlite.SQLiteQueryBuilder;
73import android.database.sqlite.SQLiteStatement;
74import android.net.Uri;
75import android.os.AsyncTask;
76import android.os.Bundle;
77import android.os.MemoryFile;
78import android.os.RemoteException;
79import android.os.SystemProperties;
80import android.pim.vcard.VCardComposer;
81import android.pim.vcard.VCardConfig;
82import android.preference.PreferenceManager;
83import android.provider.BaseColumns;
84import android.provider.ContactsContract;
85import android.provider.LiveFolders;
86import android.provider.OpenableColumns;
87import android.provider.SyncStateContract;
88import android.provider.ContactsContract.AggregationExceptions;
89import android.provider.ContactsContract.ContactCounts;
90import android.provider.ContactsContract.Contacts;
91import android.provider.ContactsContract.Data;
92import android.provider.ContactsContract.DisplayNameSources;
93import android.provider.ContactsContract.FullNameStyle;
94import android.provider.ContactsContract.Groups;
95import android.provider.ContactsContract.Intents;
96import android.provider.ContactsContract.PhoneLookup;
97import android.provider.ContactsContract.PhoneticNameStyle;
98import android.provider.ContactsContract.ProviderStatus;
99import android.provider.ContactsContract.RawContacts;
100import android.provider.ContactsContract.SearchSnippetColumns;
101import android.provider.ContactsContract.Settings;
102import android.provider.ContactsContract.StatusUpdates;
103import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
104import android.provider.ContactsContract.CommonDataKinds.Email;
105import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
106import android.provider.ContactsContract.CommonDataKinds.Im;
107import android.provider.ContactsContract.CommonDataKinds.Nickname;
108import android.provider.ContactsContract.CommonDataKinds.Organization;
109import android.provider.ContactsContract.CommonDataKinds.Phone;
110import android.provider.ContactsContract.CommonDataKinds.Photo;
111import android.provider.ContactsContract.CommonDataKinds.StructuredName;
112import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
113import android.telephony.PhoneNumberUtils;
114import android.text.TextUtils;
115import android.util.Log;
116
117import java.io.ByteArrayOutputStream;
118import java.io.FileNotFoundException;
119import java.io.IOException;
120import java.io.OutputStream;
121import java.text.SimpleDateFormat;
122import java.util.ArrayList;
123import java.util.Collections;
124import java.util.Date;
125import java.util.HashMap;
126import java.util.HashSet;
127import java.util.List;
128import java.util.Locale;
129import java.util.Map;
130import java.util.Set;
131import java.util.concurrent.CountDownLatch;
132
133/**
134 * Contacts content provider. The contract between this provider and applications
135 * is defined in {@link ContactsContract}.
136 */
137public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {
138
139    private static final String TAG = "ContactsProvider";
140
141    private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
142
143    // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
144    // TODO: check for restricted flag during insert(), update(), and delete() calls
145
146    /** Default for the maximum number of returned aggregation suggestions. */
147    private static final int DEFAULT_MAX_SUGGESTIONS = 5;
148
149    /**
150     * Property key for the legacy contact import version. The need for a version
151     * as opposed to a boolean flag is that if we discover bugs in the contact import process,
152     * we can trigger re-import by incrementing the import version.
153     */
154    private static final String PROPERTY_CONTACTS_IMPORTED = "contacts_imported_v1";
155    private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1;
156    private static final String PREF_LOCALE = "locale";
157
158    private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
159
160    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
161
162    private static final String TIMES_CONTACED_SORT_COLUMN = "times_contacted_sort";
163
164    private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
165            + TIMES_CONTACED_SORT_COLUMN + " DESC, "
166            + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
167    private static final String STREQUENT_LIMIT =
168            "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
169            + Contacts.STARRED + "=1) + 25";
170
171    /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
172            "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
173            " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
174            " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?";
175
176    /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
177            "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
178            " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
179            " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?";
180
181    /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
182
183    private static final int CONTACTS = 1000;
184    private static final int CONTACTS_ID = 1001;
185    private static final int CONTACTS_LOOKUP = 1002;
186    private static final int CONTACTS_LOOKUP_ID = 1003;
187    private static final int CONTACTS_DATA = 1004;
188    private static final int CONTACTS_FILTER = 1005;
189    private static final int CONTACTS_STREQUENT = 1006;
190    private static final int CONTACTS_STREQUENT_FILTER = 1007;
191    private static final int CONTACTS_GROUP = 1008;
192    private static final int CONTACTS_PHOTO = 1009;
193    private static final int CONTACTS_AS_VCARD = 1010;
194    private static final int CONTACTS_AS_MULTI_VCARD = 1011;
195    private static final int CONTACTS_LOOKUP_DATA = 1012;
196    private static final int CONTACTS_LOOKUP_ID_DATA = 1013;
197
198    private static final int RAW_CONTACTS = 2002;
199    private static final int RAW_CONTACTS_ID = 2003;
200    private static final int RAW_CONTACTS_DATA = 2004;
201    private static final int RAW_CONTACT_ENTITY_ID = 2005;
202
203    private static final int DATA = 3000;
204    private static final int DATA_ID = 3001;
205    private static final int PHONES = 3002;
206    private static final int PHONES_ID = 3003;
207    private static final int PHONES_FILTER = 3004;
208    private static final int EMAILS = 3005;
209    private static final int EMAILS_ID = 3006;
210    private static final int EMAILS_LOOKUP = 3007;
211    private static final int EMAILS_FILTER = 3008;
212    private static final int POSTALS = 3009;
213    private static final int POSTALS_ID = 3010;
214
215    private static final int PHONE_LOOKUP = 4000;
216
217    private static final int AGGREGATION_EXCEPTIONS = 6000;
218    private static final int AGGREGATION_EXCEPTION_ID = 6001;
219
220    private static final int STATUS_UPDATES = 7000;
221    private static final int STATUS_UPDATES_ID = 7001;
222
223    private static final int AGGREGATION_SUGGESTIONS = 8000;
224
225    private static final int SETTINGS = 9000;
226
227    private static final int GROUPS = 10000;
228    private static final int GROUPS_ID = 10001;
229    private static final int GROUPS_SUMMARY = 10003;
230
231    private static final int SYNCSTATE = 11000;
232    private static final int SYNCSTATE_ID = 11001;
233
234    private static final int SEARCH_SUGGESTIONS = 12001;
235    private static final int SEARCH_SHORTCUT = 12002;
236
237    private static final int LIVE_FOLDERS_CONTACTS = 14000;
238    private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001;
239    private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002;
240    private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003;
241
242    private static final int RAW_CONTACT_ENTITIES = 15001;
243
244    private static final int PROVIDER_STATUS = 16001;
245
246    private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
247            RawContactsColumns.CONCRETE_ID + "=? AND "
248                    + GroupsColumns.CONCRETE_ACCOUNT_NAME
249                    + "=" + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
250                    + GroupsColumns.CONCRETE_ACCOUNT_TYPE
251                    + "=" + RawContactsColumns.CONCRETE_ACCOUNT_TYPE
252                    + " AND " + Groups.FAVORITES + " != 0";
253
254    private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID =
255            RawContactsColumns.CONCRETE_ID + "=? AND "
256                    + GroupsColumns.CONCRETE_ACCOUNT_NAME + "="
257                    + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
258                    + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
259                    + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND "
260                    + Groups.AUTO_ADD + " != 0";
261
262    private static final String[] PROJECTION_GROUP_ID
263            = new String[]{Tables.GROUPS + "." + Groups._ID};
264
265    private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? "
266            + "AND " + GroupMembership.GROUP_ROW_ID + "=? "
267            + "AND " + GroupMembership.RAW_CONTACT_ID + "=?";
268
269    private static final String SELECTION_STARRED_FROM_RAW_CONTACTS =
270            "SELECT " + RawContacts.STARRED
271                    + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?";
272
273    private interface DataContactsQuery {
274        public static final String TABLE = "data "
275                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
276                + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
277
278        public static final String[] PROJECTION = new String[] {
279            RawContactsColumns.CONCRETE_ID,
280            DataColumns.CONCRETE_ID,
281            ContactsColumns.CONCRETE_ID
282        };
283
284        public static final int RAW_CONTACT_ID = 0;
285        public static final int DATA_ID = 1;
286        public static final int CONTACT_ID = 2;
287    }
288
289    private interface DataDeleteQuery {
290        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
291
292        public static final String[] CONCRETE_COLUMNS = new String[] {
293            DataColumns.CONCRETE_ID,
294            MimetypesColumns.MIMETYPE,
295            Data.RAW_CONTACT_ID,
296            Data.IS_PRIMARY,
297            Data.DATA1,
298        };
299
300        public static final String[] COLUMNS = new String[] {
301            Data._ID,
302            MimetypesColumns.MIMETYPE,
303            Data.RAW_CONTACT_ID,
304            Data.IS_PRIMARY,
305            Data.DATA1,
306        };
307
308        public static final int _ID = 0;
309        public static final int MIMETYPE = 1;
310        public static final int RAW_CONTACT_ID = 2;
311        public static final int IS_PRIMARY = 3;
312        public static final int DATA1 = 4;
313    }
314
315    private interface DataUpdateQuery {
316        String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };
317
318        int _ID = 0;
319        int RAW_CONTACT_ID = 1;
320        int MIMETYPE = 2;
321    }
322
323
324    private interface RawContactsQuery {
325        String TABLE = Tables.RAW_CONTACTS;
326
327        String[] COLUMNS = new String[] {
328                RawContacts.DELETED,
329                RawContacts.ACCOUNT_TYPE,
330                RawContacts.ACCOUNT_NAME,
331        };
332
333        int DELETED = 0;
334        int ACCOUNT_TYPE = 1;
335        int ACCOUNT_NAME = 2;
336    }
337
338    public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
339    public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google";
340
341    /** Sql where statement for filtering on groups. */
342    private static final String CONTACTS_IN_GROUP_SELECT =
343            Contacts._ID + " IN "
344                    + "(SELECT " + RawContacts.CONTACT_ID
345                    + " FROM " + Tables.RAW_CONTACTS
346                    + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
347                            + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
348                            + " FROM " + Tables.DATA_JOIN_MIMETYPES
349                            + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
350                                    + "' AND " + GroupMembership.GROUP_ROW_ID + "="
351                                    + "(SELECT " + Tables.GROUPS + "." + Groups._ID
352                                    + " FROM " + Tables.GROUPS
353                                    + " WHERE " + Groups.TITLE + "=?)))";
354
355    /** Sql for updating DIRTY flag on multiple raw contacts */
356    private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
357            "UPDATE " + Tables.RAW_CONTACTS +
358            " SET " + RawContacts.DIRTY + "=1" +
359            " WHERE " + RawContacts._ID + " IN (";
360
361    /** Sql for updating VERSION on multiple raw contacts */
362    private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
363            "UPDATE " + Tables.RAW_CONTACTS +
364            " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
365            " WHERE " + RawContacts._ID + " IN (";
366
367    /** Name lookup types used for contact filtering */
368    private static final String CONTACT_LOOKUP_NAME_TYPES =
369            NameLookupType.NAME_COLLATION_KEY + "," +
370            NameLookupType.EMAIL_BASED_NICKNAME + "," +
371            NameLookupType.NICKNAME + "," +
372            NameLookupType.NAME_SHORTHAND + "," +
373            NameLookupType.ORGANIZATION + "," +
374            NameLookupType.NAME_CONSONANTS;
375
376
377    /** Contains just BaseColumns._COUNT */
378    private static final HashMap<String, String> sCountProjectionMap;
379    /** Contains just the contacts columns */
380    private static final HashMap<String, String> sContactsProjectionMap;
381    /** Contains just the contacts columns */
382    private static final HashMap<String, String> sContactsProjectionWithSnippetMap;
383
384    /** Used for pushing starred contacts to the top of a times contacted list **/
385    private static final HashMap<String, String> sStrequentStarredProjectionMap;
386    private static final HashMap<String, String> sStrequentFrequentProjectionMap;
387    /** Contains just the contacts vCard columns */
388    private static final HashMap<String, String> sContactsVCardProjectionMap;
389    /** Contains just the raw contacts columns */
390    private static final HashMap<String, String> sRawContactsProjectionMap;
391    /** Contains the columns from the raw contacts entity view*/
392    private static final HashMap<String, String> sRawContactsEntityProjectionMap;
393    /** Contains columns from the data view */
394    private static final HashMap<String, String> sDataProjectionMap;
395    /** Contains columns from the data view */
396    private static final HashMap<String, String> sDistinctDataProjectionMap;
397    /** Contains the data and contacts columns, for joined tables */
398    private static final HashMap<String, String> sPhoneLookupProjectionMap;
399    /** Contains the just the {@link Groups} columns */
400    private static final HashMap<String, String> sGroupsProjectionMap;
401    /** Contains {@link Groups} columns along with summary details */
402    private static final HashMap<String, String> sGroupsSummaryProjectionMap;
403    /** Contains the agg_exceptions columns */
404    private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
405    /** Contains the agg_exceptions columns */
406    private static final HashMap<String, String> sSettingsProjectionMap;
407    /** Contains StatusUpdates columns */
408    private static final HashMap<String, String> sStatusUpdatesProjectionMap;
409    /** Contains Live Folders columns */
410    private static final HashMap<String, String> sLiveFoldersProjectionMap;
411
412    // where clause to update the status_updates table
413    private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
414            StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
415            " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
416            " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
417
418    private static final String[] EMPTY_STRING_ARRAY = new String[0];
419
420    /**
421     * Notification ID for failure to import contacts.
422     */
423    private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1;
424
425    /** Precompiled sql statement for setting a data record to the primary. */
426    private SQLiteStatement mSetPrimaryStatement;
427    /** Precompiled sql statement for setting a data record to the super primary. */
428    private SQLiteStatement mSetSuperPrimaryStatement;
429    /** Precompiled sql statement for updating a contact display name */
430    private SQLiteStatement mRawContactDisplayNameUpdate;
431    /** Precompiled sql statement for updating an aggregated status update */
432    private SQLiteStatement mLastStatusUpdate;
433    private SQLiteStatement mNameLookupInsert;
434    private SQLiteStatement mNameLookupDelete;
435    private SQLiteStatement mStatusUpdateAutoTimestamp;
436    private SQLiteStatement mStatusUpdateInsert;
437    private SQLiteStatement mStatusUpdateReplace;
438    private SQLiteStatement mStatusAttributionUpdate;
439    private SQLiteStatement mStatusUpdateDelete;
440    private SQLiteStatement mResetNameVerifiedForOtherRawContacts;
441
442    private long mMimeTypeIdEmail;
443    private long mMimeTypeIdIm;
444    private long mMimeTypeIdStructuredName;
445    private long mMimeTypeIdOrganization;
446    private long mMimeTypeIdNickname;
447    private long mMimeTypeIdPhone;
448    private StringBuilder mSb = new StringBuilder();
449    private String[] mSelectionArgs1 = new String[1];
450    private String[] mSelectionArgs2 = new String[2];
451    private ArrayList<String> mSelectionArgs = Lists.newArrayList();
452
453    private Account mAccount;
454
455    static {
456        // Contacts URI matching table
457        final UriMatcher matcher = sUriMatcher;
458        matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
459        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
460        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
461        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
462                AGGREGATION_SUGGESTIONS);
463        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
464                AGGREGATION_SUGGESTIONS);
465        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO);
466        matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER);
467        matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
468        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
469        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA);
470        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
471        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data",
472                CONTACTS_LOOKUP_ID_DATA);
473        matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
474        matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
475                CONTACTS_AS_MULTI_VCARD);
476        matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT);
477        matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*",
478                CONTACTS_STREQUENT_FILTER);
479        matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP);
480
481        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
482        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
483        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
484        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID);
485
486        matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
487
488        matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
489        matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
490        matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
491        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
492        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
493        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
494        matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
495        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
496        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
497        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
498        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
499        matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
500        matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
501
502        matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
503        matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
504        matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
505
506        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
507        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#",
508                SYNCSTATE_ID);
509
510        matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
511        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
512                AGGREGATION_EXCEPTIONS);
513        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
514                AGGREGATION_EXCEPTION_ID);
515
516        matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);
517
518        matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
519        matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID);
520
521        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
522                SEARCH_SUGGESTIONS);
523        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
524                SEARCH_SUGGESTIONS);
525        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
526                SEARCH_SHORTCUT);
527
528        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts",
529                LIVE_FOLDERS_CONTACTS);
530        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*",
531                LIVE_FOLDERS_CONTACTS_GROUP_NAME);
532        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones",
533                LIVE_FOLDERS_CONTACTS_WITH_PHONES);
534        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites",
535                LIVE_FOLDERS_CONTACTS_FAVORITES);
536
537        matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
538    }
539
540    static {
541        sCountProjectionMap = new HashMap<String, String>();
542        sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)");
543
544        sContactsProjectionMap = new HashMap<String, String>();
545        sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
546        sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY);
547        sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
548                Contacts.DISPLAY_NAME_ALTERNATIVE);
549        sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
550        sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
551        sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
552        sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
553        sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
554        sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
555        sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
556        sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
557        sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
558        sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
559        sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
560        sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER);
561        sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
562        sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
563        sContactsProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID);
564
565        // Handle projections for Contacts-level statuses
566        addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE,
567                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
568        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS,
569                ContactsStatusUpdatesColumns.CONCRETE_STATUS);
570        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
571                ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
572        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
573                ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
574        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL,
575                ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
576        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON,
577                ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
578
579        sContactsProjectionWithSnippetMap = new HashMap<String, String>();
580        sContactsProjectionWithSnippetMap.putAll(sContactsProjectionMap);
581        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_MIMETYPE,
582                SearchSnippetColumns.SNIPPET_MIMETYPE);
583        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA_ID,
584                SearchSnippetColumns.SNIPPET_DATA_ID);
585        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA1,
586                SearchSnippetColumns.SNIPPET_DATA1);
587        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA2,
588                SearchSnippetColumns.SNIPPET_DATA2);
589        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA3,
590                SearchSnippetColumns.SNIPPET_DATA3);
591        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA4,
592                SearchSnippetColumns.SNIPPET_DATA4);
593
594        sStrequentStarredProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
595        sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
596                  Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN);
597
598        sStrequentFrequentProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
599        sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
600                  Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN);
601
602        sContactsVCardProjectionMap = Maps.newHashMap();
603        sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME
604                + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME);
605        sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "NULL AS " + OpenableColumns.SIZE);
606
607        sRawContactsProjectionMap = new HashMap<String, String>();
608        sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
609        sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
610        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
611        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
612        sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
613        sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
614        sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
615        sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
616        sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY,
617                RawContacts.DISPLAY_NAME_PRIMARY);
618        sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE,
619                RawContacts.DISPLAY_NAME_ALTERNATIVE);
620        sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE,
621                RawContacts.DISPLAY_NAME_SOURCE);
622        sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME,
623                RawContacts.PHONETIC_NAME);
624        sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE,
625                RawContacts.PHONETIC_NAME_STYLE);
626        sRawContactsProjectionMap.put(RawContacts.NAME_VERIFIED,
627                RawContacts.NAME_VERIFIED);
628        sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY,
629                RawContacts.SORT_KEY_PRIMARY);
630        sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE,
631                RawContacts.SORT_KEY_ALTERNATIVE);
632        sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
633        sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
634                RawContacts.LAST_TIME_CONTACTED);
635        sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE);
636        sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL);
637        sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED);
638        sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE);
639        sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1);
640        sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2);
641        sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3);
642        sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4);
643
644        sDataProjectionMap = new HashMap<String, String>();
645        sDataProjectionMap.put(Data._ID, Data._ID);
646        sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
647        sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
648        sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
649        sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
650        sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
651        sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
652        sDataProjectionMap.put(Data.DATA1, Data.DATA1);
653        sDataProjectionMap.put(Data.DATA2, Data.DATA2);
654        sDataProjectionMap.put(Data.DATA3, Data.DATA3);
655        sDataProjectionMap.put(Data.DATA4, Data.DATA4);
656        sDataProjectionMap.put(Data.DATA5, Data.DATA5);
657        sDataProjectionMap.put(Data.DATA6, Data.DATA6);
658        sDataProjectionMap.put(Data.DATA7, Data.DATA7);
659        sDataProjectionMap.put(Data.DATA8, Data.DATA8);
660        sDataProjectionMap.put(Data.DATA9, Data.DATA9);
661        sDataProjectionMap.put(Data.DATA10, Data.DATA10);
662        sDataProjectionMap.put(Data.DATA11, Data.DATA11);
663        sDataProjectionMap.put(Data.DATA12, Data.DATA12);
664        sDataProjectionMap.put(Data.DATA13, Data.DATA13);
665        sDataProjectionMap.put(Data.DATA14, Data.DATA14);
666        sDataProjectionMap.put(Data.DATA15, Data.DATA15);
667        sDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
668        sDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
669        sDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
670        sDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
671        sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID);
672        sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
673        sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
674        sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
675        sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
676        sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
677        sDataProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
678        sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
679        sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
680        sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
681                Contacts.DISPLAY_NAME_ALTERNATIVE);
682        sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
683        sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
684        sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
685        sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
686        sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
687        sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
688        sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
689        sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
690        sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
691        sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
692        sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
693        sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
694        sDataProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID);
695        sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
696
697        HashMap<String, String> columns;
698        columns = new HashMap<String, String>();
699        columns.put(RawContacts._ID, RawContacts._ID);
700        columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
701        columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
702        columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
703        columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
704        columns.put(RawContacts.VERSION, RawContacts.VERSION);
705        columns.put(RawContacts.DIRTY, RawContacts.DIRTY);
706        columns.put(RawContacts.DELETED, RawContacts.DELETED);
707        columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED);
708        columns.put(RawContacts.SYNC1, RawContacts.SYNC1);
709        columns.put(RawContacts.SYNC2, RawContacts.SYNC2);
710        columns.put(RawContacts.SYNC3, RawContacts.SYNC3);
711        columns.put(RawContacts.SYNC4, RawContacts.SYNC4);
712        columns.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
713        columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
714        columns.put(Data.MIMETYPE, Data.MIMETYPE);
715        columns.put(Data.DATA1, Data.DATA1);
716        columns.put(Data.DATA2, Data.DATA2);
717        columns.put(Data.DATA3, Data.DATA3);
718        columns.put(Data.DATA4, Data.DATA4);
719        columns.put(Data.DATA5, Data.DATA5);
720        columns.put(Data.DATA6, Data.DATA6);
721        columns.put(Data.DATA7, Data.DATA7);
722        columns.put(Data.DATA8, Data.DATA8);
723        columns.put(Data.DATA9, Data.DATA9);
724        columns.put(Data.DATA10, Data.DATA10);
725        columns.put(Data.DATA11, Data.DATA11);
726        columns.put(Data.DATA12, Data.DATA12);
727        columns.put(Data.DATA13, Data.DATA13);
728        columns.put(Data.DATA14, Data.DATA14);
729        columns.put(Data.DATA15, Data.DATA15);
730        columns.put(Data.SYNC1, Data.SYNC1);
731        columns.put(Data.SYNC2, Data.SYNC2);
732        columns.put(Data.SYNC3, Data.SYNC3);
733        columns.put(Data.SYNC4, Data.SYNC4);
734        columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID);
735        columns.put(Data.STARRED, Data.STARRED);
736        columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
737        columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
738        columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
739        columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
740        sRawContactsEntityProjectionMap = columns;
741
742        // Handle projections for Contacts-level statuses
743        addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE,
744                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
745        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS,
746                ContactsStatusUpdatesColumns.CONCRETE_STATUS);
747        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
748                ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
749        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
750                ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
751        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
752                ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
753        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
754                ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
755
756        // Handle projections for Data-level statuses
757        addProjection(sDataProjectionMap, Data.PRESENCE,
758                Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
759        addProjection(sDataProjectionMap, Data.STATUS,
760                StatusUpdatesColumns.CONCRETE_STATUS);
761        addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP,
762                StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
763        addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE,
764                StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
765        addProjection(sDataProjectionMap, Data.STATUS_LABEL,
766                StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
767        addProjection(sDataProjectionMap, Data.STATUS_ICON,
768                StatusUpdatesColumns.CONCRETE_STATUS_ICON);
769
770        // Projection map for data grouped by contact (not raw contact) and some data field(s)
771        sDistinctDataProjectionMap = new HashMap<String, String>();
772        sDistinctDataProjectionMap.put(Data._ID,
773                "MIN(" + Data._ID + ") AS " + Data._ID);
774        sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
775        sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
776        sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
777        sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
778        sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
779        sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1);
780        sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2);
781        sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3);
782        sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4);
783        sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5);
784        sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6);
785        sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7);
786        sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8);
787        sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9);
788        sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10);
789        sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11);
790        sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12);
791        sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13);
792        sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14);
793        sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15);
794        sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
795        sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
796        sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
797        sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
798        sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
799        sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
800        sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
801        sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
802                Contacts.DISPLAY_NAME_ALTERNATIVE);
803        sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
804        sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
805        sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
806        sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
807        sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE,
808                Contacts.SORT_KEY_ALTERNATIVE);
809        sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
810        sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
811        sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
812        sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
813        sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
814        sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
815        sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
816        sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID,
817                GroupMembership.GROUP_SOURCE_ID);
818
819        // Handle projections for Contacts-level statuses
820        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE,
821                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
822        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS,
823                ContactsStatusUpdatesColumns.CONCRETE_STATUS);
824        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
825                ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
826        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
827                ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
828        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
829                ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
830        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
831                ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
832
833        // Handle projections for Data-level statuses
834        addProjection(sDistinctDataProjectionMap, Data.PRESENCE,
835                Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
836        addProjection(sDistinctDataProjectionMap, Data.STATUS,
837                StatusUpdatesColumns.CONCRETE_STATUS);
838        addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP,
839                StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
840        addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE,
841                StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
842        addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL,
843                StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
844        addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON,
845                StatusUpdatesColumns.CONCRETE_STATUS_ICON);
846
847        sPhoneLookupProjectionMap = new HashMap<String, String>();
848        sPhoneLookupProjectionMap.put(PhoneLookup._ID,
849                "contacts_view." + Contacts._ID
850                        + " AS " + PhoneLookup._ID);
851        sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY,
852                "contacts_view." + Contacts.LOOKUP_KEY
853                        + " AS " + PhoneLookup.LOOKUP_KEY);
854        sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
855                "contacts_view." + Contacts.DISPLAY_NAME
856                        + " AS " + PhoneLookup.DISPLAY_NAME);
857        sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
858                "contacts_view." + Contacts.LAST_TIME_CONTACTED
859                        + " AS " + PhoneLookup.LAST_TIME_CONTACTED);
860        sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
861                "contacts_view." + Contacts.TIMES_CONTACTED
862                        + " AS " + PhoneLookup.TIMES_CONTACTED);
863        sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
864                "contacts_view." + Contacts.STARRED
865                        + " AS " + PhoneLookup.STARRED);
866        sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
867                "contacts_view." + Contacts.IN_VISIBLE_GROUP
868                        + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
869        sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
870                "contacts_view." + Contacts.PHOTO_ID
871                        + " AS " + PhoneLookup.PHOTO_ID);
872        sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
873                "contacts_view." + Contacts.CUSTOM_RINGTONE
874                        + " AS " + PhoneLookup.CUSTOM_RINGTONE);
875        sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
876                "contacts_view." + Contacts.HAS_PHONE_NUMBER
877                        + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
878        sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
879                "contacts_view." + Contacts.SEND_TO_VOICEMAIL
880                        + " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
881        sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
882                Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
883        sPhoneLookupProjectionMap.put(PhoneLookup.TYPE,
884                Phone.TYPE + " AS " + PhoneLookup.TYPE);
885        sPhoneLookupProjectionMap.put(PhoneLookup.LABEL,
886                Phone.LABEL + " AS " + PhoneLookup.LABEL);
887
888        // Groups projection map
889        columns = new HashMap<String, String>();
890        columns.put(Groups._ID, Groups._ID);
891        columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
892        columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
893        columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
894        columns.put(Groups.DIRTY, Groups.DIRTY);
895        columns.put(Groups.VERSION, Groups.VERSION);
896        columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE);
897        columns.put(Groups.TITLE, Groups.TITLE);
898        columns.put(Groups.TITLE_RES, Groups.TITLE_RES);
899        columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
900        columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID);
901        columns.put(Groups.DELETED, Groups.DELETED);
902        columns.put(Groups.NOTES, Groups.NOTES);
903        columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC);
904        columns.put(Groups.FAVORITES, Groups.FAVORITES);
905        columns.put(Groups.AUTO_ADD, Groups.AUTO_ADD);
906        columns.put(Groups.SYNC1, Groups.SYNC1);
907        columns.put(Groups.SYNC2, Groups.SYNC2);
908        columns.put(Groups.SYNC3, Groups.SYNC3);
909        columns.put(Groups.SYNC4, Groups.SYNC4);
910        sGroupsProjectionMap = columns;
911
912        // RawContacts and groups projection map
913        columns = new HashMap<String, String>();
914        columns.putAll(sGroupsProjectionMap);
915        columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
916                + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
917                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
918                + ") AS " + Groups.SUMMARY_COUNT);
919        columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
920                + ContactsColumns.CONCRETE_ID + ") FROM "
921                + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
922                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
923                + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES);
924        sGroupsSummaryProjectionMap = columns;
925
926        // Aggregate exception projection map
927        columns = new HashMap<String, String>();
928        columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
929        columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
930        columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1);
931        columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2);
932        sAggregationExceptionsProjectionMap = columns;
933
934        // Settings projection map
935        columns = new HashMap<String, String>();
936        columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME);
937        columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE);
938        columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE);
939        columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC);
940        columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
941                + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN("
942                + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE "
943                + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME
944                + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
945                + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS "
946                + Settings.ANY_UNSYNCED);
947        columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
948                + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY "
949                + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS
950                + ")) AS " + Settings.UNGROUPED_COUNT);
951        columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
952                + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE "
953                + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
954                + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS "
955                + Settings.UNGROUPED_WITH_PHONES);
956        sSettingsProjectionMap = columns;
957
958        columns = new HashMap<String, String>();
959        columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID);
960        columns.put(StatusUpdates.DATA_ID,
961                DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID);
962        columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT);
963        columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE);
964        columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL);
965        // We cannot allow a null in the custom protocol field, because SQLite3 does not
966        // properly enforce uniqueness of null values
967        columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL
968                + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS "
969                + StatusUpdates.CUSTOM_PROTOCOL);
970        columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE);
971        columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS);
972        columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP);
973        columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE);
974        columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON);
975        columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL);
976        sStatusUpdatesProjectionMap = columns;
977
978        // Live folder projection
979        sLiveFoldersProjectionMap = new HashMap<String, String>();
980        sLiveFoldersProjectionMap.put(LiveFolders._ID,
981                Contacts._ID + " AS " + LiveFolders._ID);
982        sLiveFoldersProjectionMap.put(LiveFolders.NAME,
983                Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME);
984        // TODO: Put contact photo back when we have a way to display a default icon
985        // for contacts without a photo
986        // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP,
987        //      Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
988    }
989
990    private static void addProjection(HashMap<String, String> map, String toField, String fromField) {
991        map.put(toField, fromField + " AS " + toField);
992    }
993
994    /**
995     * Handles inserts and update for a specific Data type.
996     */
997    private abstract class DataRowHandler {
998
999        protected final String mMimetype;
1000        protected long mMimetypeId;
1001
1002        @SuppressWarnings("all")
1003        public DataRowHandler(String mimetype) {
1004            mMimetype = mimetype;
1005
1006            // To ensure the data column position. This is dead code if properly configured.
1007            if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1
1008                    || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
1009                    || Email.DATA != Data.DATA1) {
1010                throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
1011                        + " data is not in DATA1 column");
1012            }
1013        }
1014
1015        protected long getMimeTypeId() {
1016            if (mMimetypeId == 0) {
1017                mMimetypeId = mDbHelper.getMimeTypeId(mMimetype);
1018            }
1019            return mMimetypeId;
1020        }
1021
1022        /**
1023         * Inserts a row into the {@link Data} table.
1024         */
1025        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1026            final long dataId = db.insert(Tables.DATA, null, values);
1027
1028            Integer primary = values.getAsInteger(Data.IS_PRIMARY);
1029            if (primary != null && primary != 0) {
1030                setIsPrimary(rawContactId, dataId, getMimeTypeId());
1031            }
1032
1033            return dataId;
1034        }
1035
1036        /**
1037         * Validates data and updates a {@link Data} row using the cursor, which contains
1038         * the current data.
1039         *
1040         * @return true if update changed something
1041         */
1042        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1043                boolean callerIsSyncAdapter) {
1044            long dataId = c.getLong(DataUpdateQuery._ID);
1045            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1046
1047            if (values.containsKey(Data.IS_SUPER_PRIMARY)) {
1048                long mimeTypeId = getMimeTypeId();
1049                setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
1050                setIsPrimary(rawContactId, dataId, mimeTypeId);
1051
1052                // Now that we've taken care of setting these, remove them from "values".
1053                values.remove(Data.IS_SUPER_PRIMARY);
1054                values.remove(Data.IS_PRIMARY);
1055            } else if (values.containsKey(Data.IS_PRIMARY)) {
1056                setIsPrimary(rawContactId, dataId, getMimeTypeId());
1057
1058                // Now that we've taken care of setting this, remove it from "values".
1059                values.remove(Data.IS_PRIMARY);
1060            }
1061
1062            if (values.size() > 0) {
1063                mSelectionArgs1[0] = String.valueOf(dataId);
1064                mDb.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1);
1065            }
1066
1067            if (!callerIsSyncAdapter) {
1068                setRawContactDirty(rawContactId);
1069            }
1070
1071            return true;
1072        }
1073
1074        public int delete(SQLiteDatabase db, Cursor c) {
1075            long dataId = c.getLong(DataDeleteQuery._ID);
1076            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1077            boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
1078            mSelectionArgs1[0] = String.valueOf(dataId);
1079            int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
1080            mSelectionArgs1[0] = String.valueOf(rawContactId);
1081            db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
1082            if (count != 0 && primary) {
1083                fixPrimary(db, rawContactId);
1084            }
1085            return count;
1086        }
1087
1088        private void fixPrimary(SQLiteDatabase db, long rawContactId) {
1089            long mimeTypeId = getMimeTypeId();
1090            long primaryId = -1;
1091            int primaryType = -1;
1092            mSelectionArgs1[0] = String.valueOf(rawContactId);
1093            Cursor c = db.query(DataDeleteQuery.TABLE,
1094                    DataDeleteQuery.CONCRETE_COLUMNS,
1095                    Data.RAW_CONTACT_ID + "=?" +
1096                        " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
1097                    mSelectionArgs1, null, null, null);
1098            try {
1099                while (c.moveToNext()) {
1100                    long dataId = c.getLong(DataDeleteQuery._ID);
1101                    int type = c.getInt(DataDeleteQuery.DATA1);
1102                    if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
1103                        primaryId = dataId;
1104                        primaryType = type;
1105                    }
1106                }
1107            } finally {
1108                c.close();
1109            }
1110            if (primaryId != -1) {
1111                setIsPrimary(rawContactId, primaryId, mimeTypeId);
1112            }
1113        }
1114
1115        /**
1116         * Returns the rank of a specific record type to be used in determining the primary
1117         * row. Lower number represents higher priority.
1118         */
1119        protected int getTypeRank(int type) {
1120            return 0;
1121        }
1122
1123        protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
1124            if (!isNewRawContact(rawContactId)) {
1125                updateRawContactDisplayName(db, rawContactId);
1126                mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
1127            }
1128        }
1129
1130        /**
1131         * Return set of values, using current values at given {@link Data#_ID}
1132         * as baseline, but augmented with any updates.  Returns null if there is
1133         * no change.
1134         */
1135        public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
1136                ContentValues update) {
1137            boolean changing = false;
1138            final ContentValues values = new ContentValues();
1139            mSelectionArgs1[0] = String.valueOf(dataId);
1140            final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
1141                    mSelectionArgs1, null, null, null);
1142            try {
1143                if (cursor.moveToFirst()) {
1144                    for (int i = 0; i < cursor.getColumnCount(); i++) {
1145                        final String key = cursor.getColumnName(i);
1146                        final String value = cursor.getString(i);
1147                        if (!changing && update.containsKey(key)) {
1148                            Object newValue = update.get(key);
1149                            String newString = newValue == null ? null : newValue.toString();
1150                            changing |= !TextUtils.equals(newString, value);
1151                        }
1152                        values.put(key, value);
1153                    }
1154                }
1155            } finally {
1156                cursor.close();
1157            }
1158            if (!changing) {
1159                return null;
1160            }
1161
1162            values.putAll(update);
1163            return values;
1164        }
1165    }
1166
1167    public class CustomDataRowHandler extends DataRowHandler {
1168
1169        public CustomDataRowHandler(String mimetype) {
1170            super(mimetype);
1171        }
1172    }
1173
1174    public class StructuredNameRowHandler extends DataRowHandler {
1175        private final NameSplitter mSplitter;
1176
1177        public StructuredNameRowHandler(NameSplitter splitter) {
1178            super(StructuredName.CONTENT_ITEM_TYPE);
1179            mSplitter = splitter;
1180        }
1181
1182        @Override
1183        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1184            fixStructuredNameComponents(values, values);
1185
1186            long dataId = super.insert(db, rawContactId, values);
1187
1188            String name = values.getAsString(StructuredName.DISPLAY_NAME);
1189            Integer fullNameStyle = values.getAsInteger(StructuredName.FULL_NAME_STYLE);
1190            insertNameLookupForStructuredName(rawContactId, dataId, name,
1191                    fullNameStyle != null
1192                            ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle)
1193                            : FullNameStyle.UNDEFINED);
1194            insertNameLookupForPhoneticName(rawContactId, dataId, values);
1195            fixRawContactDisplayName(db, rawContactId);
1196            triggerAggregation(rawContactId);
1197            return dataId;
1198        }
1199
1200        @Override
1201        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1202                boolean callerIsSyncAdapter) {
1203            final long dataId = c.getLong(DataUpdateQuery._ID);
1204            final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1205
1206            final ContentValues augmented = getAugmentedValues(db, dataId, values);
1207            if (augmented == null) {  // No change
1208                return false;
1209            }
1210
1211            fixStructuredNameComponents(augmented, values);
1212
1213            super.update(db, values, c, callerIsSyncAdapter);
1214            if (values.containsKey(StructuredName.DISPLAY_NAME) ||
1215                    values.containsKey(StructuredName.PHONETIC_FAMILY_NAME) ||
1216                    values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME) ||
1217                    values.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) {
1218                augmented.putAll(values);
1219                String name = augmented.getAsString(StructuredName.DISPLAY_NAME);
1220                deleteNameLookup(dataId);
1221                Integer fullNameStyle = augmented.getAsInteger(StructuredName.FULL_NAME_STYLE);
1222                insertNameLookupForStructuredName(rawContactId, dataId, name,
1223                        fullNameStyle != null
1224                                ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle)
1225                                : FullNameStyle.UNDEFINED);
1226                insertNameLookupForPhoneticName(rawContactId, dataId, augmented);
1227            }
1228            fixRawContactDisplayName(db, rawContactId);
1229            triggerAggregation(rawContactId);
1230            return true;
1231        }
1232
1233        @Override
1234        public int delete(SQLiteDatabase db, Cursor c) {
1235            long dataId = c.getLong(DataDeleteQuery._ID);
1236            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1237
1238            int count = super.delete(db, c);
1239
1240            deleteNameLookup(dataId);
1241            fixRawContactDisplayName(db, rawContactId);
1242            triggerAggregation(rawContactId);
1243            return count;
1244        }
1245
1246        /**
1247         * Specific list of structured fields.
1248         */
1249        private final String[] STRUCTURED_FIELDS = new String[] {
1250                StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME,
1251                StructuredName.FAMILY_NAME, StructuredName.SUFFIX
1252        };
1253
1254        /**
1255         * Parses the supplied display name, but only if the incoming values do
1256         * not already contain structured name parts. Also, if the display name
1257         * is not provided, generate one by concatenating first name and last
1258         * name.
1259         */
1260        private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) {
1261            final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME);
1262
1263            final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
1264            final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);
1265
1266            if (touchedUnstruct && !touchedStruct) {
1267                NameSplitter.Name name = new NameSplitter.Name();
1268                mSplitter.split(name, unstruct);
1269                name.toValues(update);
1270            } else if (!touchedUnstruct
1271                    && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
1272                // We need to update the display name when any structured components
1273                // are specified, even when they are null, which is why we are checking
1274                // areAnySpecified.  The touchedStruct in the condition is an optimization:
1275                // if there are non-null values, we know for a fact that some values are present.
1276                NameSplitter.Name name = new NameSplitter.Name();
1277                name.fromValues(augmented);
1278                // As the name could be changed, let's guess the name style again.
1279                name.fullNameStyle = FullNameStyle.UNDEFINED;
1280                mSplitter.guessNameStyle(name);
1281                int unadjustedFullNameStyle = name.fullNameStyle;
1282                name.fullNameStyle = mSplitter.getAdjustedFullNameStyle(name.fullNameStyle);
1283                final String joined = mSplitter.join(name, true);
1284                update.put(StructuredName.DISPLAY_NAME, joined);
1285
1286                update.put(StructuredName.FULL_NAME_STYLE, unadjustedFullNameStyle);
1287                update.put(StructuredName.PHONETIC_NAME_STYLE, name.phoneticNameStyle);
1288            } else if (touchedUnstruct && touchedStruct){
1289                if (!update.containsKey(StructuredName.FULL_NAME_STYLE)) {
1290                    update.put(StructuredName.FULL_NAME_STYLE,
1291                            mSplitter.guessFullNameStyle(unstruct));
1292                }
1293                if (!update.containsKey(StructuredName.PHONETIC_NAME_STYLE)) {
1294                    update.put(StructuredName.PHONETIC_NAME_STYLE,
1295                            mSplitter.guessPhoneticNameStyle(unstruct));
1296                }
1297            }
1298        }
1299    }
1300
1301    public class StructuredPostalRowHandler extends DataRowHandler {
1302        private PostalSplitter mSplitter;
1303
1304        public StructuredPostalRowHandler(PostalSplitter splitter) {
1305            super(StructuredPostal.CONTENT_ITEM_TYPE);
1306            mSplitter = splitter;
1307        }
1308
1309        @Override
1310        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1311            fixStructuredPostalComponents(values, values);
1312            return super.insert(db, rawContactId, values);
1313        }
1314
1315        @Override
1316        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1317                boolean callerIsSyncAdapter) {
1318            final long dataId = c.getLong(DataUpdateQuery._ID);
1319            final ContentValues augmented = getAugmentedValues(db, dataId, values);
1320            if (augmented == null) {    // No change
1321                return false;
1322            }
1323
1324            fixStructuredPostalComponents(augmented, values);
1325            super.update(db, values, c, callerIsSyncAdapter);
1326            return true;
1327        }
1328
1329        /**
1330         * Specific list of structured fields.
1331         */
1332        private final String[] STRUCTURED_FIELDS = new String[] {
1333                StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD,
1334                StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE,
1335                StructuredPostal.COUNTRY,
1336        };
1337
1338        /**
1339         * Prepares the given {@link StructuredPostal} row, building
1340         * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured
1341         * values when missing. When structured components are missing, the
1342         * unstructured value is assigned to {@link StructuredPostal#STREET}.
1343         */
1344        private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) {
1345            final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS);
1346
1347            final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
1348            final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);
1349
1350            final PostalSplitter.Postal postal = new PostalSplitter.Postal();
1351
1352            if (touchedUnstruct && !touchedStruct) {
1353                mSplitter.split(postal, unstruct);
1354                postal.toValues(update);
1355            } else if (!touchedUnstruct
1356                    && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
1357                // See comment in
1358                postal.fromValues(augmented);
1359                final String joined = mSplitter.join(postal);
1360                update.put(StructuredPostal.FORMATTED_ADDRESS, joined);
1361            }
1362        }
1363    }
1364
1365    public class CommonDataRowHandler extends DataRowHandler {
1366
1367        private final String mTypeColumn;
1368        private final String mLabelColumn;
1369
1370        public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) {
1371            super(mimetype);
1372            mTypeColumn = typeColumn;
1373            mLabelColumn = labelColumn;
1374        }
1375
1376        @Override
1377        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1378            enforceTypeAndLabel(values, values);
1379            return super.insert(db, rawContactId, values);
1380        }
1381
1382        @Override
1383        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1384                boolean callerIsSyncAdapter) {
1385            final long dataId = c.getLong(DataUpdateQuery._ID);
1386            final ContentValues augmented = getAugmentedValues(db, dataId, values);
1387            if (augmented == null) {        // No change
1388                return false;
1389            }
1390            enforceTypeAndLabel(augmented, values);
1391            return super.update(db, values, c, callerIsSyncAdapter);
1392        }
1393
1394        /**
1395         * If the given {@link ContentValues} defines {@link #mTypeColumn},
1396         * enforce that {@link #mLabelColumn} only appears when type is
1397         * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise.
1398         */
1399        private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) {
1400            final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn));
1401            final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn));
1402
1403            if (hasLabel && !hasType) {
1404                // When label exists, assert that some type is defined
1405                throw new IllegalArgumentException(mTypeColumn + " must be specified when "
1406                        + mLabelColumn + " is defined.");
1407            }
1408        }
1409    }
1410
1411    public class OrganizationDataRowHandler extends CommonDataRowHandler {
1412
1413        public OrganizationDataRowHandler() {
1414            super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
1415        }
1416
1417        @Override
1418        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1419            String company = values.getAsString(Organization.COMPANY);
1420            String title = values.getAsString(Organization.TITLE);
1421
1422            long dataId = super.insert(db, rawContactId, values);
1423
1424            fixRawContactDisplayName(db, rawContactId);
1425            insertNameLookupForOrganization(rawContactId, dataId, company, title);
1426            return dataId;
1427        }
1428
1429        @Override
1430        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1431                boolean callerIsSyncAdapter) {
1432            if (!super.update(db, values, c, callerIsSyncAdapter)) {
1433                return false;
1434            }
1435
1436            boolean containsCompany = values.containsKey(Organization.COMPANY);
1437            boolean containsTitle = values.containsKey(Organization.TITLE);
1438            if (containsCompany || containsTitle) {
1439                long dataId = c.getLong(DataUpdateQuery._ID);
1440                long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1441
1442                String company;
1443
1444                if (containsCompany) {
1445                    company = values.getAsString(Organization.COMPANY);
1446                } else {
1447                    mSelectionArgs1[0] = String.valueOf(dataId);
1448                    company = DatabaseUtils.stringForQuery(db,
1449                            "SELECT " + Organization.COMPANY +
1450                            " FROM " + Tables.DATA +
1451                            " WHERE " + Data._ID + "=?", mSelectionArgs1);
1452                }
1453
1454                String title;
1455                if (containsTitle) {
1456                    title = values.getAsString(Organization.TITLE);
1457                } else {
1458                    mSelectionArgs1[0] = String.valueOf(dataId);
1459                    title = DatabaseUtils.stringForQuery(db,
1460                            "SELECT " + Organization.TITLE +
1461                            " FROM " + Tables.DATA +
1462                            " WHERE " + Data._ID + "=?", mSelectionArgs1);
1463                }
1464
1465                deleteNameLookup(dataId);
1466                insertNameLookupForOrganization(rawContactId, dataId, company, title);
1467
1468                fixRawContactDisplayName(db, rawContactId);
1469            }
1470            return true;
1471        }
1472
1473        @Override
1474        public int delete(SQLiteDatabase db, Cursor c) {
1475            long dataId = c.getLong(DataUpdateQuery._ID);
1476            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1477
1478            int count = super.delete(db, c);
1479            fixRawContactDisplayName(db, rawContactId);
1480            deleteNameLookup(dataId);
1481            return count;
1482        }
1483
1484        @Override
1485        protected int getTypeRank(int type) {
1486            switch (type) {
1487                case Organization.TYPE_WORK: return 0;
1488                case Organization.TYPE_CUSTOM: return 1;
1489                case Organization.TYPE_OTHER: return 2;
1490                default: return 1000;
1491            }
1492        }
1493    }
1494
1495    public class EmailDataRowHandler extends CommonDataRowHandler {
1496
1497        public EmailDataRowHandler() {
1498            super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
1499        }
1500
1501        @Override
1502        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1503            String email = values.getAsString(Email.DATA);
1504
1505            long dataId = super.insert(db, rawContactId, values);
1506
1507            fixRawContactDisplayName(db, rawContactId);
1508            String address = insertNameLookupForEmail(rawContactId, dataId, email);
1509            if (address != null) {
1510                triggerAggregation(rawContactId);
1511            }
1512            return dataId;
1513        }
1514
1515        @Override
1516        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1517                boolean callerIsSyncAdapter) {
1518            if (!super.update(db, values, c, callerIsSyncAdapter)) {
1519                return false;
1520            }
1521
1522            if (values.containsKey(Email.DATA)) {
1523                long dataId = c.getLong(DataUpdateQuery._ID);
1524                long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1525
1526                String address = values.getAsString(Email.DATA);
1527                deleteNameLookup(dataId);
1528                insertNameLookupForEmail(rawContactId, dataId, address);
1529                fixRawContactDisplayName(db, rawContactId);
1530                triggerAggregation(rawContactId);
1531            }
1532
1533            return true;
1534        }
1535
1536        @Override
1537        public int delete(SQLiteDatabase db, Cursor c) {
1538            long dataId = c.getLong(DataDeleteQuery._ID);
1539            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1540
1541            int count = super.delete(db, c);
1542
1543            deleteNameLookup(dataId);
1544            fixRawContactDisplayName(db, rawContactId);
1545            triggerAggregation(rawContactId);
1546            return count;
1547        }
1548
1549        @Override
1550        protected int getTypeRank(int type) {
1551            switch (type) {
1552                case Email.TYPE_HOME: return 0;
1553                case Email.TYPE_WORK: return 1;
1554                case Email.TYPE_CUSTOM: return 2;
1555                case Email.TYPE_OTHER: return 3;
1556                default: return 1000;
1557            }
1558        }
1559    }
1560
1561    public class NicknameDataRowHandler extends CommonDataRowHandler {
1562
1563        public NicknameDataRowHandler() {
1564            super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL);
1565        }
1566
1567        @Override
1568        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1569            String nickname = values.getAsString(Nickname.NAME);
1570
1571            long dataId = super.insert(db, rawContactId, values);
1572
1573            if (!TextUtils.isEmpty(nickname)) {
1574                fixRawContactDisplayName(db, rawContactId);
1575                insertNameLookupForNickname(rawContactId, dataId, nickname);
1576                triggerAggregation(rawContactId);
1577            }
1578            return dataId;
1579        }
1580
1581        @Override
1582        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1583                boolean callerIsSyncAdapter) {
1584            long dataId = c.getLong(DataUpdateQuery._ID);
1585            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1586
1587            if (!super.update(db, values, c, callerIsSyncAdapter)) {
1588                return false;
1589            }
1590
1591            if (values.containsKey(Nickname.NAME)) {
1592                String nickname = values.getAsString(Nickname.NAME);
1593                deleteNameLookup(dataId);
1594                insertNameLookupForNickname(rawContactId, dataId, nickname);
1595                fixRawContactDisplayName(db, rawContactId);
1596                triggerAggregation(rawContactId);
1597            }
1598
1599            return true;
1600        }
1601
1602        @Override
1603        public int delete(SQLiteDatabase db, Cursor c) {
1604            long dataId = c.getLong(DataDeleteQuery._ID);
1605            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1606
1607            int count = super.delete(db, c);
1608
1609            deleteNameLookup(dataId);
1610            fixRawContactDisplayName(db, rawContactId);
1611            triggerAggregation(rawContactId);
1612            return count;
1613        }
1614    }
1615
1616    public class PhoneDataRowHandler extends CommonDataRowHandler {
1617
1618        public PhoneDataRowHandler() {
1619            super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
1620        }
1621
1622        @Override
1623        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1624            long dataId;
1625            if (values.containsKey(Phone.NUMBER)) {
1626                String number = values.getAsString(Phone.NUMBER);
1627                String normalizedNumber = computeNormalizedNumber(number);
1628                values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
1629                dataId = super.insert(db, rawContactId, values);
1630
1631                updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
1632                mContactAggregator.updateHasPhoneNumber(db, rawContactId);
1633                fixRawContactDisplayName(db, rawContactId);
1634                if (normalizedNumber != null) {
1635                    triggerAggregation(rawContactId);
1636                }
1637            } else {
1638                dataId = super.insert(db, rawContactId, values);
1639            }
1640            return dataId;
1641        }
1642
1643        @Override
1644        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1645                boolean callerIsSyncAdapter) {
1646            String number = null;
1647            String normalizedNumber = null;
1648            if (values.containsKey(Phone.NUMBER)) {
1649                number = values.getAsString(Phone.NUMBER);
1650                normalizedNumber = computeNormalizedNumber(number);
1651                values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
1652            }
1653
1654            if (!super.update(db, values, c, callerIsSyncAdapter)) {
1655                return false;
1656            }
1657
1658            if (values.containsKey(Phone.NUMBER)) {
1659                long dataId = c.getLong(DataUpdateQuery._ID);
1660                long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1661                updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
1662                mContactAggregator.updateHasPhoneNumber(db, rawContactId);
1663                fixRawContactDisplayName(db, rawContactId);
1664                triggerAggregation(rawContactId);
1665            }
1666            return true;
1667        }
1668
1669        @Override
1670        public int delete(SQLiteDatabase db, Cursor c) {
1671            long dataId = c.getLong(DataDeleteQuery._ID);
1672            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1673
1674            int count = super.delete(db, c);
1675
1676            updatePhoneLookup(db, rawContactId, dataId, null, null);
1677            mContactAggregator.updateHasPhoneNumber(db, rawContactId);
1678            fixRawContactDisplayName(db, rawContactId);
1679            triggerAggregation(rawContactId);
1680            return count;
1681        }
1682
1683        private String computeNormalizedNumber(String number) {
1684            String normalizedNumber = null;
1685            if (number != null) {
1686                normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
1687            }
1688            return normalizedNumber;
1689        }
1690
1691        private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId,
1692                String number, String normalizedNumber) {
1693            if (number != null) {
1694                ContentValues phoneValues = new ContentValues();
1695                phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
1696                phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
1697                phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
1698                phoneValues.put(PhoneLookupColumns.MIN_MATCH,
1699                        PhoneNumberUtils.toCallerIDMinMatch(number));
1700
1701                db.replace(Tables.PHONE_LOOKUP, null, phoneValues);
1702            } else {
1703                mSelectionArgs1[0] = String.valueOf(dataId);
1704                db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1);
1705            }
1706        }
1707
1708        @Override
1709        protected int getTypeRank(int type) {
1710            switch (type) {
1711                case Phone.TYPE_MOBILE: return 0;
1712                case Phone.TYPE_WORK: return 1;
1713                case Phone.TYPE_HOME: return 2;
1714                case Phone.TYPE_PAGER: return 3;
1715                case Phone.TYPE_CUSTOM: return 4;
1716                case Phone.TYPE_OTHER: return 5;
1717                case Phone.TYPE_FAX_WORK: return 6;
1718                case Phone.TYPE_FAX_HOME: return 7;
1719                default: return 1000;
1720            }
1721        }
1722    }
1723
1724    public class GroupMembershipRowHandler extends DataRowHandler {
1725
1726        private static final String SELECTION_RAW_CONTACT_ID = RawContacts._ID + "=?";
1727
1728        private static final String QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID =
1729                "SELECT COUNT(*) FROM " + Tables.DATA + " LEFT OUTER JOIN " + Tables .GROUPS
1730                        + " ON " + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID
1731                        + "=" + GroupsColumns.CONCRETE_ID
1732                        + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
1733                        + " AND " + Tables.DATA + "." + GroupMembership.RAW_CONTACT_ID + "=?"
1734                        + " AND " + Groups.FAVORITES + "!=0";
1735
1736        public GroupMembershipRowHandler() {
1737            super(GroupMembership.CONTENT_ITEM_TYPE);
1738        }
1739
1740        @Override
1741        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1742            resolveGroupSourceIdInValues(rawContactId, db, values, true);
1743            long dataId = super.insert(db, rawContactId, values);
1744            if (hasFavoritesGroupMembership(db, rawContactId)) {
1745                updateRawContactsStar(db, rawContactId, true /* starred */);
1746            }
1747            updateVisibility(rawContactId);
1748            return dataId;
1749        }
1750
1751        @Override
1752        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1753                boolean callerIsSyncAdapter) {
1754            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1755            boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId);
1756            resolveGroupSourceIdInValues(rawContactId, db, values, false);
1757            if (!super.update(db, values, c, callerIsSyncAdapter)) {
1758                return false;
1759            }
1760            boolean isStarred = hasFavoritesGroupMembership(db, rawContactId);
1761            if (wasStarred != isStarred) {
1762                updateRawContactsStar(db, rawContactId, isStarred);
1763            }
1764            updateVisibility(rawContactId);
1765            return true;
1766        }
1767
1768        private void updateRawContactsStar(SQLiteDatabase db, long rawContactId, boolean starred) {
1769            ContentValues rawContactValues = new ContentValues();
1770            rawContactValues.put(RawContacts.STARRED, starred ? 1 : 0);
1771            if (db.update(Tables.RAW_CONTACTS, rawContactValues, SELECTION_RAW_CONTACT_ID,
1772                    new String[]{Long.toString(rawContactId)}) > 0) {
1773                mContactAggregator.updateStarred(rawContactId);
1774            }
1775        }
1776
1777        private boolean hasFavoritesGroupMembership(SQLiteDatabase db, long rawContactId) {
1778            final long groupMembershipMimetypeId = mDbHelper
1779                    .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
1780            boolean isStarred = 0 < DatabaseUtils
1781                    .longForQuery(db, QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID,
1782                    new String[]{Long.toString(groupMembershipMimetypeId), Long.toString(rawContactId)});
1783            return isStarred;
1784        }
1785
1786        @Override
1787        public int delete(SQLiteDatabase db, Cursor c) {
1788            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1789            boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId);
1790            int count = super.delete(db, c);
1791            boolean isStarred = hasFavoritesGroupMembership(db, rawContactId);
1792            if (wasStarred && !isStarred) {
1793                updateRawContactsStar(db, rawContactId, false /* starred */);
1794            }
1795            updateVisibility(rawContactId);
1796            return count;
1797        }
1798
1799        private void updateVisibility(long rawContactId) {
1800            long contactId = mDbHelper.getContactId(rawContactId);
1801            if (contactId != 0) {
1802                mDbHelper.updateContactVisible(contactId);
1803            }
1804        }
1805
1806        private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db,
1807                ContentValues values, boolean isInsert) {
1808            boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
1809            boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
1810            if (containsGroupSourceId && containsGroupId) {
1811                throw new IllegalArgumentException(
1812                        "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
1813                                + "and GroupMembership.GROUP_ROW_ID");
1814            }
1815
1816            if (!containsGroupSourceId && !containsGroupId) {
1817                if (isInsert) {
1818                    throw new IllegalArgumentException(
1819                            "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
1820                                    + "and GroupMembership.GROUP_ROW_ID");
1821                } else {
1822                    return;
1823                }
1824            }
1825
1826            if (containsGroupSourceId) {
1827                final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
1828                final long groupId = getOrMakeGroup(db, rawContactId, sourceId,
1829                        mInsertedRawContacts.get(rawContactId));
1830                values.remove(GroupMembership.GROUP_SOURCE_ID);
1831                values.put(GroupMembership.GROUP_ROW_ID, groupId);
1832            }
1833        }
1834    }
1835
1836    public class PhotoDataRowHandler extends DataRowHandler {
1837
1838        public PhotoDataRowHandler() {
1839            super(Photo.CONTENT_ITEM_TYPE);
1840        }
1841
1842        @Override
1843        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1844            long dataId = super.insert(db, rawContactId, values);
1845            if (!isNewRawContact(rawContactId)) {
1846                mContactAggregator.updatePhotoId(db, rawContactId);
1847            }
1848            return dataId;
1849        }
1850
1851        @Override
1852        public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1853                boolean callerIsSyncAdapter) {
1854            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1855            if (!super.update(db, values, c, callerIsSyncAdapter)) {
1856                return false;
1857            }
1858
1859            mContactAggregator.updatePhotoId(db, rawContactId);
1860            return true;
1861        }
1862
1863        @Override
1864        public int delete(SQLiteDatabase db, Cursor c) {
1865            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1866            int count = super.delete(db, c);
1867            mContactAggregator.updatePhotoId(db, rawContactId);
1868            return count;
1869        }
1870    }
1871
1872    /**
1873     * An entry in group id cache. It maps the combination of (account type, account name
1874     * and source id) to group row id.
1875     */
1876    public class GroupIdCacheEntry {
1877        String accountType;
1878        String accountName;
1879        String sourceId;
1880        long groupId;
1881    }
1882
1883    private HashMap<String, DataRowHandler> mDataRowHandlers;
1884    private ContactsDatabaseHelper mDbHelper;
1885
1886    private NameSplitter mNameSplitter;
1887    private NameLookupBuilder mNameLookupBuilder;
1888
1889    private PostalSplitter mPostalSplitter;
1890
1891    // We don't need a soft cache for groups - the assumption is that there will only
1892    // be a small number of contact groups. The cache is keyed off source id.  The value
1893    // is a list of groups with this group id.
1894    private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
1895
1896    private ContactAggregator mContactAggregator;
1897    private LegacyApiSupport mLegacyApiSupport;
1898    private GlobalSearchSupport mGlobalSearchSupport;
1899    private CommonNicknameCache mCommonNicknameCache;
1900
1901    private ContentValues mValues = new ContentValues();
1902    private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128);
1903    private NameSplitter.Name mName = new NameSplitter.Name();
1904    private HashMap<String, Boolean> mAccountWritability = Maps.newHashMap();
1905
1906    private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
1907    private long mEstimatedStorageRequirement = 0;
1908    private volatile CountDownLatch mAccessLatch;
1909
1910    private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap();
1911    private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
1912    private HashSet<Long> mDirtyRawContacts = Sets.newHashSet();
1913    private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap();
1914
1915    private boolean mVisibleTouched = false;
1916
1917    private boolean mSyncToNetwork;
1918
1919    private Locale mCurrentLocale;
1920
1921
1922    @Override
1923    public boolean onCreate() {
1924        super.onCreate();
1925        try {
1926            return initialize();
1927        } catch (RuntimeException e) {
1928            Log.e(TAG, "Cannot start provider", e);
1929            return false;
1930        }
1931    }
1932
1933    private boolean initialize() {
1934        final Context context = getContext();
1935        mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
1936        mGlobalSearchSupport = new GlobalSearchSupport(this);
1937        mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
1938        mContactAggregator = new ContactAggregator(this, mDbHelper,
1939                createPhotoPriorityResolver(context));
1940        mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
1941
1942        mDb = mDbHelper.getWritableDatabase();
1943
1944        initForDefaultLocale();
1945
1946        mSetPrimaryStatement = mDb.compileStatement(
1947                "UPDATE " + Tables.DATA +
1948                " SET " + Data.IS_PRIMARY + "=(_id=?)" +
1949                " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
1950                "   AND " + Data.RAW_CONTACT_ID + "=?");
1951
1952        mSetSuperPrimaryStatement = mDb.compileStatement(
1953                "UPDATE " + Tables.DATA +
1954                " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" +
1955                " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
1956                "   AND " + Data.RAW_CONTACT_ID + " IN (" +
1957                        "SELECT " + RawContacts._ID +
1958                        " FROM " + Tables.RAW_CONTACTS +
1959                        " WHERE " + RawContacts.CONTACT_ID + " =(" +
1960                                "SELECT " + RawContacts.CONTACT_ID +
1961                                " FROM " + Tables.RAW_CONTACTS +
1962                                " WHERE " + RawContacts._ID + "=?))");
1963
1964        mRawContactDisplayNameUpdate = mDb.compileStatement(
1965                "UPDATE " + Tables.RAW_CONTACTS +
1966                " SET " +
1967                        RawContacts.DISPLAY_NAME_SOURCE + "=?," +
1968                        RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
1969                        RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
1970                        RawContacts.PHONETIC_NAME + "=?," +
1971                        RawContacts.PHONETIC_NAME_STYLE + "=?," +
1972                        RawContacts.SORT_KEY_PRIMARY + "=?," +
1973                        RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
1974                " WHERE " + RawContacts._ID + "=?");
1975
1976        mLastStatusUpdate = mDb.compileStatement(
1977                "UPDATE " + Tables.CONTACTS +
1978                " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
1979                        "(SELECT " + DataColumns.CONCRETE_ID +
1980                        " FROM " + Tables.STATUS_UPDATES +
1981                        " JOIN " + Tables.DATA +
1982                        "   ON (" + StatusUpdatesColumns.DATA_ID + "="
1983                                + DataColumns.CONCRETE_ID + ")" +
1984                        " JOIN " + Tables.RAW_CONTACTS +
1985                        "   ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
1986                                + RawContactsColumns.CONCRETE_ID + ")" +
1987                        " WHERE " + RawContacts.CONTACT_ID + "=?" +
1988                        " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
1989                                + StatusUpdates.STATUS +
1990                        " LIMIT 1)" +
1991                " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
1992
1993        mNameLookupInsert = mDb.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
1994                + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + ","
1995                + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME
1996                + ") VALUES (?,?,?,?)");
1997        mNameLookupDelete = mDb.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
1998                + NameLookupColumns.DATA_ID + "=?");
1999
2000        mStatusUpdateInsert = mDb.compileStatement(
2001                "INSERT INTO " + Tables.STATUS_UPDATES + "("
2002                        + StatusUpdatesColumns.DATA_ID + ", "
2003                        + StatusUpdates.STATUS + ","
2004                        + StatusUpdates.STATUS_RES_PACKAGE + ","
2005                        + StatusUpdates.STATUS_ICON + ","
2006                        + StatusUpdates.STATUS_LABEL + ")" +
2007                " VALUES (?,?,?,?,?)");
2008
2009        mStatusUpdateReplace = mDb.compileStatement(
2010                "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "("
2011                        + StatusUpdatesColumns.DATA_ID + ", "
2012                        + StatusUpdates.STATUS_TIMESTAMP + ","
2013                        + StatusUpdates.STATUS + ","
2014                        + StatusUpdates.STATUS_RES_PACKAGE + ","
2015                        + StatusUpdates.STATUS_ICON + ","
2016                        + StatusUpdates.STATUS_LABEL + ")" +
2017                " VALUES (?,?,?,?,?,?)");
2018
2019        mStatusUpdateAutoTimestamp = mDb.compileStatement(
2020                "UPDATE " + Tables.STATUS_UPDATES +
2021                " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?,"
2022                        + StatusUpdates.STATUS + "=?" +
2023                " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"
2024                        + " AND " + StatusUpdates.STATUS + "!=?");
2025
2026        mStatusAttributionUpdate = mDb.compileStatement(
2027                "UPDATE " + Tables.STATUS_UPDATES +
2028                " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?,"
2029                        + StatusUpdates.STATUS_ICON + "=?,"
2030                        + StatusUpdates.STATUS_LABEL + "=?" +
2031                " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
2032
2033        mStatusUpdateDelete = mDb.compileStatement(
2034                "DELETE FROM " + Tables.STATUS_UPDATES +
2035                " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
2036
2037        // When setting NAME_VERIFIED to 1 on a raw contact, reset it to 0
2038        // on all other raw contacts in the same aggregate
2039        mResetNameVerifiedForOtherRawContacts = mDb.compileStatement(
2040                "UPDATE " + Tables.RAW_CONTACTS +
2041                " SET " + RawContacts.NAME_VERIFIED + "=0" +
2042                " WHERE " + RawContacts.CONTACT_ID + "=(" +
2043                        "SELECT " + RawContacts.CONTACT_ID +
2044                        " FROM " + Tables.RAW_CONTACTS +
2045                        " WHERE " + RawContacts._ID + "=?)" +
2046                " AND " + RawContacts._ID + "!=?");
2047
2048        mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
2049        mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
2050        mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
2051        mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
2052        mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE);
2053        mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
2054
2055        verifyAccounts();
2056        verifyLocale();
2057
2058        if (isLegacyContactImportNeeded()) {
2059            importLegacyContactsAsync();
2060        }
2061
2062        return (mDb != null);
2063    }
2064
2065    private void initDataRowHandlers() {
2066      mDataRowHandlers = new HashMap<String, DataRowHandler>();
2067
2068      mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
2069      mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
2070              new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
2071      mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
2072              StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
2073      mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
2074      mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
2075      mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler());
2076      mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
2077              new StructuredNameRowHandler(mNameSplitter));
2078      mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE,
2079              new StructuredPostalRowHandler(mPostalSplitter));
2080      mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler());
2081      mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler());
2082    }
2083    /**
2084     * Visible for testing.
2085     */
2086    /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
2087        return new PhotoPriorityResolver(context);
2088    }
2089
2090    /**
2091     * (Re)allocates all locale-sensitive structures.
2092     */
2093    private void initForDefaultLocale() {
2094        mCurrentLocale = getLocale();
2095        mNameSplitter = mDbHelper.createNameSplitter();
2096        mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
2097        mPostalSplitter = new PostalSplitter(mCurrentLocale);
2098        mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase());
2099        ContactLocaleUtils.getIntance().setLocale(mCurrentLocale);
2100        initDataRowHandlers();
2101    }
2102
2103    @Override
2104    public void onConfigurationChanged(Configuration newConfig) {
2105        initForDefaultLocale();
2106        verifyLocale();
2107    }
2108
2109    protected void verifyAccounts() {
2110        AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
2111        onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
2112    }
2113
2114    /**
2115     * Verifies that the contacts database is properly configured for the current locale.
2116     * If not, changes the database locale to the current locale using an asynchronous task.
2117     * This needs to be done asynchronously because the process involves rebuilding
2118     * large data structures (name lookup, sort keys), which can take minutes on
2119     * a large set of contacts.
2120     */
2121    protected void verifyLocale() {
2122
2123        // The process is already running - postpone the change
2124        if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) {
2125            return;
2126        }
2127
2128        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
2129        final String providerLocale = prefs.getString(PREF_LOCALE, null);
2130        if (providerLocale == null) {
2131            // The provider has just been created for the first time. There are no
2132            // contacts in the database, so we can safely set locale on the UI thread.
2133            mDbHelper.setLocale(ContactsProvider2.this, mCurrentLocale);
2134            prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit();
2135            return;
2136        }
2137
2138        final Locale currentLocale = mCurrentLocale;
2139        if (currentLocale.toString().equals(providerLocale)) {
2140            return;
2141        }
2142
2143        int providerStatus = mProviderStatus;
2144        setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
2145
2146        AsyncTask<Integer, Void, Void> task = new AsyncTask<Integer, Void, Void>() {
2147
2148            int savedProviderStatus;
2149
2150            @Override
2151            protected Void doInBackground(Integer... params) {
2152                savedProviderStatus = params[0];
2153                mDbHelper.setLocale(ContactsProvider2.this, currentLocale);
2154                return null;
2155            }
2156
2157            @Override
2158            protected void onPostExecute(Void result) {
2159                prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).commit();
2160                setProviderStatus(savedProviderStatus);
2161
2162                // Recursive invocation, needed to cover the case where locale
2163                // changes once and then changes again before the db upgrade is completed.
2164                verifyLocale();
2165            }
2166        };
2167
2168        task.execute(providerStatus);
2169    }
2170
2171    /* Visible for testing */
2172    @Override
2173    protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
2174        return ContactsDatabaseHelper.getInstance(context);
2175    }
2176
2177    /* package */ NameSplitter getNameSplitter() {
2178        return mNameSplitter;
2179    }
2180
2181    /* Visible for testing */
2182    protected Locale getLocale() {
2183        return Locale.getDefault();
2184    }
2185
2186    protected boolean isLegacyContactImportNeeded() {
2187        int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0"));
2188        return version < PROPERTY_CONTACTS_IMPORT_VERSION;
2189    }
2190
2191    protected LegacyContactImporter getLegacyContactImporter() {
2192        return new LegacyContactImporter(getContext(), this);
2193    }
2194
2195    /**
2196     * Imports legacy contacts in a separate thread.  As long as the import process is running
2197     * all other access to the contacts is blocked.
2198     */
2199    private void importLegacyContactsAsync() {
2200        Log.v(TAG, "Importing legacy contacts");
2201        setProviderStatus(ProviderStatus.STATUS_UPGRADING);
2202        if (mAccessLatch == null) {
2203            mAccessLatch = new CountDownLatch(1);
2204        }
2205
2206        Thread importThread = new Thread("LegacyContactImport") {
2207            @Override
2208            public void run() {
2209                LegacyContactImporter importer = getLegacyContactImporter();
2210                if (importLegacyContacts(importer)) {
2211                    onLegacyContactImportSuccess();
2212                } else {
2213                    onLegacyContactImportFailure();
2214                }
2215            }
2216        };
2217
2218        importThread.start();
2219    }
2220
2221    /**
2222     * Unlocks the provider and declares that the import process is complete.
2223     */
2224    private void onLegacyContactImportSuccess() {
2225        NotificationManager nm =
2226            (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE);
2227        nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION);
2228
2229        // Store a property in the database indicating that the conversion process succeeded
2230        mDbHelper.setProperty(PROPERTY_CONTACTS_IMPORTED,
2231                String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION));
2232        setProviderStatus(ProviderStatus.STATUS_NORMAL);
2233        mAccessLatch.countDown();
2234        mAccessLatch = null;
2235        Log.v(TAG, "Completed import of legacy contacts");
2236    }
2237
2238    /**
2239     * Announces the provider status and keeps the provider locked.
2240     */
2241    private void onLegacyContactImportFailure() {
2242        Context context = getContext();
2243        NotificationManager nm =
2244            (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
2245
2246        // Show a notification
2247        Notification n = new Notification(android.R.drawable.stat_notify_error,
2248                context.getString(R.string.upgrade_out_of_memory_notification_ticker),
2249                System.currentTimeMillis());
2250        n.setLatestEventInfo(context,
2251                context.getString(R.string.upgrade_out_of_memory_notification_title),
2252                context.getString(R.string.upgrade_out_of_memory_notification_text),
2253                PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0));
2254        n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
2255
2256        nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n);
2257
2258        setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY);
2259        Log.v(TAG, "Failed to import legacy contacts");
2260    }
2261
2262    /* Visible for testing */
2263    /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
2264        boolean aggregatorEnabled = mContactAggregator.isEnabled();
2265        mContactAggregator.setEnabled(false);
2266        try {
2267            if (importer.importContacts()) {
2268
2269                // TODO aggregate all newly added raw contacts
2270                mContactAggregator.setEnabled(aggregatorEnabled);
2271                return true;
2272            }
2273        } catch (Throwable e) {
2274           Log.e(TAG, "Legacy contact import failed", e);
2275        }
2276        mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement();
2277        return false;
2278    }
2279
2280    /**
2281     * Wipes all data from the contacts database.
2282     */
2283    /* package */ void wipeData() {
2284        mDbHelper.wipeData();
2285    }
2286
2287    /**
2288     * While importing and aggregating contacts, this content provider will
2289     * block all attempts to change contacts data. In particular, it will hold
2290     * up all contact syncs. As soon as the import process is complete, all
2291     * processes waiting to write to the provider are unblocked and can proceed
2292     * to compete for the database transaction monitor.
2293     */
2294    private void waitForAccess() {
2295        CountDownLatch latch = mAccessLatch;
2296        if (latch != null) {
2297            while (true) {
2298                try {
2299                    latch.await();
2300                    mAccessLatch = null;
2301                    return;
2302                } catch (InterruptedException e) {
2303                    Thread.currentThread().interrupt();
2304                }
2305            }
2306        }
2307    }
2308
2309    @Override
2310    public Uri insert(Uri uri, ContentValues values) {
2311        waitForAccess();
2312        return super.insert(uri, values);
2313    }
2314
2315    @Override
2316    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
2317        if (mAccessLatch != null) {
2318            // We are stuck trying to upgrade contacts db.  The only update request
2319            // allowed in this case is an update of provider status, which will trigger
2320            // an attempt to upgrade contacts again.
2321            int match = sUriMatcher.match(uri);
2322            if (match == PROVIDER_STATUS && isLegacyContactImportNeeded()) {
2323                Integer newStatus = values.getAsInteger(ProviderStatus.STATUS);
2324                if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) {
2325                    importLegacyContactsAsync();
2326                    return 1;
2327                } else {
2328                    return 0;
2329                }
2330            }
2331        }
2332        waitForAccess();
2333        return super.update(uri, values, selection, selectionArgs);
2334    }
2335
2336    @Override
2337    public int delete(Uri uri, String selection, String[] selectionArgs) {
2338        waitForAccess();
2339        return super.delete(uri, selection, selectionArgs);
2340    }
2341
2342    @Override
2343    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
2344            throws OperationApplicationException {
2345        waitForAccess();
2346        return super.applyBatch(operations);
2347    }
2348
2349    @Override
2350    protected void onBeginTransaction() {
2351        if (VERBOSE_LOGGING) {
2352            Log.v(TAG, "onBeginTransaction");
2353        }
2354        super.onBeginTransaction();
2355        mContactAggregator.clearPendingAggregations();
2356        clearTransactionalChanges();
2357    }
2358
2359    private void clearTransactionalChanges() {
2360        mInsertedRawContacts.clear();
2361        mUpdatedRawContacts.clear();
2362        mUpdatedSyncStates.clear();
2363        mDirtyRawContacts.clear();
2364    }
2365
2366    @Override
2367    protected void beforeTransactionCommit() {
2368
2369        if (VERBOSE_LOGGING) {
2370            Log.v(TAG, "beforeTransactionCommit");
2371        }
2372        super.beforeTransactionCommit();
2373        flushTransactionalChanges();
2374        mContactAggregator.aggregateInTransaction(mDb);
2375        if (mVisibleTouched) {
2376            mVisibleTouched = false;
2377            mDbHelper.updateAllVisible();
2378        }
2379    }
2380
2381    private void flushTransactionalChanges() {
2382        if (VERBOSE_LOGGING) {
2383            Log.v(TAG, "flushTransactionChanges");
2384        }
2385
2386        for (long rawContactId : mInsertedRawContacts.keySet()) {
2387            updateRawContactDisplayName(mDb, rawContactId);
2388            mContactAggregator.onRawContactInsert(mDb, rawContactId);
2389        }
2390
2391        if (!mDirtyRawContacts.isEmpty()) {
2392            mSb.setLength(0);
2393            mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
2394            appendIds(mSb, mDirtyRawContacts);
2395            mSb.append(")");
2396            mDb.execSQL(mSb.toString());
2397        }
2398
2399        if (!mUpdatedRawContacts.isEmpty()) {
2400            mSb.setLength(0);
2401            mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
2402            appendIds(mSb, mUpdatedRawContacts);
2403            mSb.append(")");
2404            mDb.execSQL(mSb.toString());
2405        }
2406
2407        for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
2408            long id = entry.getKey();
2409            if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) {
2410                throw new IllegalStateException(
2411                        "unable to update sync state, does it still exist?");
2412            }
2413        }
2414
2415        clearTransactionalChanges();
2416    }
2417
2418    /**
2419     * Appends comma separated ids.
2420     * @param ids Should not be empty
2421     */
2422    private void appendIds(StringBuilder sb, HashSet<Long> ids) {
2423        for (long id : ids) {
2424            sb.append(id).append(',');
2425        }
2426
2427        sb.setLength(sb.length() - 1); // Yank the last comma
2428    }
2429
2430    @Override
2431    protected void notifyChange() {
2432        notifyChange(mSyncToNetwork);
2433        mSyncToNetwork = false;
2434    }
2435
2436    protected void notifyChange(boolean syncToNetwork) {
2437        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
2438                syncToNetwork);
2439    }
2440
2441    protected void setProviderStatus(int status) {
2442        mProviderStatus = status;
2443        getContext().getContentResolver().notifyChange(ContactsContract.ProviderStatus.CONTENT_URI,
2444                null, false);
2445    }
2446
2447    private boolean isNewRawContact(long rawContactId) {
2448        return mInsertedRawContacts.containsKey(rawContactId);
2449    }
2450
2451    private DataRowHandler getDataRowHandler(final String mimeType) {
2452        DataRowHandler handler = mDataRowHandlers.get(mimeType);
2453        if (handler == null) {
2454            handler = new CustomDataRowHandler(mimeType);
2455            mDataRowHandlers.put(mimeType, handler);
2456        }
2457        return handler;
2458    }
2459
2460    @Override
2461    protected Uri insertInTransaction(Uri uri, ContentValues values) {
2462        if (VERBOSE_LOGGING) {
2463            Log.v(TAG, "insertInTransaction: " + uri + " " + values);
2464        }
2465
2466        final boolean callerIsSyncAdapter =
2467                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2468
2469        final int match = sUriMatcher.match(uri);
2470        long id = 0;
2471
2472        switch (match) {
2473            case SYNCSTATE:
2474                id = mDbHelper.getSyncState().insert(mDb, values);
2475                break;
2476
2477            case CONTACTS: {
2478                insertContact(values);
2479                break;
2480            }
2481
2482            case RAW_CONTACTS: {
2483                id = insertRawContact(uri, values, callerIsSyncAdapter);
2484                mSyncToNetwork |= !callerIsSyncAdapter;
2485                break;
2486            }
2487
2488            case RAW_CONTACTS_DATA: {
2489                values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
2490                id = insertData(values, callerIsSyncAdapter);
2491                mSyncToNetwork |= !callerIsSyncAdapter;
2492                break;
2493            }
2494
2495            case DATA: {
2496                id = insertData(values, callerIsSyncAdapter);
2497                mSyncToNetwork |= !callerIsSyncAdapter;
2498                break;
2499            }
2500
2501            case GROUPS: {
2502                id = insertGroup(uri, values, callerIsSyncAdapter);
2503                mSyncToNetwork |= !callerIsSyncAdapter;
2504                break;
2505            }
2506
2507            case SETTINGS: {
2508                id = insertSettings(uri, values);
2509                mSyncToNetwork |= !callerIsSyncAdapter;
2510                break;
2511            }
2512
2513            case STATUS_UPDATES: {
2514                id = insertStatusUpdate(values);
2515                break;
2516            }
2517
2518            default:
2519                mSyncToNetwork = true;
2520                return mLegacyApiSupport.insert(uri, values);
2521        }
2522
2523        if (id < 0) {
2524            return null;
2525        }
2526
2527        return ContentUris.withAppendedId(uri, id);
2528    }
2529
2530    /**
2531     * If account is non-null then store it in the values. If the account is
2532     * already specified in the values then it must be consistent with the
2533     * account, if it is non-null.
2534     *
2535     * @param uri Current {@link Uri} being operated on.
2536     * @param values {@link ContentValues} to read and possibly update.
2537     * @throws IllegalArgumentException when only one of
2538     *             {@link RawContacts#ACCOUNT_NAME} or
2539     *             {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
2540     *             other undefined.
2541     * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
2542     *             and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
2543     *             the given {@link Uri} and {@link ContentValues}.
2544     */
2545    private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
2546        String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
2547        String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
2548        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
2549
2550        String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
2551        String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
2552        final boolean partialValues = TextUtils.isEmpty(valueAccountName)
2553                ^ TextUtils.isEmpty(valueAccountType);
2554
2555        if (partialUri || partialValues) {
2556            // Throw when either account is incomplete
2557            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
2558                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
2559        }
2560
2561        // Accounts are valid by only checking one parameter, since we've
2562        // already ruled out partial accounts.
2563        final boolean validUri = !TextUtils.isEmpty(accountName);
2564        final boolean validValues = !TextUtils.isEmpty(valueAccountName);
2565
2566        if (validValues && validUri) {
2567            // Check that accounts match when both present
2568            final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
2569                    && TextUtils.equals(accountType, valueAccountType);
2570            if (!accountMatch) {
2571                throw new IllegalArgumentException(mDbHelper.exceptionMessage(
2572                        "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
2573            }
2574        } else if (validUri) {
2575            // Fill values from Uri when not present
2576            values.put(RawContacts.ACCOUNT_NAME, accountName);
2577            values.put(RawContacts.ACCOUNT_TYPE, accountType);
2578        } else if (validValues) {
2579            accountName = valueAccountName;
2580            accountType = valueAccountType;
2581        } else {
2582            return null;
2583        }
2584
2585        // Use cached Account object when matches, otherwise create
2586        if (mAccount == null
2587                || !mAccount.name.equals(accountName)
2588                || !mAccount.type.equals(accountType)) {
2589            mAccount = new Account(accountName, accountType);
2590        }
2591
2592        return mAccount;
2593    }
2594
2595    /**
2596     * Inserts an item in the contacts table
2597     *
2598     * @param values the values for the new row
2599     * @return the row ID of the newly created row
2600     */
2601    private long insertContact(ContentValues values) {
2602        throw new UnsupportedOperationException("Aggregate contacts are created automatically");
2603    }
2604
2605    /**
2606     * Inserts an item in the contacts table
2607     *
2608     * @param uri the values for the new row
2609     * @param values the account this contact should be associated with. may be null.
2610     * @param callerIsSyncAdapter
2611     * @return the row ID of the newly created row
2612     */
2613    private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
2614        mValues.clear();
2615        mValues.putAll(values);
2616        mValues.putNull(RawContacts.CONTACT_ID);
2617
2618        final Account account = resolveAccount(uri, mValues);
2619
2620        if (values.containsKey(RawContacts.DELETED)
2621                && values.getAsInteger(RawContacts.DELETED) != 0) {
2622            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
2623        }
2624
2625        long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues);
2626        int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
2627        if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) {
2628            aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE);
2629        }
2630        mContactAggregator.markNewForAggregation(rawContactId, aggregationMode);
2631
2632        // Trigger creation of a Contact based on this RawContact at the end of transaction
2633        mInsertedRawContacts.put(rawContactId, account);
2634
2635        if (!callerIsSyncAdapter) {
2636            addAutoAddMembership(rawContactId);
2637            final Long starred = values.getAsLong(RawContacts.STARRED);
2638            if (starred != null && starred != 0) {
2639                updateFavoritesMembership(rawContactId, starred != 0);
2640            }
2641        }
2642
2643        return rawContactId;
2644    }
2645
2646    private void addAutoAddMembership(long rawContactId) {
2647        final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID,
2648                rawContactId);
2649        if (groupId != null) {
2650            insertDataGroupMembership(rawContactId, groupId);
2651        }
2652    }
2653
2654    private Long findGroupByRawContactId(String selection, long rawContactId) {
2655        Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID,
2656                selection,
2657                new String[]{Long.toString(rawContactId)},
2658                null /* groupBy */, null /* having */, null /* orderBy */);
2659        try {
2660            while (c.moveToNext()) {
2661                return c.getLong(0);
2662            }
2663            return null;
2664        } finally {
2665            c.close();
2666        }
2667    }
2668
2669    private void updateFavoritesMembership(long rawContactId, boolean isStarred) {
2670        final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID,
2671                rawContactId);
2672        if (groupId != null) {
2673            if (isStarred) {
2674                insertDataGroupMembership(rawContactId, groupId);
2675            } else {
2676                deleteDataGroupMembership(rawContactId, groupId);
2677            }
2678        }
2679    }
2680
2681    private void insertDataGroupMembership(long rawContactId, long groupId) {
2682        ContentValues groupMembershipValues = new ContentValues();
2683        groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId);
2684        groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
2685        groupMembershipValues.put(DataColumns.MIMETYPE_ID,
2686                mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
2687        mDb.insert(Tables.DATA, null, groupMembershipValues);
2688    }
2689
2690    private void deleteDataGroupMembership(long rawContactId, long groupId) {
2691        final String[] selectionArgs = {
2692                Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)),
2693                Long.toString(groupId),
2694                Long.toString(rawContactId)};
2695        mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs);
2696    }
2697
2698    /**
2699     * Inserts an item in the data table
2700     *
2701     * @param values the values for the new row
2702     * @return the row ID of the newly created row
2703     */
2704    private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
2705        long id = 0;
2706        mValues.clear();
2707        mValues.putAll(values);
2708
2709        long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
2710
2711        // Replace package with internal mapping
2712        final String packageName = mValues.getAsString(Data.RES_PACKAGE);
2713        if (packageName != null) {
2714            mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
2715        }
2716        mValues.remove(Data.RES_PACKAGE);
2717
2718        // Replace mimetype with internal mapping
2719        final String mimeType = mValues.getAsString(Data.MIMETYPE);
2720        if (TextUtils.isEmpty(mimeType)) {
2721            throw new IllegalArgumentException(Data.MIMETYPE + " is required");
2722        }
2723
2724        mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
2725        mValues.remove(Data.MIMETYPE);
2726
2727        DataRowHandler rowHandler = getDataRowHandler(mimeType);
2728        id = rowHandler.insert(mDb, rawContactId, mValues);
2729        if (!callerIsSyncAdapter) {
2730            setRawContactDirty(rawContactId);
2731        }
2732        mUpdatedRawContacts.add(rawContactId);
2733        return id;
2734    }
2735
2736    private void triggerAggregation(long rawContactId) {
2737        if (!mContactAggregator.isEnabled()) {
2738            return;
2739        }
2740
2741        int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
2742        switch (aggregationMode) {
2743            case RawContacts.AGGREGATION_MODE_DISABLED:
2744                break;
2745
2746            case RawContacts.AGGREGATION_MODE_DEFAULT: {
2747                mContactAggregator.markForAggregation(rawContactId, aggregationMode, false);
2748                break;
2749            }
2750
2751            case RawContacts.AGGREGATION_MODE_SUSPENDED: {
2752                long contactId = mDbHelper.getContactId(rawContactId);
2753
2754                if (contactId != 0) {
2755                    mContactAggregator.updateAggregateData(contactId);
2756                }
2757                break;
2758            }
2759
2760            case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
2761                long contactId = mDbHelper.getContactId(rawContactId);
2762                mContactAggregator.aggregateContact(mDb, rawContactId, contactId);
2763                break;
2764            }
2765        }
2766    }
2767
2768    /**
2769     * Returns the group id of the group with sourceId and the same account as rawContactId.
2770     * If the group doesn't already exist then it is first created,
2771     * @param db SQLiteDatabase to use for this operation
2772     * @param rawContactId the contact this group is associated with
2773     * @param sourceId the sourceIf of the group to query or create
2774     * @return the group id of the existing or created group
2775     * @throws IllegalArgumentException if the contact is not associated with an account
2776     * @throws IllegalStateException if a group needs to be created but the creation failed
2777     */
2778    private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId,
2779            Account account) {
2780
2781        if (account == null) {
2782            mSelectionArgs1[0] = String.valueOf(rawContactId);
2783            Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
2784                    RawContacts._ID + "=?", mSelectionArgs1, null, null, null);
2785            try {
2786                if (c.moveToFirst()) {
2787                    String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME);
2788                    String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
2789                    if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
2790                        account = new Account(accountName, accountType);
2791                    }
2792                }
2793            } finally {
2794                c.close();
2795            }
2796        }
2797
2798        if (account == null) {
2799            throw new IllegalArgumentException("if the groupmembership only "
2800                    + "has a sourceid the the contact must be associated with "
2801                    + "an account");
2802        }
2803
2804        ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId);
2805        if (entries == null) {
2806            entries = new ArrayList<GroupIdCacheEntry>(1);
2807            mGroupIdCache.put(sourceId, entries);
2808        }
2809
2810        int count = entries.size();
2811        for (int i = 0; i < count; i++) {
2812            GroupIdCacheEntry entry = entries.get(i);
2813            if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) {
2814                return entry.groupId;
2815            }
2816        }
2817
2818        GroupIdCacheEntry entry = new GroupIdCacheEntry();
2819        entry.accountName = account.name;
2820        entry.accountType = account.type;
2821        entry.sourceId = sourceId;
2822        entries.add(0, entry);
2823
2824        // look up the group that contains this sourceId and has the same account name and type
2825        // as the contact refered to by rawContactId
2826        Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
2827                Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
2828                new String[]{sourceId, account.name, account.type}, null, null, null);
2829        try {
2830            if (c.moveToFirst()) {
2831                entry.groupId = c.getLong(0);
2832            } else {
2833                ContentValues groupValues = new ContentValues();
2834                groupValues.put(Groups.ACCOUNT_NAME, account.name);
2835                groupValues.put(Groups.ACCOUNT_TYPE, account.type);
2836                groupValues.put(Groups.SOURCE_ID, sourceId);
2837                long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
2838                if (groupId < 0) {
2839                    throw new IllegalStateException("unable to create a new group with "
2840                            + "this sourceid: " + groupValues);
2841                }
2842                entry.groupId = groupId;
2843            }
2844        } finally {
2845            c.close();
2846        }
2847
2848        return entry.groupId;
2849    }
2850
2851    private interface DisplayNameQuery {
2852        public static final String RAW_SQL =
2853                "SELECT "
2854                        + DataColumns.MIMETYPE_ID + ","
2855                        + Data.IS_PRIMARY + ","
2856                        + Data.DATA1 + ","
2857                        + Data.DATA2 + ","
2858                        + Data.DATA3 + ","
2859                        + Data.DATA4 + ","
2860                        + Data.DATA5 + ","
2861                        + Data.DATA6 + ","
2862                        + Data.DATA7 + ","
2863                        + Data.DATA8 + ","
2864                        + Data.DATA9 + ","
2865                        + Data.DATA10 + ","
2866                        + Data.DATA11 +
2867                " FROM " + Tables.DATA +
2868                " WHERE " + Data.RAW_CONTACT_ID + "=?" +
2869                        " AND (" + Data.DATA1 + " NOT NULL OR " +
2870                                Organization.TITLE + " NOT NULL)";
2871
2872        public static final int MIMETYPE = 0;
2873        public static final int IS_PRIMARY = 1;
2874        public static final int DATA1 = 2;
2875        public static final int GIVEN_NAME = 3;                         // data2
2876        public static final int FAMILY_NAME = 4;                        // data3
2877        public static final int PREFIX = 5;                             // data4
2878        public static final int TITLE = 5;                              // data4
2879        public static final int MIDDLE_NAME = 6;                        // data5
2880        public static final int SUFFIX = 7;                             // data6
2881        public static final int PHONETIC_GIVEN_NAME = 8;                // data7
2882        public static final int PHONETIC_MIDDLE_NAME = 9;               // data8
2883        public static final int ORGANIZATION_PHONETIC_NAME = 9;         // data8
2884        public static final int PHONETIC_FAMILY_NAME = 10;              // data9
2885        public static final int FULL_NAME_STYLE = 11;                   // data10
2886        public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11;  // data10
2887        public static final int PHONETIC_NAME_STYLE = 12;               // data11
2888    }
2889
2890    /**
2891     * Updates a raw contact display name based on data rows, e.g. structured name,
2892     * organization, email etc.
2893     */
2894    public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
2895        int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
2896        NameSplitter.Name bestName = null;
2897        String bestDisplayName = null;
2898        String bestPhoneticName = null;
2899        int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
2900
2901        mSelectionArgs1[0] = String.valueOf(rawContactId);
2902        Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1);
2903        try {
2904            while (c.moveToNext()) {
2905                int mimeType = c.getInt(DisplayNameQuery.MIMETYPE);
2906                int source = getDisplayNameSource(mimeType);
2907                if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
2908                    continue;
2909                }
2910
2911                if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) {
2912                    continue;
2913                }
2914
2915                if (mimeType == mMimeTypeIdStructuredName) {
2916                    NameSplitter.Name name;
2917                    if (bestName != null) {
2918                        name = new NameSplitter.Name();
2919                    } else {
2920                        name = mName;
2921                        name.clear();
2922                    }
2923                    name.prefix = c.getString(DisplayNameQuery.PREFIX);
2924                    name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME);
2925                    name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME);
2926                    name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME);
2927                    name.suffix = c.getString(DisplayNameQuery.SUFFIX);
2928                    name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE)
2929                            ? FullNameStyle.UNDEFINED
2930                            : c.getInt(DisplayNameQuery.FULL_NAME_STYLE);
2931                    name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME);
2932                    name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME);
2933                    name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME);
2934                    name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE)
2935                            ? PhoneticNameStyle.UNDEFINED
2936                            : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE);
2937                    if (!name.isEmpty()) {
2938                        bestDisplayNameSource = source;
2939                        bestName = name;
2940                    }
2941                } else if (mimeType == mMimeTypeIdOrganization) {
2942                    mCharArrayBuffer.sizeCopied = 0;
2943                    c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
2944                    if (mCharArrayBuffer.sizeCopied != 0) {
2945                        bestDisplayNameSource = source;
2946                        bestDisplayName = new String(mCharArrayBuffer.data, 0,
2947                                mCharArrayBuffer.sizeCopied);
2948                        bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME);
2949                        bestPhoneticNameStyle =
2950                                c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE)
2951                                    ? PhoneticNameStyle.UNDEFINED
2952                                    : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE);
2953                    } else {
2954                        c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer);
2955                        if (mCharArrayBuffer.sizeCopied != 0) {
2956                            bestDisplayNameSource = source;
2957                            bestDisplayName = new String(mCharArrayBuffer.data, 0,
2958                                    mCharArrayBuffer.sizeCopied);
2959                            bestPhoneticName = null;
2960                            bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
2961                        }
2962                    }
2963                } else {
2964                    // Display name is at DATA1 in all other types.
2965                    // This is ensured in the constructor.
2966
2967                    mCharArrayBuffer.sizeCopied = 0;
2968                    c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
2969                    if (mCharArrayBuffer.sizeCopied != 0) {
2970                        bestDisplayNameSource = source;
2971                        bestDisplayName = new String(mCharArrayBuffer.data, 0,
2972                                mCharArrayBuffer.sizeCopied);
2973                        bestPhoneticName = null;
2974                        bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
2975                    }
2976                }
2977            }
2978
2979        } finally {
2980            c.close();
2981        }
2982
2983        String displayNamePrimary;
2984        String displayNameAlternative;
2985        String sortKeyPrimary = null;
2986        String sortKeyAlternative = null;
2987        int displayNameStyle = FullNameStyle.UNDEFINED;
2988
2989        if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) {
2990            displayNameStyle = bestName.fullNameStyle;
2991            if (displayNameStyle == FullNameStyle.CJK
2992                    || displayNameStyle == FullNameStyle.UNDEFINED) {
2993                displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
2994                bestName.fullNameStyle = displayNameStyle;
2995            }
2996
2997            displayNamePrimary = mNameSplitter.join(bestName, true);
2998            displayNameAlternative = mNameSplitter.join(bestName, false);
2999
3000            bestPhoneticName = mNameSplitter.joinPhoneticName(bestName);
3001            bestPhoneticNameStyle = bestName.phoneticNameStyle;
3002        } else {
3003            displayNamePrimary = displayNameAlternative = bestDisplayName;
3004        }
3005
3006        if (bestPhoneticName != null) {
3007            sortKeyPrimary = sortKeyAlternative = bestPhoneticName;
3008            if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
3009                bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName);
3010            }
3011        } else {
3012            if (displayNameStyle == FullNameStyle.UNDEFINED) {
3013                displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName);
3014                if (displayNameStyle == FullNameStyle.UNDEFINED
3015                        || displayNameStyle == FullNameStyle.CJK) {
3016                    displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle(
3017                            displayNameStyle, bestPhoneticNameStyle);
3018                }
3019                displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
3020            }
3021            if (displayNameStyle == FullNameStyle.CHINESE ||
3022                    displayNameStyle == FullNameStyle.CJK) {
3023                sortKeyPrimary = sortKeyAlternative =
3024                        ContactLocaleUtils.getIntance().getSortKey(
3025                                displayNamePrimary, displayNameStyle);
3026            }
3027        }
3028
3029        if (sortKeyPrimary == null) {
3030            sortKeyPrimary = displayNamePrimary;
3031            sortKeyAlternative = displayNameAlternative;
3032        }
3033
3034        setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary,
3035                displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle,
3036                sortKeyPrimary, sortKeyAlternative);
3037    }
3038
3039    private int getDisplayNameSource(int mimeTypeId) {
3040        if (mimeTypeId == mMimeTypeIdStructuredName) {
3041            return DisplayNameSources.STRUCTURED_NAME;
3042        } else if (mimeTypeId == mMimeTypeIdEmail) {
3043            return DisplayNameSources.EMAIL;
3044        } else if (mimeTypeId == mMimeTypeIdPhone) {
3045            return DisplayNameSources.PHONE;
3046        } else if (mimeTypeId == mMimeTypeIdOrganization) {
3047            return DisplayNameSources.ORGANIZATION;
3048        } else if (mimeTypeId == mMimeTypeIdNickname) {
3049            return DisplayNameSources.NICKNAME;
3050        } else {
3051            return DisplayNameSources.UNDEFINED;
3052        }
3053    }
3054
3055    /**
3056     * Delete data row by row so that fixing of primaries etc work correctly.
3057     */
3058    private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
3059        int count = 0;
3060
3061        // Note that the query will return data according to the access restrictions,
3062        // so we don't need to worry about deleting data we don't have permission to read.
3063        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null);
3064        try {
3065            while(c.moveToNext()) {
3066                long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
3067                String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
3068                DataRowHandler rowHandler = getDataRowHandler(mimeType);
3069                count += rowHandler.delete(mDb, c);
3070                if (!callerIsSyncAdapter) {
3071                    setRawContactDirty(rawContactId);
3072                }
3073            }
3074        } finally {
3075            c.close();
3076        }
3077
3078        return count;
3079    }
3080
3081    /**
3082     * Delete a data row provided that it is one of the allowed mime types.
3083     */
3084    public int deleteData(long dataId, String[] allowedMimeTypes) {
3085
3086        // Note that the query will return data according to the access restrictions,
3087        // so we don't need to worry about deleting data we don't have permission to read.
3088        mSelectionArgs1[0] = String.valueOf(dataId);
3089        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?",
3090                mSelectionArgs1, null);
3091
3092        try {
3093            if (!c.moveToFirst()) {
3094                return 0;
3095            }
3096
3097            String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
3098            boolean valid = false;
3099            for (int i = 0; i < allowedMimeTypes.length; i++) {
3100                if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
3101                    valid = true;
3102                    break;
3103                }
3104            }
3105
3106            if (!valid) {
3107                throw new IllegalArgumentException("Data type mismatch: expected "
3108                        + Lists.newArrayList(allowedMimeTypes));
3109            }
3110
3111            DataRowHandler rowHandler = getDataRowHandler(mimeType);
3112            return rowHandler.delete(mDb, c);
3113        } finally {
3114            c.close();
3115        }
3116    }
3117
3118    /**
3119     * Inserts an item in the groups table
3120     */
3121    private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
3122        mValues.clear();
3123        mValues.putAll(values);
3124
3125        final Account account = resolveAccount(uri, mValues);
3126
3127        // Replace package with internal mapping
3128        final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
3129        if (packageName != null) {
3130            mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
3131        }
3132        mValues.remove(Groups.RES_PACKAGE);
3133
3134        final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null
3135                ? mValues.getAsLong(Groups.FAVORITES) != 0
3136                : false;
3137
3138        if (!callerIsSyncAdapter) {
3139            mValues.put(Groups.DIRTY, 1);
3140        }
3141
3142        long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
3143
3144        if (!callerIsSyncAdapter && isFavoritesGroup) {
3145            // add all starred raw contacts to this group
3146            String selection;
3147            String[] selectionArgs;
3148            if (account == null) {
3149                selection = RawContacts.ACCOUNT_NAME + " IS NULL AND "
3150                        + RawContacts.ACCOUNT_TYPE + " IS NULL";
3151                selectionArgs = null;
3152            } else {
3153                selection = RawContacts.ACCOUNT_NAME + "=? AND "
3154                        + RawContacts.ACCOUNT_TYPE + "=?";
3155                selectionArgs = new String[]{account.name, account.type};
3156            }
3157            Cursor c = mDb.query(Tables.RAW_CONTACTS,
3158                    new String[]{RawContacts._ID, RawContacts.STARRED},
3159                    selection, selectionArgs, null, null, null);
3160            while (c.moveToNext()) {
3161                if (c.getLong(1) != 0) {
3162                    final long rawContactId = c.getLong(0);
3163                    insertDataGroupMembership(rawContactId, result);
3164                    setRawContactDirty(rawContactId);
3165                }
3166            }
3167        }
3168
3169        if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
3170            mVisibleTouched = true;
3171        }
3172
3173        return result;
3174    }
3175
3176    private long insertSettings(Uri uri, ContentValues values) {
3177        final long id = mDb.insert(Tables.SETTINGS, null, values);
3178
3179        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
3180            mVisibleTouched = true;
3181        }
3182
3183        return id;
3184    }
3185
3186    /**
3187     * Inserts a status update.
3188     */
3189    public long insertStatusUpdate(ContentValues values) {
3190        final String handle = values.getAsString(StatusUpdates.IM_HANDLE);
3191        final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL);
3192        String customProtocol = null;
3193
3194        if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
3195            customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
3196            if (TextUtils.isEmpty(customProtocol)) {
3197                throw new IllegalArgumentException(
3198                        "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
3199            }
3200        }
3201
3202        long rawContactId = -1;
3203        long contactId = -1;
3204        Long dataId = values.getAsLong(StatusUpdates.DATA_ID);
3205        mSb.setLength(0);
3206        mSelectionArgs.clear();
3207        if (dataId != null) {
3208            // Lookup the contact info for the given data row.
3209
3210            mSb.append(Tables.DATA + "." + Data._ID + "=?");
3211            mSelectionArgs.add(String.valueOf(dataId));
3212        } else {
3213            // Lookup the data row to attach this presence update to
3214
3215            if (TextUtils.isEmpty(handle) || protocol == null) {
3216                throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
3217            }
3218
3219            // TODO: generalize to allow other providers to match against email
3220            boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
3221
3222            String mimeTypeIdIm = String.valueOf(mMimeTypeIdIm);
3223            if (matchEmail) {
3224                String mimeTypeIdEmail = String.valueOf(mMimeTypeIdEmail);
3225
3226                // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
3227                // the "OR" conjunction confuses it and it switches to a full scan of
3228                // the raw_contacts table.
3229
3230                // This code relies on the fact that Im.DATA and Email.DATA are in fact the same
3231                // column - Data.DATA1
3232                mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" +
3233                        " AND " + Data.DATA1 + "=?" +
3234                        " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?");
3235                mSelectionArgs.add(mimeTypeIdEmail);
3236                mSelectionArgs.add(mimeTypeIdIm);
3237                mSelectionArgs.add(handle);
3238                mSelectionArgs.add(mimeTypeIdIm);
3239                mSelectionArgs.add(String.valueOf(protocol));
3240                if (customProtocol != null) {
3241                    mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
3242                    mSelectionArgs.add(customProtocol);
3243                }
3244                mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))");
3245                mSelectionArgs.add(mimeTypeIdEmail);
3246            } else {
3247                mSb.append(DataColumns.MIMETYPE_ID + "=?" +
3248                        " AND " + Im.PROTOCOL + "=?" +
3249                        " AND " + Im.DATA + "=?");
3250                mSelectionArgs.add(mimeTypeIdIm);
3251                mSelectionArgs.add(String.valueOf(protocol));
3252                mSelectionArgs.add(handle);
3253                if (customProtocol != null) {
3254                    mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
3255                    mSelectionArgs.add(customProtocol);
3256                }
3257            }
3258
3259            if (values.containsKey(StatusUpdates.DATA_ID)) {
3260                mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?");
3261                mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID));
3262            }
3263        }
3264        mSb.append(" AND ").append(getContactsRestrictions());
3265
3266        Cursor cursor = null;
3267        try {
3268            cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
3269                    mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
3270                    Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID);
3271            if (cursor.moveToFirst()) {
3272                dataId = cursor.getLong(DataContactsQuery.DATA_ID);
3273                rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
3274                contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
3275            } else {
3276                // No contact found, return a null URI
3277                return -1;
3278            }
3279        } finally {
3280            if (cursor != null) {
3281                cursor.close();
3282            }
3283        }
3284
3285        if (values.containsKey(StatusUpdates.PRESENCE)) {
3286            if (customProtocol == null) {
3287                // We cannot allow a null in the custom protocol field, because SQLite3 does not
3288                // properly enforce uniqueness of null values
3289                customProtocol = "";
3290            }
3291
3292            mValues.clear();
3293            mValues.put(StatusUpdates.DATA_ID, dataId);
3294            mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
3295            mValues.put(PresenceColumns.CONTACT_ID, contactId);
3296            mValues.put(StatusUpdates.PROTOCOL, protocol);
3297            mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
3298            mValues.put(StatusUpdates.IM_HANDLE, handle);
3299            if (values.containsKey(StatusUpdates.IM_ACCOUNT)) {
3300                mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT));
3301            }
3302            mValues.put(StatusUpdates.PRESENCE,
3303                    values.getAsString(StatusUpdates.PRESENCE));
3304
3305            // Insert the presence update
3306            mDb.replace(Tables.PRESENCE, null, mValues);
3307        }
3308
3309
3310        if (values.containsKey(StatusUpdates.STATUS)) {
3311            String status = values.getAsString(StatusUpdates.STATUS);
3312            String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
3313            Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL);
3314
3315            if (TextUtils.isEmpty(resPackage)
3316                    && (labelResource == null || labelResource == 0)
3317                    && protocol != null) {
3318                labelResource = Im.getProtocolLabelResource(protocol);
3319            }
3320
3321            Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON);
3322            // TODO compute the default icon based on the protocol
3323
3324            if (TextUtils.isEmpty(status)) {
3325                mStatusUpdateDelete.bindLong(1, dataId);
3326                mStatusUpdateDelete.execute();
3327            } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) {
3328                long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
3329                mStatusUpdateReplace.bindLong(1, dataId);
3330                mStatusUpdateReplace.bindLong(2, timestamp);
3331                bindString(mStatusUpdateReplace, 3, status);
3332                bindString(mStatusUpdateReplace, 4, resPackage);
3333                bindLong(mStatusUpdateReplace, 5, iconResource);
3334                bindLong(mStatusUpdateReplace, 6, labelResource);
3335                mStatusUpdateReplace.execute();
3336            } else {
3337
3338                try {
3339                    mStatusUpdateInsert.bindLong(1, dataId);
3340                    bindString(mStatusUpdateInsert, 2, status);
3341                    bindString(mStatusUpdateInsert, 3, resPackage);
3342                    bindLong(mStatusUpdateInsert, 4, iconResource);
3343                    bindLong(mStatusUpdateInsert, 5, labelResource);
3344                    mStatusUpdateInsert.executeInsert();
3345                } catch (SQLiteConstraintException e) {
3346                    // The row already exists - update it
3347                    long timestamp = System.currentTimeMillis();
3348                    mStatusUpdateAutoTimestamp.bindLong(1, timestamp);
3349                    bindString(mStatusUpdateAutoTimestamp, 2, status);
3350                    mStatusUpdateAutoTimestamp.bindLong(3, dataId);
3351                    bindString(mStatusUpdateAutoTimestamp, 4, status);
3352                    mStatusUpdateAutoTimestamp.execute();
3353
3354                    bindString(mStatusAttributionUpdate, 1, resPackage);
3355                    bindLong(mStatusAttributionUpdate, 2, iconResource);
3356                    bindLong(mStatusAttributionUpdate, 3, labelResource);
3357                    mStatusAttributionUpdate.bindLong(4, dataId);
3358                    mStatusAttributionUpdate.execute();
3359                }
3360            }
3361        }
3362
3363        if (contactId != -1) {
3364            mLastStatusUpdate.bindLong(1, contactId);
3365            mLastStatusUpdate.bindLong(2, contactId);
3366            mLastStatusUpdate.execute();
3367        }
3368
3369        return dataId;
3370    }
3371
3372    @Override
3373    protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
3374        if (VERBOSE_LOGGING) {
3375            Log.v(TAG, "deleteInTransaction: " + uri);
3376        }
3377        flushTransactionalChanges();
3378        final boolean callerIsSyncAdapter =
3379                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
3380        final int match = sUriMatcher.match(uri);
3381        switch (match) {
3382            case SYNCSTATE:
3383                return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
3384
3385            case SYNCSTATE_ID:
3386                String selectionWithId =
3387                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
3388                        + (selection == null ? "" : " AND (" + selection + ")");
3389                return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs);
3390
3391            case CONTACTS: {
3392                // TODO
3393                return 0;
3394            }
3395
3396            case CONTACTS_ID: {
3397                long contactId = ContentUris.parseId(uri);
3398                return deleteContact(contactId, callerIsSyncAdapter);
3399            }
3400
3401            case CONTACTS_LOOKUP: {
3402                final List<String> pathSegments = uri.getPathSegments();
3403                final int segmentCount = pathSegments.size();
3404                if (segmentCount < 3) {
3405                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
3406                            "Missing a lookup key", uri));
3407                }
3408                final String lookupKey = pathSegments.get(2);
3409                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
3410                return deleteContact(contactId, callerIsSyncAdapter);
3411            }
3412
3413            case CONTACTS_LOOKUP_ID: {
3414                // lookup contact by id and lookup key to see if they still match the actual record
3415                long contactId = ContentUris.parseId(uri);
3416                final List<String> pathSegments = uri.getPathSegments();
3417                final String lookupKey = pathSegments.get(2);
3418                SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
3419                setTablesAndProjectionMapForContacts(lookupQb, uri, null);
3420                String[] args;
3421                if (selectionArgs == null) {
3422                    args = new String[2];
3423                } else {
3424                    args = new String[selectionArgs.length + 2];
3425                    System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
3426                }
3427                args[0] = String.valueOf(contactId);
3428                args[1] = Uri.encode(lookupKey);
3429                lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
3430                final SQLiteDatabase db = mDbHelper.getReadableDatabase();
3431                Cursor c = query(db, lookupQb, null, selection, args, null, null, null);
3432                try {
3433                    if (c.getCount() == 1) {
3434                        // contact was unmodified so go ahead and delete it
3435                        return deleteContact(contactId, callerIsSyncAdapter);
3436                    } else {
3437                        // row was changed (e.g. the merging might have changed), we got multiple
3438                        // rows or the supplied selection filtered the record out
3439                        return 0;
3440                    }
3441                } finally {
3442                    c.close();
3443                }
3444            }
3445
3446            case RAW_CONTACTS: {
3447                int numDeletes = 0;
3448                Cursor c = mDb.query(Tables.RAW_CONTACTS,
3449                        new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
3450                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
3451                try {
3452                    while (c.moveToNext()) {
3453                        final long rawContactId = c.getLong(0);
3454                        long contactId = c.getLong(1);
3455                        numDeletes += deleteRawContact(rawContactId, contactId,
3456                                callerIsSyncAdapter);
3457                    }
3458                } finally {
3459                    c.close();
3460                }
3461                return numDeletes;
3462            }
3463
3464            case RAW_CONTACTS_ID: {
3465                final long rawContactId = ContentUris.parseId(uri);
3466                return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId),
3467                        callerIsSyncAdapter);
3468            }
3469
3470            case DATA: {
3471                mSyncToNetwork |= !callerIsSyncAdapter;
3472                return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
3473                        callerIsSyncAdapter);
3474            }
3475
3476            case DATA_ID:
3477            case PHONES_ID:
3478            case EMAILS_ID:
3479            case POSTALS_ID: {
3480                long dataId = ContentUris.parseId(uri);
3481                mSyncToNetwork |= !callerIsSyncAdapter;
3482                mSelectionArgs1[0] = String.valueOf(dataId);
3483                return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
3484            }
3485
3486            case GROUPS_ID: {
3487                mSyncToNetwork |= !callerIsSyncAdapter;
3488                return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
3489            }
3490
3491            case GROUPS: {
3492                int numDeletes = 0;
3493                Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
3494                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
3495                try {
3496                    while (c.moveToNext()) {
3497                        numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
3498                    }
3499                } finally {
3500                    c.close();
3501                }
3502                if (numDeletes > 0) {
3503                    mSyncToNetwork |= !callerIsSyncAdapter;
3504                }
3505                return numDeletes;
3506            }
3507
3508            case SETTINGS: {
3509                mSyncToNetwork |= !callerIsSyncAdapter;
3510                return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs);
3511            }
3512
3513            case STATUS_UPDATES: {
3514                return deleteStatusUpdates(selection, selectionArgs);
3515            }
3516
3517            default: {
3518                mSyncToNetwork = true;
3519                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
3520            }
3521        }
3522    }
3523
3524    public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
3525        mGroupIdCache.clear();
3526        final long groupMembershipMimetypeId = mDbHelper
3527                .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
3528        mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
3529                + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
3530                + groupId, null);
3531
3532        try {
3533            if (callerIsSyncAdapter) {
3534                return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
3535            } else {
3536                mValues.clear();
3537                mValues.put(Groups.DELETED, 1);
3538                mValues.put(Groups.DIRTY, 1);
3539                return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
3540            }
3541        } finally {
3542            mVisibleTouched = true;
3543        }
3544    }
3545
3546    private int deleteSettings(Uri uri, String selection, String[] selectionArgs) {
3547        final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs);
3548        mVisibleTouched = true;
3549        return count;
3550    }
3551
3552    private int deleteContact(long contactId, boolean callerIsSyncAdapter) {
3553        mSelectionArgs1[0] = Long.toString(contactId);
3554        Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
3555                RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
3556                null, null, null);
3557        try {
3558            while (c.moveToNext()) {
3559                long rawContactId = c.getLong(0);
3560                markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
3561            }
3562        } finally {
3563            c.close();
3564        }
3565
3566        return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
3567    }
3568
3569    public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
3570        mContactAggregator.invalidateAggregationExceptionCache();
3571        if (callerIsSyncAdapter) {
3572            mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
3573            int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
3574            mContactAggregator.updateDisplayNameForContact(mDb, contactId);
3575            return count;
3576        } else {
3577            mDbHelper.removeContactIfSingleton(rawContactId);
3578            return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
3579        }
3580    }
3581
3582    private int deleteStatusUpdates(String selection, String[] selectionArgs) {
3583      // delete from both tables: presence and status_updates
3584      // TODO should account type/name be appended to the where clause?
3585      if (VERBOSE_LOGGING) {
3586          Log.v(TAG, "deleting data from status_updates for " + selection);
3587      }
3588      mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
3589          selectionArgs);
3590      return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
3591    }
3592
3593    private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) {
3594        mSyncToNetwork = true;
3595
3596        mValues.clear();
3597        mValues.put(RawContacts.DELETED, 1);
3598        mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
3599        mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
3600        mValues.putNull(RawContacts.CONTACT_ID);
3601        mValues.put(RawContacts.DIRTY, 1);
3602        return updateRawContact(rawContactId, mValues, callerIsSyncAdapter);
3603    }
3604
3605    @Override
3606    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
3607            String[] selectionArgs) {
3608        if (VERBOSE_LOGGING) {
3609            Log.v(TAG, "updateInTransaction: " + uri);
3610        }
3611
3612        int count = 0;
3613
3614        final int match = sUriMatcher.match(uri);
3615        if (match == SYNCSTATE_ID && selection == null) {
3616            long rowId = ContentUris.parseId(uri);
3617            Object data = values.get(ContactsContract.SyncState.DATA);
3618            mUpdatedSyncStates.put(rowId, data);
3619            return 1;
3620        }
3621        flushTransactionalChanges();
3622        final boolean callerIsSyncAdapter =
3623                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
3624        switch(match) {
3625            case SYNCSTATE:
3626                return mDbHelper.getSyncState().update(mDb, values,
3627                        appendAccountToSelection(uri, selection), selectionArgs);
3628
3629            case SYNCSTATE_ID: {
3630                selection = appendAccountToSelection(uri, selection);
3631                String selectionWithId =
3632                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
3633                        + (selection == null ? "" : " AND (" + selection + ")");
3634                return mDbHelper.getSyncState().update(mDb, values,
3635                        selectionWithId, selectionArgs);
3636            }
3637
3638            case CONTACTS: {
3639                count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
3640                break;
3641            }
3642
3643            case CONTACTS_ID: {
3644                count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter);
3645                break;
3646            }
3647
3648            case CONTACTS_LOOKUP:
3649            case CONTACTS_LOOKUP_ID: {
3650                final List<String> pathSegments = uri.getPathSegments();
3651                final int segmentCount = pathSegments.size();
3652                if (segmentCount < 3) {
3653                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
3654                            "Missing a lookup key", uri));
3655                }
3656                final String lookupKey = pathSegments.get(2);
3657                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
3658                count = updateContactOptions(contactId, values, callerIsSyncAdapter);
3659                break;
3660            }
3661
3662            case RAW_CONTACTS_DATA: {
3663                final String rawContactId = uri.getPathSegments().get(1);
3664                String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
3665                    + (selection == null ? "" : " AND " + selection);
3666
3667                count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
3668
3669                break;
3670            }
3671
3672            case DATA: {
3673                count = updateData(uri, values, appendAccountToSelection(uri, selection),
3674                        selectionArgs, callerIsSyncAdapter);
3675                if (count > 0) {
3676                    mSyncToNetwork |= !callerIsSyncAdapter;
3677                }
3678                break;
3679            }
3680
3681            case DATA_ID:
3682            case PHONES_ID:
3683            case EMAILS_ID:
3684            case POSTALS_ID: {
3685                count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
3686                if (count > 0) {
3687                    mSyncToNetwork |= !callerIsSyncAdapter;
3688                }
3689                break;
3690            }
3691
3692            case RAW_CONTACTS: {
3693                selection = appendAccountToSelection(uri, selection);
3694                count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
3695                break;
3696            }
3697
3698            case RAW_CONTACTS_ID: {
3699                long rawContactId = ContentUris.parseId(uri);
3700                if (selection != null) {
3701                    selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
3702                    count = updateRawContacts(values, RawContacts._ID + "=?"
3703                                    + " AND(" + selection + ")", selectionArgs,
3704                            callerIsSyncAdapter);
3705                } else {
3706                    mSelectionArgs1[0] = String.valueOf(rawContactId);
3707                    count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
3708                            callerIsSyncAdapter);
3709                }
3710                break;
3711            }
3712
3713            case GROUPS: {
3714                count = updateGroups(uri, values, appendAccountToSelection(uri, selection),
3715                        selectionArgs, callerIsSyncAdapter);
3716                if (count > 0) {
3717                    mSyncToNetwork |= !callerIsSyncAdapter;
3718                }
3719                break;
3720            }
3721
3722            case GROUPS_ID: {
3723                long groupId = ContentUris.parseId(uri);
3724                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
3725                String selectionWithId = Groups._ID + "=? "
3726                        + (selection == null ? "" : " AND " + selection);
3727                count = updateGroups(uri, values, selectionWithId, selectionArgs,
3728                        callerIsSyncAdapter);
3729                if (count > 0) {
3730                    mSyncToNetwork |= !callerIsSyncAdapter;
3731                }
3732                break;
3733            }
3734
3735            case AGGREGATION_EXCEPTIONS: {
3736                count = updateAggregationException(mDb, values);
3737                break;
3738            }
3739
3740            case SETTINGS: {
3741                count = updateSettings(uri, values, appendAccountToSelection(uri, selection),
3742                        selectionArgs);
3743                mSyncToNetwork |= !callerIsSyncAdapter;
3744                break;
3745            }
3746
3747            case STATUS_UPDATES: {
3748                count = updateStatusUpdate(uri, values, selection, selectionArgs);
3749                break;
3750            }
3751
3752            default: {
3753                mSyncToNetwork = true;
3754                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
3755            }
3756        }
3757
3758        return count;
3759    }
3760
3761    private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
3762        String[] selectionArgs) {
3763        // update status_updates table, if status is provided
3764        // TODO should account type/name be appended to the where clause?
3765        int updateCount = 0;
3766        ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
3767        if (settableValues.size() > 0) {
3768          updateCount = mDb.update(Tables.STATUS_UPDATES,
3769                    settableValues,
3770                    getWhereClauseForStatusUpdatesTable(selection),
3771                    selectionArgs);
3772        }
3773
3774        // now update the Presence table
3775        settableValues = getSettableColumnsForPresenceTable(values);
3776        if (settableValues.size() > 0) {
3777          updateCount = mDb.update(Tables.PRESENCE, settableValues,
3778                    selection, selectionArgs);
3779        }
3780        // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
3781        // potentially get updated in this method.
3782        return updateCount;
3783    }
3784
3785    /**
3786     * Build a where clause to select the rows to be updated in status_updates table.
3787     */
3788    private String getWhereClauseForStatusUpdatesTable(String selection) {
3789        mSb.setLength(0);
3790        mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
3791        mSb.append(selection);
3792        mSb.append(")");
3793        return mSb.toString();
3794    }
3795
3796    private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
3797        mValues.clear();
3798        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
3799            StatusUpdates.STATUS);
3800        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
3801            StatusUpdates.STATUS_TIMESTAMP);
3802        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
3803            StatusUpdates.STATUS_RES_PACKAGE);
3804        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
3805            StatusUpdates.STATUS_LABEL);
3806        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
3807            StatusUpdates.STATUS_ICON);
3808        return mValues;
3809    }
3810
3811    private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
3812        mValues.clear();
3813        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
3814            StatusUpdates.PRESENCE);
3815        return mValues;
3816    }
3817
3818    private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
3819            String[] selectionArgs, boolean callerIsSyncAdapter) {
3820
3821        mGroupIdCache.clear();
3822
3823        ContentValues updatedValues;
3824        if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
3825            updatedValues = mValues;
3826            updatedValues.clear();
3827            updatedValues.putAll(values);
3828            updatedValues.put(Groups.DIRTY, 1);
3829        } else {
3830            updatedValues = values;
3831        }
3832
3833        int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs);
3834        if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
3835            mVisibleTouched = true;
3836        }
3837        if (updatedValues.containsKey(Groups.SHOULD_SYNC)
3838                && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
3839            Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
3840                    Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null,
3841                    null, null);
3842            String accountName;
3843            String accountType;
3844            try {
3845                while (c.moveToNext()) {
3846                    accountName = c.getString(0);
3847                    accountType = c.getString(1);
3848                    if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
3849                        Account account = new Account(accountName, accountType);
3850                        ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
3851                                new Bundle());
3852                        break;
3853                    }
3854                }
3855            } finally {
3856                c.close();
3857            }
3858        }
3859        return count;
3860    }
3861
3862    private int updateSettings(Uri uri, ContentValues values, String selection,
3863            String[] selectionArgs) {
3864        final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
3865        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
3866            mVisibleTouched = true;
3867        }
3868        return count;
3869    }
3870
3871    private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs,
3872            boolean callerIsSyncAdapter) {
3873        if (values.containsKey(RawContacts.CONTACT_ID)) {
3874            throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
3875                    "in content values. Contact IDs are assigned automatically");
3876        }
3877
3878        int count = 0;
3879        Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
3880                new String[] { RawContacts._ID }, selection,
3881                selectionArgs, null, null, null);
3882        try {
3883            while (cursor.moveToNext()) {
3884                long rawContactId = cursor.getLong(0);
3885                updateRawContact(rawContactId, values, callerIsSyncAdapter);
3886                count++;
3887            }
3888        } finally {
3889            cursor.close();
3890        }
3891
3892        return count;
3893    }
3894
3895    private int updateRawContact(long rawContactId, ContentValues values,
3896            boolean callerIsSyncAdapter) {
3897        final String selection = RawContacts._ID + " = ?";
3898        mSelectionArgs1[0] = Long.toString(rawContactId);
3899        final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
3900                && values.getAsInteger(RawContacts.DELETED) == 0);
3901        int previousDeleted = 0;
3902        String accountType = null;
3903        String accountName = null;
3904        if (requestUndoDelete) {
3905            Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
3906                    mSelectionArgs1, null, null, null);
3907            try {
3908                if (cursor.moveToFirst()) {
3909                    previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
3910                    accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
3911                    accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
3912                }
3913            } finally {
3914                cursor.close();
3915            }
3916            values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
3917                    ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
3918        }
3919
3920        int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1);
3921        if (count != 0) {
3922            if (values.containsKey(RawContacts.AGGREGATION_MODE)) {
3923                int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE);
3924
3925                // As per ContactsContract documentation, changing aggregation mode
3926                // to DEFAULT should not trigger aggregation
3927                if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
3928                    mContactAggregator.markForAggregation(rawContactId, aggregationMode, false);
3929                }
3930            }
3931            if (values.containsKey(RawContacts.STARRED)) {
3932                if (!callerIsSyncAdapter) {
3933                    updateFavoritesMembership(rawContactId,
3934                            values.getAsLong(RawContacts.STARRED) != 0);
3935                }
3936                mContactAggregator.updateStarred(rawContactId);
3937            } else {
3938                // if this raw contact is being associated with an account, then update the
3939                // favorites group membership based on whether or not this contact is starred.
3940                // If it is starred, add a group membership, if one doesn't already exist
3941                // otherwise delete any matching group memberships.
3942                if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
3943                    boolean starred = 0 != DatabaseUtils.longForQuery(mDb,
3944                            SELECTION_STARRED_FROM_RAW_CONTACTS,
3945                            new String[]{Long.toString(rawContactId)});
3946                    updateFavoritesMembership(rawContactId, starred);
3947                }
3948            }
3949
3950            // if this raw contact is being associated with an account, then add a
3951            // group membership to the group marked as AutoAdd, if any.
3952            if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
3953                addAutoAddMembership(rawContactId);
3954            }
3955
3956            if (values.containsKey(RawContacts.SOURCE_ID)) {
3957                mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId);
3958            }
3959            if (values.containsKey(RawContacts.NAME_VERIFIED)) {
3960
3961                // If setting NAME_VERIFIED for this raw contact, reset it for all
3962                // other raw contacts in the same aggregate
3963                if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) {
3964                    mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId);
3965                    mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId);
3966                    mResetNameVerifiedForOtherRawContacts.execute();
3967                }
3968                mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId);
3969            }
3970            if (requestUndoDelete && previousDeleted == 1) {
3971                // undo delete, needs aggregation again.
3972                mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType));
3973            }
3974        }
3975        return count;
3976    }
3977
3978    private int updateData(Uri uri, ContentValues values, String selection,
3979            String[] selectionArgs, boolean callerIsSyncAdapter) {
3980        mValues.clear();
3981        mValues.putAll(values);
3982        mValues.remove(Data._ID);
3983        mValues.remove(Data.RAW_CONTACT_ID);
3984        mValues.remove(Data.MIMETYPE);
3985
3986        String packageName = values.getAsString(Data.RES_PACKAGE);
3987        if (packageName != null) {
3988            mValues.remove(Data.RES_PACKAGE);
3989            mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
3990        }
3991
3992        boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
3993        boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
3994
3995        // Remove primary or super primary values being set to 0. This is disallowed by the
3996        // content provider.
3997        if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
3998            containsIsSuperPrimary = false;
3999            mValues.remove(Data.IS_SUPER_PRIMARY);
4000        }
4001        if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
4002            containsIsPrimary = false;
4003            mValues.remove(Data.IS_PRIMARY);
4004        }
4005
4006        int count = 0;
4007
4008        // Note that the query will return data according to the access restrictions,
4009        // so we don't need to worry about updating data we don't have permission to read.
4010        Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null);
4011        try {
4012            while(c.moveToNext()) {
4013                count += updateData(mValues, c, callerIsSyncAdapter);
4014            }
4015        } finally {
4016            c.close();
4017        }
4018
4019        return count;
4020    }
4021
4022    private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
4023        if (values.size() == 0) {
4024            return 0;
4025        }
4026
4027        final String mimeType = c.getString(DataUpdateQuery.MIMETYPE);
4028        DataRowHandler rowHandler = getDataRowHandler(mimeType);
4029        if (rowHandler.update(mDb, values, c, callerIsSyncAdapter)) {
4030            return 1;
4031        } else {
4032            return 0;
4033        }
4034    }
4035
4036    private int updateContactOptions(ContentValues values, String selection,
4037            String[] selectionArgs, boolean callerIsSyncAdapter) {
4038        int count = 0;
4039        Cursor cursor = mDb.query(mDbHelper.getContactView(),
4040                new String[] { Contacts._ID }, selection,
4041                selectionArgs, null, null, null);
4042        try {
4043            while (cursor.moveToNext()) {
4044                long contactId = cursor.getLong(0);
4045                updateContactOptions(contactId, values, callerIsSyncAdapter);
4046                count++;
4047            }
4048        } finally {
4049            cursor.close();
4050        }
4051
4052        return count;
4053    }
4054
4055    private int updateContactOptions(long contactId, ContentValues values,
4056            boolean callerIsSyncAdapter) {
4057
4058        mValues.clear();
4059        ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
4060                values, Contacts.CUSTOM_RINGTONE);
4061        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
4062                values, Contacts.SEND_TO_VOICEMAIL);
4063        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
4064                values, Contacts.LAST_TIME_CONTACTED);
4065        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
4066                values, Contacts.TIMES_CONTACTED);
4067        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
4068                values, Contacts.STARRED);
4069
4070        // Nothing to update - just return
4071        if (mValues.size() == 0) {
4072            return 0;
4073        }
4074
4075        if (mValues.containsKey(RawContacts.STARRED)) {
4076            // Mark dirty when changing starred to trigger sync
4077            mValues.put(RawContacts.DIRTY, 1);
4078        }
4079
4080        mSelectionArgs1[0] = String.valueOf(contactId);
4081        mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
4082
4083        if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) {
4084            Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
4085                    new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?",
4086                    mSelectionArgs1, null, null, null);
4087            try {
4088                while (cursor.moveToNext()) {
4089                    long rawContactId = cursor.getLong(0);
4090                    updateFavoritesMembership(rawContactId,
4091                            mValues.getAsLong(RawContacts.STARRED) != 0);
4092                }
4093            } finally {
4094                cursor.close();
4095            }
4096        }
4097
4098        // Copy changeable values to prevent automatically managed fields from
4099        // being explicitly updated by clients.
4100        mValues.clear();
4101        ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
4102                values, Contacts.CUSTOM_RINGTONE);
4103        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
4104                values, Contacts.SEND_TO_VOICEMAIL);
4105        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
4106                values, Contacts.LAST_TIME_CONTACTED);
4107        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
4108                values, Contacts.TIMES_CONTACTED);
4109        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
4110                values, Contacts.STARRED);
4111
4112        int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1);
4113
4114        if (values.containsKey(Contacts.LAST_TIME_CONTACTED) &&
4115                !values.containsKey(Contacts.TIMES_CONTACTED)) {
4116            mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
4117            mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
4118        }
4119        return rslt;
4120    }
4121
4122    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
4123        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
4124        long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
4125        long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
4126
4127        long rawContactId1, rawContactId2;
4128        if (rcId1 < rcId2) {
4129            rawContactId1 = rcId1;
4130            rawContactId2 = rcId2;
4131        } else {
4132            rawContactId2 = rcId1;
4133            rawContactId1 = rcId2;
4134        }
4135
4136        if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
4137            mSelectionArgs2[0] = String.valueOf(rawContactId1);
4138            mSelectionArgs2[1] = String.valueOf(rawContactId2);
4139            db.delete(Tables.AGGREGATION_EXCEPTIONS,
4140                    AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
4141                    + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
4142        } else {
4143            ContentValues exceptionValues = new ContentValues(3);
4144            exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
4145            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
4146            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
4147            db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
4148                    exceptionValues);
4149        }
4150
4151        mContactAggregator.invalidateAggregationExceptionCache();
4152        mContactAggregator.markForAggregation(rawContactId1,
4153                RawContacts.AGGREGATION_MODE_DEFAULT, true);
4154        mContactAggregator.markForAggregation(rawContactId2,
4155                RawContacts.AGGREGATION_MODE_DEFAULT, true);
4156
4157        long contactId1 = mDbHelper.getContactId(rawContactId1);
4158        mContactAggregator.aggregateContact(db, rawContactId1, contactId1);
4159
4160        long contactId2 = mDbHelper.getContactId(rawContactId2);
4161        mContactAggregator.aggregateContact(db, rawContactId2, contactId2);
4162
4163        // The return value is fake - we just confirm that we made a change, not count actual
4164        // rows changed.
4165        return 1;
4166    }
4167
4168    public void onAccountsUpdated(Account[] accounts) {
4169        // TODO : Check the unit test.
4170        HashSet<Account> existingAccounts = new HashSet<Account>();
4171        mDb.beginTransaction();
4172        try {
4173            findValidAccounts(existingAccounts);
4174
4175            // Add a row to the ACCOUNTS table for each new account
4176            for (Account account : accounts) {
4177                if (!existingAccounts.contains(account)) {
4178                    mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
4179                            + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)",
4180                            new String[] {account.name, account.type});
4181                }
4182            }
4183
4184            // Remove all valid accounts from the existing account set. What is left
4185            // in the accountsToDelete set will be extra accounts whose data must be deleted.
4186            HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts);
4187            for (Account account : accounts) {
4188                accountsToDelete.remove(account);
4189            }
4190
4191            for (Account account : accountsToDelete) {
4192                Log.d(TAG, "removing data for removed account " + account);
4193                String[] params = new String[] {account.name, account.type};
4194                mDb.execSQL(
4195                        "DELETE FROM " + Tables.GROUPS +
4196                        " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
4197                                " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
4198                mDb.execSQL(
4199                        "DELETE FROM " + Tables.PRESENCE +
4200                        " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
4201                                "SELECT " + RawContacts._ID +
4202                                " FROM " + Tables.RAW_CONTACTS +
4203                                " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
4204                                " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params);
4205                mDb.execSQL(
4206                        "DELETE FROM " + Tables.RAW_CONTACTS +
4207                        " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
4208                        " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
4209                mDb.execSQL(
4210                        "DELETE FROM " + Tables.SETTINGS +
4211                        " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
4212                        " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
4213                mDb.execSQL(
4214                        "DELETE FROM " + Tables.ACCOUNTS +
4215                        " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
4216                        " AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
4217            }
4218
4219            if (!accountsToDelete.isEmpty()) {
4220                // Find all aggregated contacts that used to contain the raw contacts
4221                // we have just deleted and see if they are still referencing the deleted
4222                // names of photos.  If so, fix up those contacts.
4223                HashSet<Long> orphanContactIds = Sets.newHashSet();
4224                Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID +
4225                        " FROM " + Tables.CONTACTS +
4226                        " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
4227                                Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
4228                                        "(SELECT " + RawContacts._ID +
4229                                        " FROM " + Tables.RAW_CONTACTS + "))" +
4230                        " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
4231                                Contacts.PHOTO_ID + " NOT IN " +
4232                                        "(SELECT " + Data._ID +
4233                                        " FROM " + Tables.DATA + "))", null);
4234                try {
4235                    while (cursor.moveToNext()) {
4236                        orphanContactIds.add(cursor.getLong(0));
4237                    }
4238                } finally {
4239                    cursor.close();
4240                }
4241
4242                for (Long contactId : orphanContactIds) {
4243                    mContactAggregator.updateAggregateData(contactId);
4244                }
4245            }
4246
4247            mDbHelper.updateAllVisible();
4248
4249            mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
4250            mDb.setTransactionSuccessful();
4251        } finally {
4252            mDb.endTransaction();
4253        }
4254        mAccountWritability.clear();
4255    }
4256
4257    /**
4258     * Finds all distinct accounts present in the specified table.
4259     */
4260    private void findValidAccounts(Set<Account> validAccounts) {
4261        Cursor c = mDb.rawQuery(
4262                "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE +
4263                " FROM " + Tables.ACCOUNTS, null);
4264        try {
4265            while (c.moveToNext()) {
4266                if (!c.isNull(0) || !c.isNull(1)) {
4267                    validAccounts.add(new Account(c.getString(0), c.getString(1)));
4268                }
4269            }
4270        } finally {
4271            c.close();
4272        }
4273    }
4274
4275    /**
4276     * Test all against {@link TextUtils#isEmpty(CharSequence)}.
4277     */
4278    private static boolean areAllEmpty(ContentValues values, String[] keys) {
4279        for (String key : keys) {
4280            if (!TextUtils.isEmpty(values.getAsString(key))) {
4281                return false;
4282            }
4283        }
4284        return true;
4285    }
4286
4287    /**
4288     * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
4289     */
4290    private static boolean areAnySpecified(ContentValues values, String[] keys) {
4291        for (String key : keys) {
4292            if (values.containsKey(key)) {
4293                return true;
4294            }
4295        }
4296        return false;
4297    }
4298
4299    @Override
4300    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
4301            String sortOrder) {
4302        if (VERBOSE_LOGGING) {
4303            Log.v(TAG, "query: " + uri);
4304        }
4305
4306        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
4307
4308        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
4309        String groupBy = null;
4310        String limit = getLimit(uri);
4311
4312        // TODO: Consider writing a test case for RestrictionExceptions when you
4313        // write a new query() block to make sure it protects restricted data.
4314        final int match = sUriMatcher.match(uri);
4315        switch (match) {
4316            case SYNCSTATE:
4317                return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
4318                        sortOrder);
4319
4320            case CONTACTS: {
4321                setTablesAndProjectionMapForContacts(qb, uri, projection);
4322                break;
4323            }
4324
4325            case CONTACTS_ID: {
4326                long contactId = ContentUris.parseId(uri);
4327                setTablesAndProjectionMapForContacts(qb, uri, projection);
4328                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4329                qb.appendWhere(Contacts._ID + "=?");
4330                break;
4331            }
4332
4333            case CONTACTS_LOOKUP:
4334            case CONTACTS_LOOKUP_ID: {
4335                List<String> pathSegments = uri.getPathSegments();
4336                int segmentCount = pathSegments.size();
4337                if (segmentCount < 3) {
4338                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
4339                            "Missing a lookup key", uri));
4340                }
4341                String lookupKey = pathSegments.get(2);
4342                if (segmentCount == 4) {
4343                    // TODO: pull this out into a method and generalize to not require contactId
4344                    long contactId = Long.parseLong(pathSegments.get(3));
4345                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
4346                    setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
4347                    String[] args;
4348                    if (selectionArgs == null) {
4349                        args = new String[2];
4350                    } else {
4351                        args = new String[selectionArgs.length + 2];
4352                        System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
4353                    }
4354                    args[0] = String.valueOf(contactId);
4355                    args[1] = Uri.encode(lookupKey);
4356                    lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
4357                    Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
4358                            groupBy, limit);
4359                    if (c.getCount() != 0) {
4360                        return c;
4361                    }
4362
4363                    c.close();
4364                }
4365
4366                setTablesAndProjectionMapForContacts(qb, uri, projection);
4367                selectionArgs = insertSelectionArg(selectionArgs,
4368                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
4369                qb.appendWhere(Contacts._ID + "=?");
4370                break;
4371            }
4372
4373            case CONTACTS_LOOKUP_DATA:
4374            case CONTACTS_LOOKUP_ID_DATA: {
4375                List<String> pathSegments = uri.getPathSegments();
4376                int segmentCount = pathSegments.size();
4377                if (segmentCount < 4) {
4378                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
4379                            "Missing a lookup key", uri));
4380                }
4381                String lookupKey = pathSegments.get(2);
4382                if (segmentCount == 5) {
4383                    long contactId = Long.parseLong(pathSegments.get(3));
4384                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
4385                    setTablesAndProjectionMapForData(lookupQb, uri, projection, false);
4386                    String[] args;
4387                    if (selectionArgs == null) {
4388                        args = new String[2];
4389                    } else {
4390                        args = new String[selectionArgs.length + 2];
4391                        System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
4392                    }
4393                    args[0] = String.valueOf(contactId);
4394                    args[1] = Uri.encode(lookupKey);
4395                    lookupQb.appendWhere(" AND " + Data.CONTACT_ID + "=?"
4396                            + " AND " + Data.LOOKUP_KEY + "=?");
4397                    Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
4398                            groupBy, limit);
4399                    if (c.getCount() != 0) {
4400                        return c;
4401                    }
4402
4403                    c.close();
4404
4405                    // TODO see if the contact exists but has no data rows (rare)
4406                }
4407
4408                setTablesAndProjectionMapForData(qb, uri, projection, false);
4409                selectionArgs = insertSelectionArg(selectionArgs,
4410                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
4411                qb.appendWhere(" AND " + Data.CONTACT_ID + "=?");
4412                break;
4413            }
4414
4415            case CONTACTS_AS_VCARD: {
4416                // When reading as vCard always use restricted view
4417                final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
4418                qb.setTables(mDbHelper.getContactView(true /* require restricted */));
4419                qb.setProjectionMap(sContactsVCardProjectionMap);
4420                selectionArgs = insertSelectionArg(selectionArgs,
4421                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
4422                qb.appendWhere(Contacts._ID + "=?");
4423                break;
4424            }
4425
4426            case CONTACTS_AS_MULTI_VCARD: {
4427                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
4428                String currentDateString = dateFormat.format(new Date()).toString();
4429                return db.rawQuery(
4430                    "SELECT" +
4431                    " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," +
4432                    " NULL AS " + OpenableColumns.SIZE,
4433                    new String[] { currentDateString });
4434            }
4435
4436            case CONTACTS_FILTER: {
4437                String filterParam = "";
4438                if (uri.getPathSegments().size() > 2) {
4439                    filterParam = uri.getLastPathSegment();
4440                }
4441                setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam);
4442                break;
4443            }
4444
4445            case CONTACTS_STREQUENT_FILTER:
4446            case CONTACTS_STREQUENT: {
4447                String filterSql = null;
4448                if (match == CONTACTS_STREQUENT_FILTER
4449                        && uri.getPathSegments().size() > 3) {
4450                    String filterParam = uri.getLastPathSegment();
4451                    StringBuilder sb = new StringBuilder();
4452                    sb.append(Contacts._ID + " IN ");
4453                    appendContactFilterAsNestedQuery(sb, filterParam);
4454                    filterSql = sb.toString();
4455                }
4456
4457                setTablesAndProjectionMapForContacts(qb, uri, projection);
4458
4459                String[] starredProjection = null;
4460                String[] frequentProjection = null;
4461                if (projection != null) {
4462                    starredProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
4463                    frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
4464                }
4465
4466                // Build the first query for starred
4467                if (filterSql != null) {
4468                    qb.appendWhere(filterSql);
4469                }
4470                qb.setProjectionMap(sStrequentStarredProjectionMap);
4471                final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1",
4472                        null, Contacts._ID, null, null, null);
4473
4474                // Build the second query for frequent
4475                qb = new SQLiteQueryBuilder();
4476                setTablesAndProjectionMapForContacts(qb, uri, projection);
4477                if (filterSql != null) {
4478                    qb.appendWhere(filterSql);
4479                }
4480                qb.setProjectionMap(sStrequentFrequentProjectionMap);
4481                final String frequentQuery = qb.buildQuery(frequentProjection,
4482                        Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
4483                        + " = 0 OR " + Contacts.STARRED + " IS NULL)",
4484                        null, Contacts._ID, null, null, null);
4485
4486                // Put them together
4487                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
4488                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
4489                Cursor c = db.rawQuery(query, null);
4490                if (c != null) {
4491                    c.setNotificationUri(getContext().getContentResolver(),
4492                            ContactsContract.AUTHORITY_URI);
4493                }
4494                return c;
4495            }
4496
4497            case CONTACTS_GROUP: {
4498                setTablesAndProjectionMapForContacts(qb, uri, projection);
4499                if (uri.getPathSegments().size() > 2) {
4500                    qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
4501                    selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4502                }
4503                break;
4504            }
4505
4506            case CONTACTS_DATA: {
4507                long contactId = Long.parseLong(uri.getPathSegments().get(1));
4508                setTablesAndProjectionMapForData(qb, uri, projection, false);
4509                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4510                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
4511                break;
4512            }
4513
4514            case CONTACTS_PHOTO: {
4515                long contactId = Long.parseLong(uri.getPathSegments().get(1));
4516                setTablesAndProjectionMapForData(qb, uri, projection, false);
4517                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4518                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
4519                qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
4520                break;
4521            }
4522
4523            case PHONES: {
4524                setTablesAndProjectionMapForData(qb, uri, projection, false);
4525                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
4526                break;
4527            }
4528
4529            case PHONES_ID: {
4530                setTablesAndProjectionMapForData(qb, uri, projection, false);
4531                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4532                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
4533                qb.appendWhere(" AND " + Data._ID + "=?");
4534                break;
4535            }
4536
4537            case PHONES_FILTER: {
4538                setTablesAndProjectionMapForData(qb, uri, projection, true);
4539                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
4540                if (uri.getPathSegments().size() > 2) {
4541                    String filterParam = uri.getLastPathSegment();
4542                    StringBuilder sb = new StringBuilder();
4543                    sb.append(" AND (");
4544
4545                    boolean hasCondition = false;
4546                    boolean orNeeded = false;
4547                    String normalizedName = NameNormalizer.normalize(filterParam);
4548                    if (normalizedName.length() > 0) {
4549                        sb.append(Data.RAW_CONTACT_ID + " IN ");
4550                        appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
4551                        orNeeded = true;
4552                        hasCondition = true;
4553                    }
4554
4555                    if (isPhoneNumber(filterParam)) {
4556                        if (orNeeded) {
4557                            sb.append(" OR ");
4558                        }
4559                        String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam);
4560                        String reversed = PhoneNumberUtils.getStrippedReversed(number);
4561                        sb.append(Data._ID +
4562                                " IN (SELECT " + PhoneLookupColumns.DATA_ID
4563                                  + " FROM " + Tables.PHONE_LOOKUP
4564                                  + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%");
4565                        sb.append(reversed);
4566                        sb.append("')");
4567                        hasCondition = true;
4568                    }
4569
4570                    if (!hasCondition) {
4571                        // If it is neither a phone number nor a name, the query should return
4572                        // an empty cursor.  Let's ensure that.
4573                        sb.append("0");
4574                    }
4575                    sb.append(")");
4576                    qb.appendWhere(sb);
4577                }
4578                groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
4579                if (sortOrder == null) {
4580                    sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
4581                }
4582                break;
4583            }
4584
4585            case EMAILS: {
4586                setTablesAndProjectionMapForData(qb, uri, projection, false);
4587                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
4588                break;
4589            }
4590
4591            case EMAILS_ID: {
4592                setTablesAndProjectionMapForData(qb, uri, projection, false);
4593                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4594                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"
4595                        + " AND " + Data._ID + "=?");
4596                break;
4597            }
4598
4599            case EMAILS_LOOKUP: {
4600                setTablesAndProjectionMapForData(qb, uri, projection, false);
4601                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
4602                if (uri.getPathSegments().size() > 2) {
4603                    String email = uri.getLastPathSegment();
4604                    String address = mDbHelper.extractAddressFromEmailAddress(email);
4605                    selectionArgs = insertSelectionArg(selectionArgs, address);
4606                    qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)");
4607                }
4608                break;
4609            }
4610
4611            case EMAILS_FILTER: {
4612                setTablesAndProjectionMapForData(qb, uri, projection, true);
4613                String filterParam = null;
4614                if (uri.getPathSegments().size() > 3) {
4615                    filterParam = uri.getLastPathSegment();
4616                    if (TextUtils.isEmpty(filterParam)) {
4617                        filterParam = null;
4618                    }
4619                }
4620
4621                if (filterParam == null) {
4622                    // If the filter is unspecified, return nothing
4623                    qb.appendWhere(" AND 0");
4624                } else {
4625                    StringBuilder sb = new StringBuilder();
4626                    sb.append(" AND " + Data._ID + " IN (");
4627                    sb.append(
4628                            "SELECT " + Data._ID +
4629                            " FROM " + Tables.DATA +
4630                            " WHERE " + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
4631                            " AND " + Data.DATA1 + " LIKE ");
4632                    DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
4633                    if (!filterParam.contains("@")) {
4634                        String normalizedName = NameNormalizer.normalize(filterParam);
4635                        if (normalizedName.length() > 0) {
4636
4637                            /*
4638                             * Using a UNION instead of an "OR" to make SQLite use the right
4639                             * indexes. We need it to use the (mimetype,data1) index for the
4640                             * email lookup (see above), but not for the name lookup.
4641                             * SQLite is not smart enough to use the index on one side of an OR
4642                             * but not on the other. Using two separate nested queries
4643                             * and a UNION between them does the job.
4644                             */
4645                            sb.append(
4646                                    " UNION SELECT " + Data._ID +
4647                                    " FROM " + Tables.DATA +
4648                                    " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
4649                                    " AND " + Data.RAW_CONTACT_ID + " IN ");
4650                            appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
4651                        }
4652                    }
4653                    sb.append(")");
4654                    qb.appendWhere(sb);
4655                }
4656                groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
4657                if (sortOrder == null) {
4658                    sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
4659                }
4660                break;
4661            }
4662
4663            case POSTALS: {
4664                setTablesAndProjectionMapForData(qb, uri, projection, false);
4665                qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
4666                        + StructuredPostal.CONTENT_ITEM_TYPE + "'");
4667                break;
4668            }
4669
4670            case POSTALS_ID: {
4671                setTablesAndProjectionMapForData(qb, uri, projection, false);
4672                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4673                qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
4674                        + StructuredPostal.CONTENT_ITEM_TYPE + "'");
4675                qb.appendWhere(" AND " + Data._ID + "=?");
4676                break;
4677            }
4678
4679            case RAW_CONTACTS: {
4680                setTablesAndProjectionMapForRawContacts(qb, uri);
4681                break;
4682            }
4683
4684            case RAW_CONTACTS_ID: {
4685                long rawContactId = ContentUris.parseId(uri);
4686                setTablesAndProjectionMapForRawContacts(qb, uri);
4687                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
4688                qb.appendWhere(" AND " + RawContacts._ID + "=?");
4689                break;
4690            }
4691
4692            case RAW_CONTACTS_DATA: {
4693                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
4694                setTablesAndProjectionMapForData(qb, uri, projection, false);
4695                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
4696                qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
4697                break;
4698            }
4699
4700            case DATA: {
4701                setTablesAndProjectionMapForData(qb, uri, projection, false);
4702                break;
4703            }
4704
4705            case DATA_ID: {
4706                setTablesAndProjectionMapForData(qb, uri, projection, false);
4707                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4708                qb.appendWhere(" AND " + Data._ID + "=?");
4709                break;
4710            }
4711
4712            case PHONE_LOOKUP: {
4713
4714                if (TextUtils.isEmpty(sortOrder)) {
4715                    // Default the sort order to something reasonable so we get consistent
4716                    // results when callers don't request an ordering
4717                    sortOrder = RawContactsColumns.CONCRETE_ID;
4718                }
4719
4720                String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
4721                mDbHelper.buildPhoneLookupAndContactQuery(qb, number);
4722                qb.setProjectionMap(sPhoneLookupProjectionMap);
4723
4724                // Phone lookup cannot be combined with a selection
4725                selection = null;
4726                selectionArgs = null;
4727                break;
4728            }
4729
4730            case GROUPS: {
4731                qb.setTables(mDbHelper.getGroupView());
4732                qb.setProjectionMap(sGroupsProjectionMap);
4733                appendAccountFromParameter(qb, uri);
4734                break;
4735            }
4736
4737            case GROUPS_ID: {
4738                qb.setTables(mDbHelper.getGroupView());
4739                qb.setProjectionMap(sGroupsProjectionMap);
4740                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4741                qb.appendWhere(Groups._ID + "=?");
4742                break;
4743            }
4744
4745            case GROUPS_SUMMARY: {
4746                qb.setTables(mDbHelper.getGroupView() + " AS groups");
4747                qb.setProjectionMap(sGroupsSummaryProjectionMap);
4748                appendAccountFromParameter(qb, uri);
4749                groupBy = Groups._ID;
4750                break;
4751            }
4752
4753            case AGGREGATION_EXCEPTIONS: {
4754                qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
4755                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
4756                break;
4757            }
4758
4759            case AGGREGATION_SUGGESTIONS: {
4760                long contactId = Long.parseLong(uri.getPathSegments().get(1));
4761                String filter = null;
4762                if (uri.getPathSegments().size() > 3) {
4763                    filter = uri.getPathSegments().get(3);
4764                }
4765                final int maxSuggestions;
4766                if (limit != null) {
4767                    maxSuggestions = Integer.parseInt(limit);
4768                } else {
4769                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
4770                }
4771
4772                setTablesAndProjectionMapForContacts(qb, uri, projection);
4773
4774                return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId,
4775                        maxSuggestions, filter);
4776            }
4777
4778            case SETTINGS: {
4779                qb.setTables(Tables.SETTINGS);
4780                qb.setProjectionMap(sSettingsProjectionMap);
4781                appendAccountFromParameter(qb, uri);
4782
4783                // When requesting specific columns, this query requires
4784                // late-binding of the GroupMembership MIME-type.
4785                final String groupMembershipMimetypeId = Long.toString(mDbHelper
4786                        .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
4787                if (projection != null && projection.length != 0 &&
4788                        mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) {
4789                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
4790                }
4791                if (projection != null && projection.length != 0 &&
4792                        mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) {
4793                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
4794                }
4795
4796                break;
4797            }
4798
4799            case STATUS_UPDATES: {
4800                setTableAndProjectionMapForStatusUpdates(qb, projection);
4801                break;
4802            }
4803
4804            case STATUS_UPDATES_ID: {
4805                setTableAndProjectionMapForStatusUpdates(qb, projection);
4806                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4807                qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
4808                break;
4809            }
4810
4811            case SEARCH_SUGGESTIONS: {
4812                return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
4813            }
4814
4815            case SEARCH_SHORTCUT: {
4816                String lookupKey = uri.getLastPathSegment();
4817                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection);
4818            }
4819
4820            case LIVE_FOLDERS_CONTACTS:
4821                qb.setTables(mDbHelper.getContactView());
4822                qb.setProjectionMap(sLiveFoldersProjectionMap);
4823                break;
4824
4825            case LIVE_FOLDERS_CONTACTS_WITH_PHONES:
4826                qb.setTables(mDbHelper.getContactView());
4827                qb.setProjectionMap(sLiveFoldersProjectionMap);
4828                qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1");
4829                break;
4830
4831            case LIVE_FOLDERS_CONTACTS_FAVORITES:
4832                qb.setTables(mDbHelper.getContactView());
4833                qb.setProjectionMap(sLiveFoldersProjectionMap);
4834                qb.appendWhere(Contacts.STARRED + "=1");
4835                break;
4836
4837            case LIVE_FOLDERS_CONTACTS_GROUP_NAME:
4838                qb.setTables(mDbHelper.getContactView());
4839                qb.setProjectionMap(sLiveFoldersProjectionMap);
4840                qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
4841                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4842                break;
4843
4844            case RAW_CONTACT_ENTITIES: {
4845                setTablesAndProjectionMapForRawContactsEntities(qb, uri);
4846                break;
4847            }
4848
4849            case RAW_CONTACT_ENTITY_ID: {
4850                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
4851                setTablesAndProjectionMapForRawContactsEntities(qb, uri);
4852                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
4853                qb.appendWhere(" AND " + RawContacts._ID + "=?");
4854                break;
4855            }
4856
4857            case PROVIDER_STATUS: {
4858                return queryProviderStatus(uri, projection);
4859            }
4860
4861            default:
4862                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
4863                        sortOrder, limit);
4864        }
4865
4866        qb.setStrictProjectionMap(true);
4867
4868        Cursor cursor =
4869                query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
4870        if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
4871            cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder);
4872        }
4873        return cursor;
4874    }
4875
4876    private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
4877            String selection, String[] selectionArgs, String sortOrder, String groupBy,
4878            String limit) {
4879        if (projection != null && projection.length == 1
4880                && BaseColumns._COUNT.equals(projection[0])) {
4881            qb.setProjectionMap(sCountProjectionMap);
4882        }
4883        final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
4884                sortOrder, limit);
4885        if (c != null) {
4886            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
4887        }
4888        return c;
4889    }
4890
4891    /**
4892     * Creates a single-row cursor containing the current status of the provider.
4893     */
4894    private Cursor queryProviderStatus(Uri uri, String[] projection) {
4895        MatrixCursor cursor = new MatrixCursor(projection);
4896        RowBuilder row = cursor.newRow();
4897        for (int i = 0; i < projection.length; i++) {
4898            if (ProviderStatus.STATUS.equals(projection[i])) {
4899                row.add(mProviderStatus);
4900            } else if (ProviderStatus.DATA1.equals(projection[i])) {
4901                row.add(mEstimatedStorageRequirement);
4902            }
4903        }
4904        return cursor;
4905    }
4906
4907
4908    private static final class AddressBookIndexQuery {
4909        public static final String LETTER = "letter";
4910        public static final String TITLE = "title";
4911        public static final String COUNT = "count";
4912
4913        public static final String[] COLUMNS = new String[] {
4914                LETTER, TITLE, COUNT
4915        };
4916
4917        public static final int COLUMN_LETTER = 0;
4918        public static final int COLUMN_TITLE = 1;
4919        public static final int COLUMN_COUNT = 2;
4920
4921        public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
4922    }
4923
4924    /**
4925     * Computes counts by the address book index titles and adds the resulting tally
4926     * to the returned cursor as a bundle of extras.
4927     */
4928    private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db,
4929            SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) {
4930        String sortKey;
4931
4932        // The sort order suffix could be something like "DESC".
4933        // We want to preserve it in the query even though we will change
4934        // the sort column itself.
4935        String sortOrderSuffix = "";
4936        if (sortOrder != null) {
4937            int spaceIndex = sortOrder.indexOf(' ');
4938            if (spaceIndex != -1) {
4939                sortKey = sortOrder.substring(0, spaceIndex);
4940                sortOrderSuffix = sortOrder.substring(spaceIndex);
4941            } else {
4942                sortKey = sortOrder;
4943            }
4944        } else {
4945            sortKey = Contacts.SORT_KEY_PRIMARY;
4946        }
4947
4948        String locale = getLocale().toString();
4949        HashMap<String, String> projectionMap = Maps.newHashMap();
4950        projectionMap.put(AddressBookIndexQuery.LETTER,
4951                "SUBSTR(" + sortKey + ",1,1) AS " + AddressBookIndexQuery.LETTER);
4952
4953        /**
4954         * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3,
4955         * to map the first letter of the sort key to a character that is traditionally
4956         * used in phonebooks to represent that letter.  For example, in Korean it will
4957         * be the first consonant in the letter; for Japanese it will be Hiragana rather
4958         * than Katakana.
4959         */
4960        projectionMap.put(AddressBookIndexQuery.TITLE,
4961                "GET_PHONEBOOK_INDEX(SUBSTR(" + sortKey + ",1,1),'" + locale + "')"
4962                        + " AS " + AddressBookIndexQuery.TITLE);
4963        projectionMap.put(AddressBookIndexQuery.COUNT,
4964                "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT);
4965        qb.setProjectionMap(projectionMap);
4966
4967        Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
4968                AddressBookIndexQuery.ORDER_BY, null /* having */,
4969                AddressBookIndexQuery.ORDER_BY + sortOrderSuffix);
4970
4971        try {
4972            int groupCount = indexCursor.getCount();
4973            String titles[] = new String[groupCount];
4974            int counts[] = new int[groupCount];
4975            int indexCount = 0;
4976            String currentTitle = null;
4977
4978            // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up
4979            // with multiple entries for the same title.  The following code
4980            // collapses those duplicates.
4981            for (int i = 0; i < groupCount; i++) {
4982                indexCursor.moveToNext();
4983                String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
4984                int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
4985                if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
4986                    titles[indexCount] = currentTitle = title;
4987                    counts[indexCount] = count;
4988                    indexCount++;
4989                } else {
4990                    counts[indexCount - 1] += count;
4991                }
4992            }
4993
4994            if (indexCount < groupCount) {
4995                String[] newTitles = new String[indexCount];
4996                System.arraycopy(titles, 0, newTitles, 0, indexCount);
4997                titles = newTitles;
4998
4999                int[] newCounts = new int[indexCount];
5000                System.arraycopy(counts, 0, newCounts, 0, indexCount);
5001                counts = newCounts;
5002            }
5003
5004            final Bundle bundle = new Bundle();
5005            bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles);
5006            bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts);
5007            return new CursorWrapper(cursor) {
5008
5009                @Override
5010                public Bundle getExtras() {
5011                    return bundle;
5012                }
5013            };
5014        } finally {
5015            indexCursor.close();
5016        }
5017    }
5018
5019    /**
5020     * Returns the contact Id for the contact identified by the lookupKey.
5021     * Robust against changes in the lookup key: if the key has changed, will
5022     * look up the contact by the raw contact IDs or name encoded in the lookup
5023     * key.
5024     */
5025    public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
5026        ContactLookupKey key = new ContactLookupKey();
5027        ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
5028
5029        long contactId = -1;
5030        if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) {
5031            contactId = lookupContactIdBySourceIds(db, segments);
5032            if (contactId != -1) {
5033                return contactId;
5034            }
5035        }
5036
5037        boolean hasRawContactIds =
5038                lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID);
5039        if (hasRawContactIds) {
5040            contactId = lookupContactIdByRawContactIds(db, segments);
5041            if (contactId != -1) {
5042                return contactId;
5043            }
5044        }
5045
5046        if (hasRawContactIds
5047                || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) {
5048            contactId = lookupContactIdByDisplayNames(db, segments);
5049        }
5050
5051        return contactId;
5052    }
5053
5054    private interface LookupBySourceIdQuery {
5055        String TABLE = Tables.RAW_CONTACTS;
5056
5057        String COLUMNS[] = {
5058                RawContacts.CONTACT_ID,
5059                RawContacts.ACCOUNT_TYPE,
5060                RawContacts.ACCOUNT_NAME,
5061                RawContacts.SOURCE_ID
5062        };
5063
5064        int CONTACT_ID = 0;
5065        int ACCOUNT_TYPE = 1;
5066        int ACCOUNT_NAME = 2;
5067        int SOURCE_ID = 3;
5068    }
5069
5070    private long lookupContactIdBySourceIds(SQLiteDatabase db,
5071                ArrayList<LookupKeySegment> segments) {
5072        StringBuilder sb = new StringBuilder();
5073        sb.append(RawContacts.SOURCE_ID + " IN (");
5074        for (int i = 0; i < segments.size(); i++) {
5075            LookupKeySegment segment = segments.get(i);
5076            if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) {
5077                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
5078                sb.append(",");
5079            }
5080        }
5081        sb.setLength(sb.length() - 1);      // Last comma
5082        sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
5083
5084        Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
5085                 sb.toString(), null, null, null, null);
5086        try {
5087            while (c.moveToNext()) {
5088                String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE);
5089                String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
5090                int accountHashCode =
5091                        ContactLookupKey.getAccountHashCode(accountType, accountName);
5092                String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
5093                for (int i = 0; i < segments.size(); i++) {
5094                    LookupKeySegment segment = segments.get(i);
5095                    if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID
5096                            && accountHashCode == segment.accountHashCode
5097                            && segment.key.equals(sourceId)) {
5098                        segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
5099                        break;
5100                    }
5101                }
5102            }
5103        } finally {
5104            c.close();
5105        }
5106
5107        return getMostReferencedContactId(segments);
5108    }
5109
5110    private interface LookupByRawContactIdQuery {
5111        String TABLE = Tables.RAW_CONTACTS;
5112
5113        String COLUMNS[] = {
5114                RawContacts.CONTACT_ID,
5115                RawContacts.ACCOUNT_TYPE,
5116                RawContacts.ACCOUNT_NAME,
5117                RawContacts._ID,
5118        };
5119
5120        int CONTACT_ID = 0;
5121        int ACCOUNT_TYPE = 1;
5122        int ACCOUNT_NAME = 2;
5123        int ID = 3;
5124    }
5125
5126    private long lookupContactIdByRawContactIds(SQLiteDatabase db,
5127            ArrayList<LookupKeySegment> segments) {
5128        StringBuilder sb = new StringBuilder();
5129        sb.append(RawContacts._ID + " IN (");
5130        for (int i = 0; i < segments.size(); i++) {
5131            LookupKeySegment segment = segments.get(i);
5132            if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
5133                sb.append(segment.rawContactId);
5134                sb.append(",");
5135            }
5136        }
5137        sb.setLength(sb.length() - 1);      // Last comma
5138        sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
5139
5140        Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS,
5141                 sb.toString(), null, null, null, null);
5142        try {
5143            while (c.moveToNext()) {
5144                String accountType = c.getString(LookupByRawContactIdQuery.ACCOUNT_TYPE);
5145                String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME);
5146                int accountHashCode =
5147                        ContactLookupKey.getAccountHashCode(accountType, accountName);
5148                String rawContactId = c.getString(LookupByRawContactIdQuery.ID);
5149                for (int i = 0; i < segments.size(); i++) {
5150                    LookupKeySegment segment = segments.get(i);
5151                    if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID
5152                            && accountHashCode == segment.accountHashCode
5153                            && segment.rawContactId.equals(rawContactId)) {
5154                        segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID);
5155                        break;
5156                    }
5157                }
5158            }
5159        } finally {
5160            c.close();
5161        }
5162
5163        return getMostReferencedContactId(segments);
5164    }
5165
5166    private interface LookupByDisplayNameQuery {
5167        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
5168
5169        String COLUMNS[] = {
5170                RawContacts.CONTACT_ID,
5171                RawContacts.ACCOUNT_TYPE,
5172                RawContacts.ACCOUNT_NAME,
5173                NameLookupColumns.NORMALIZED_NAME
5174        };
5175
5176        int CONTACT_ID = 0;
5177        int ACCOUNT_TYPE = 1;
5178        int ACCOUNT_NAME = 2;
5179        int NORMALIZED_NAME = 3;
5180    }
5181
5182    private long lookupContactIdByDisplayNames(SQLiteDatabase db,
5183                ArrayList<LookupKeySegment> segments) {
5184        StringBuilder sb = new StringBuilder();
5185        sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
5186        for (int i = 0; i < segments.size(); i++) {
5187            LookupKeySegment segment = segments.get(i);
5188            if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
5189                    || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
5190                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
5191                sb.append(",");
5192            }
5193        }
5194        sb.setLength(sb.length() - 1);      // Last comma
5195        sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
5196                + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
5197
5198        Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
5199                 sb.toString(), null, null, null, null);
5200        try {
5201            while (c.moveToNext()) {
5202                String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE);
5203                String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
5204                int accountHashCode =
5205                        ContactLookupKey.getAccountHashCode(accountType, accountName);
5206                String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
5207                for (int i = 0; i < segments.size(); i++) {
5208                    LookupKeySegment segment = segments.get(i);
5209                    if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
5210                            || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID)
5211                            && accountHashCode == segment.accountHashCode
5212                            && segment.key.equals(name)) {
5213                        segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
5214                        break;
5215                    }
5216                }
5217            }
5218        } finally {
5219            c.close();
5220        }
5221
5222        return getMostReferencedContactId(segments);
5223    }
5224
5225    private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) {
5226        for (int i = 0; i < segments.size(); i++) {
5227            LookupKeySegment segment = segments.get(i);
5228            if (segment.lookupType == lookupType) {
5229                return true;
5230            }
5231        }
5232
5233        return false;
5234    }
5235
5236    public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
5237        mContactAggregator.updateLookupKeyForRawContact(db, rawContactId);
5238    }
5239
5240    /**
5241     * Returns the contact ID that is mentioned the highest number of times.
5242     */
5243    private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
5244        Collections.sort(segments);
5245
5246        long bestContactId = -1;
5247        int bestRefCount = 0;
5248
5249        long contactId = -1;
5250        int count = 0;
5251
5252        int segmentCount = segments.size();
5253        for (int i = 0; i < segmentCount; i++) {
5254            LookupKeySegment segment = segments.get(i);
5255            if (segment.contactId != -1) {
5256                if (segment.contactId == contactId) {
5257                    count++;
5258                } else {
5259                    if (count > bestRefCount) {
5260                        bestContactId = contactId;
5261                        bestRefCount = count;
5262                    }
5263                    contactId = segment.contactId;
5264                    count = 1;
5265                }
5266            }
5267        }
5268        if (count > bestRefCount) {
5269            return contactId;
5270        } else {
5271            return bestContactId;
5272        }
5273    }
5274
5275    private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
5276            String[] projection) {
5277        StringBuilder sb = new StringBuilder();
5278        appendContactsTables(sb, uri, projection);
5279        qb.setTables(sb.toString());
5280        qb.setProjectionMap(sContactsProjectionMap);
5281    }
5282
5283    /**
5284     * Finds name lookup records matching the supplied filter, picks one arbitrary match per
5285     * contact and joins that with other contacts tables.
5286     */
5287    private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri,
5288            String[] projection, String filter) {
5289
5290        StringBuilder sb = new StringBuilder();
5291        appendContactsTables(sb, uri, projection);
5292
5293        sb.append(" JOIN (SELECT " +
5294                RawContacts.CONTACT_ID + " AS snippet_contact_id");
5295
5296        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA_ID)) {
5297            sb.append(", " + DataColumns.CONCRETE_ID + " AS "
5298                    + SearchSnippetColumns.SNIPPET_DATA_ID);
5299        }
5300
5301        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA1)) {
5302            sb.append(", " + Data.DATA1 + " AS " + SearchSnippetColumns.SNIPPET_DATA1);
5303        }
5304
5305        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA2)) {
5306            sb.append(", " + Data.DATA2 + " AS " + SearchSnippetColumns.SNIPPET_DATA2);
5307        }
5308
5309        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA3)) {
5310            sb.append(", " + Data.DATA3 + " AS " + SearchSnippetColumns.SNIPPET_DATA3);
5311        }
5312
5313        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA4)) {
5314            sb.append(", " + Data.DATA4 + " AS " + SearchSnippetColumns.SNIPPET_DATA4);
5315        }
5316
5317        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_MIMETYPE)) {
5318            sb.append(", (" +
5319                    "SELECT " + MimetypesColumns.MIMETYPE +
5320                    " FROM " + Tables.MIMETYPES +
5321                    " WHERE " + MimetypesColumns._ID + "=" + DataColumns.MIMETYPE_ID +
5322                    ") AS " + SearchSnippetColumns.SNIPPET_MIMETYPE);
5323        }
5324
5325        sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS + " WHERE ");
5326
5327        if (!TextUtils.isEmpty(filter)) {
5328            sb.append(DataColumns.CONCRETE_ID + " IN (");
5329
5330            // Construct a query that gives us exactly one data _id per matching contact.
5331            // MIN stands in for ANY in this context.
5332            sb.append(
5333                    "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" +
5334                    " FROM " + Tables.NAME_LOOKUP +
5335                    " JOIN " + Tables.RAW_CONTACTS +
5336                    " ON (" + RawContactsColumns.CONCRETE_ID
5337                            + "=" + Tables.NAME_LOOKUP + "." + NameLookupColumns.RAW_CONTACT_ID + ")" +
5338                    " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '");
5339            sb.append(NameNormalizer.normalize(filter));
5340            sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
5341                        " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" +
5342                    " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID +
5343                    ")");
5344        } else {
5345            sb.append("0");     // Empty filter - return an empty set
5346        }
5347
5348        sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)");
5349
5350        qb.setTables(sb.toString());
5351        qb.setProjectionMap(sContactsProjectionWithSnippetMap);
5352    }
5353
5354    private void appendContactsTables(StringBuilder sb, Uri uri, String[] projection) {
5355        boolean excludeRestrictedData = false;
5356        String requestingPackage = getQueryParameter(uri,
5357                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5358        if (requestingPackage != null) {
5359            excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5360        }
5361        sb.append(mDbHelper.getContactView(excludeRestrictedData));
5362        if (mDbHelper.isInProjection(projection,
5363                Contacts.CONTACT_PRESENCE)) {
5364            sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
5365                    " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")");
5366        }
5367        if (mDbHelper.isInProjection(projection,
5368                Contacts.CONTACT_STATUS,
5369                Contacts.CONTACT_STATUS_RES_PACKAGE,
5370                Contacts.CONTACT_STATUS_ICON,
5371                Contacts.CONTACT_STATUS_LABEL,
5372                Contacts.CONTACT_STATUS_TIMESTAMP)) {
5373            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
5374                    + ContactsStatusUpdatesColumns.ALIAS +
5375                    " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
5376                            + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
5377        }
5378    }
5379
5380    private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
5381        StringBuilder sb = new StringBuilder();
5382        boolean excludeRestrictedData = false;
5383        String requestingPackage = getQueryParameter(uri,
5384                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5385        if (requestingPackage != null) {
5386            excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5387        }
5388        sb.append(mDbHelper.getRawContactView(excludeRestrictedData));
5389        qb.setTables(sb.toString());
5390        qb.setProjectionMap(sRawContactsProjectionMap);
5391        appendAccountFromParameter(qb, uri);
5392    }
5393
5394    private void setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) {
5395        // Note: currently, "export only" equals to "restricted", but may not in the future.
5396        boolean excludeRestrictedData = readBooleanQueryParameter(uri,
5397                Data.FOR_EXPORT_ONLY, false);
5398
5399        String requestingPackage = getQueryParameter(uri,
5400                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5401        if (requestingPackage != null) {
5402            excludeRestrictedData = excludeRestrictedData
5403                    || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5404        }
5405        qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData));
5406        qb.setProjectionMap(sRawContactsEntityProjectionMap);
5407        appendAccountFromParameter(qb, uri);
5408    }
5409
5410    private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
5411            String[] projection, boolean distinct) {
5412        StringBuilder sb = new StringBuilder();
5413        // Note: currently, "export only" equals to "restricted", but may not in the future.
5414        boolean excludeRestrictedData = readBooleanQueryParameter(uri,
5415                Data.FOR_EXPORT_ONLY, false);
5416
5417        String requestingPackage = getQueryParameter(uri,
5418                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5419        if (requestingPackage != null) {
5420            excludeRestrictedData = excludeRestrictedData
5421                    || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5422        }
5423
5424        sb.append(mDbHelper.getDataView(excludeRestrictedData));
5425        sb.append(" data");
5426
5427        // Include aggregated presence when requested
5428        if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) {
5429            sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
5430                    " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "="
5431                    + RawContacts.CONTACT_ID + ")");
5432        }
5433
5434        // Include aggregated status updates when requested
5435        if (mDbHelper.isInProjection(projection,
5436                Data.CONTACT_STATUS,
5437                Data.CONTACT_STATUS_RES_PACKAGE,
5438                Data.CONTACT_STATUS_ICON,
5439                Data.CONTACT_STATUS_LABEL,
5440                Data.CONTACT_STATUS_TIMESTAMP)) {
5441            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
5442                    + ContactsStatusUpdatesColumns.ALIAS +
5443                    " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
5444                            + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
5445        }
5446
5447        // Include individual presence when requested
5448        if (mDbHelper.isInProjection(projection, Data.PRESENCE)) {
5449            sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
5450                    " ON (" + StatusUpdates.DATA_ID + "="
5451                    + DataColumns.CONCRETE_ID + ")");
5452        }
5453
5454        // Include individual status updates when requested
5455        if (mDbHelper.isInProjection(projection,
5456                Data.STATUS,
5457                Data.STATUS_RES_PACKAGE,
5458                Data.STATUS_ICON,
5459                Data.STATUS_LABEL,
5460                Data.STATUS_TIMESTAMP)) {
5461            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
5462                    " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
5463                            + DataColumns.CONCRETE_ID + ")");
5464        }
5465
5466        qb.setTables(sb.toString());
5467        qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap);
5468        appendAccountFromParameter(qb, uri);
5469    }
5470
5471    private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
5472            String[] projection) {
5473        StringBuilder sb = new StringBuilder();
5474        sb.append(mDbHelper.getDataView());
5475        sb.append(" data");
5476
5477        if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) {
5478            sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
5479                    " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID
5480                    + "=" + DataColumns.CONCRETE_ID + ")");
5481        }
5482
5483        if (mDbHelper.isInProjection(projection,
5484                StatusUpdates.STATUS,
5485                StatusUpdates.STATUS_RES_PACKAGE,
5486                StatusUpdates.STATUS_ICON,
5487                StatusUpdates.STATUS_LABEL,
5488                StatusUpdates.STATUS_TIMESTAMP)) {
5489            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
5490                    " ON(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID
5491                    + "=" + DataColumns.CONCRETE_ID + ")");
5492        }
5493        qb.setTables(sb.toString());
5494        qb.setProjectionMap(sStatusUpdatesProjectionMap);
5495    }
5496
5497    private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
5498        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
5499        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
5500
5501        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
5502        if (partialUri) {
5503            // Throw when either account is incomplete
5504            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
5505                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
5506        }
5507
5508        // Accounts are valid by only checking one parameter, since we've
5509        // already ruled out partial accounts.
5510        final boolean validAccount = !TextUtils.isEmpty(accountName);
5511        if (validAccount) {
5512            qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
5513                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
5514                    + RawContacts.ACCOUNT_TYPE + "="
5515                    + DatabaseUtils.sqlEscapeString(accountType));
5516        } else {
5517            qb.appendWhere("1");
5518        }
5519    }
5520
5521    private String appendAccountToSelection(Uri uri, String selection) {
5522        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
5523        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
5524
5525        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
5526        if (partialUri) {
5527            // Throw when either account is incomplete
5528            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
5529                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
5530        }
5531
5532        // Accounts are valid by only checking one parameter, since we've
5533        // already ruled out partial accounts.
5534        final boolean validAccount = !TextUtils.isEmpty(accountName);
5535        if (validAccount) {
5536            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
5537                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
5538                    + RawContacts.ACCOUNT_TYPE + "="
5539                    + DatabaseUtils.sqlEscapeString(accountType));
5540            if (!TextUtils.isEmpty(selection)) {
5541                selectionSb.append(" AND (");
5542                selectionSb.append(selection);
5543                selectionSb.append(')');
5544            }
5545            return selectionSb.toString();
5546        } else {
5547            return selection;
5548        }
5549    }
5550
5551    /**
5552     * Gets the value of the "limit" URI query parameter.
5553     *
5554     * @return A string containing a non-negative integer, or <code>null</code> if
5555     *         the parameter is not set, or is set to an invalid value.
5556     */
5557    private String getLimit(Uri uri) {
5558        String limitParam = getQueryParameter(uri, "limit");
5559        if (limitParam == null) {
5560            return null;
5561        }
5562        // make sure that the limit is a non-negative integer
5563        try {
5564            int l = Integer.parseInt(limitParam);
5565            if (l < 0) {
5566                Log.w(TAG, "Invalid limit parameter: " + limitParam);
5567                return null;
5568            }
5569            return String.valueOf(l);
5570        } catch (NumberFormatException ex) {
5571            Log.w(TAG, "Invalid limit parameter: " + limitParam);
5572            return null;
5573        }
5574    }
5575
5576    /**
5577     * Returns true if all the characters are meaningful as digits
5578     * in a phone number -- letters, digits, and a few punctuation marks.
5579     */
5580    private boolean isPhoneNumber(CharSequence cons) {
5581        int len = cons.length();
5582
5583        for (int i = 0; i < len; i++) {
5584            char c = cons.charAt(i);
5585
5586            if ((c >= '0') && (c <= '9')) {
5587                continue;
5588            }
5589            if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+')
5590                    || (c == '#') || (c == '*')) {
5591                continue;
5592            }
5593            if ((c >= 'A') && (c <= 'Z')) {
5594                continue;
5595            }
5596            if ((c >= 'a') && (c <= 'z')) {
5597                continue;
5598            }
5599
5600            return false;
5601        }
5602
5603        return true;
5604    }
5605
5606    String getContactsRestrictions() {
5607        if (mDbHelper.hasAccessToRestrictedData()) {
5608            return "1";
5609        } else {
5610            return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0";
5611        }
5612    }
5613
5614    public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
5615        if (mDbHelper.hasAccessToRestrictedData()) {
5616            return "1";
5617        } else {
5618            return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
5619                    + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
5620        }
5621    }
5622
5623    @Override
5624    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
5625        int match = sUriMatcher.match(uri);
5626        switch (match) {
5627            case CONTACTS_PHOTO: {
5628                return openPhotoAssetFile(uri, mode,
5629                        Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?",
5630                        new String[]{uri.getPathSegments().get(1)});
5631            }
5632
5633            case DATA_ID: {
5634                return openPhotoAssetFile(uri, mode,
5635                        Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'",
5636                        new String[]{uri.getPathSegments().get(1)});
5637            }
5638
5639            case CONTACTS_AS_VCARD: {
5640                final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
5641                mSelectionArgs1[0] = String.valueOf(lookupContactIdByLookupKey(mDb, lookupKey));
5642                final String selection = Contacts._ID + "=?";
5643
5644                // When opening a contact as file, we pass back contents as a
5645                // vCard-encoded stream. We build into a local buffer first,
5646                // then pipe into MemoryFile once the exact size is known.
5647                final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
5648                outputRawContactsAsVCard(localStream, selection, mSelectionArgs1);
5649                return buildAssetFileDescriptor(localStream);
5650            }
5651
5652            case CONTACTS_AS_MULTI_VCARD: {
5653                final String lookupKeys = uri.getPathSegments().get(2);
5654                final String[] loopupKeyList = lookupKeys.split(":");
5655                final StringBuilder inBuilder = new StringBuilder();
5656                int index = 0;
5657                // SQLite has limits on how many parameters can be used
5658                // so the IDs are concatenated to a query string here instead
5659                for (String lookupKey : loopupKeyList) {
5660                    if (index == 0) {
5661                        inBuilder.append("(");
5662                    } else {
5663                        inBuilder.append(",");
5664                    }
5665                    inBuilder.append(lookupContactIdByLookupKey(mDb, lookupKey));
5666                    index++;
5667                }
5668                inBuilder.append(')');
5669                final String selection = Contacts._ID + " IN " + inBuilder.toString();
5670
5671                // When opening a contact as file, we pass back contents as a
5672                // vCard-encoded stream. We build into a local buffer first,
5673                // then pipe into MemoryFile once the exact size is known.
5674                final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
5675                outputRawContactsAsVCard(localStream, selection, null);
5676                return buildAssetFileDescriptor(localStream);
5677            }
5678
5679            default:
5680                throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist",
5681                        uri));
5682        }
5683    }
5684
5685    private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection,
5686            String[] selectionArgs)
5687            throws FileNotFoundException {
5688        if (!"r".equals(mode)) {
5689            throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode
5690                    + " not supported.", uri));
5691        }
5692
5693        String sql =
5694                "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
5695                " WHERE " + selection;
5696        SQLiteDatabase db = mDbHelper.getReadableDatabase();
5697        return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql,
5698                selectionArgs);
5699    }
5700
5701    private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
5702
5703    /**
5704     * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the
5705     * contents of the given {@link ByteArrayOutputStream}.
5706     */
5707    private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
5708        AssetFileDescriptor fd = null;
5709        try {
5710            stream.flush();
5711
5712            final byte[] byteData = stream.toByteArray();
5713            final int size = byteData.length;
5714
5715            final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size);
5716            memoryFile.writeBytes(byteData, 0, 0, size);
5717            memoryFile.deactivate();
5718
5719            fd = AssetFileDescriptor.fromMemoryFile(memoryFile);
5720        } catch (IOException e) {
5721            Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString());
5722        }
5723        return fd;
5724    }
5725
5726    /**
5727     * Output {@link RawContacts} matching the requested selection in the vCard
5728     * format to the given {@link OutputStream}. This method returns silently if
5729     * any errors encountered.
5730     */
5731    private void outputRawContactsAsVCard(OutputStream stream, String selection,
5732            String[] selectionArgs) {
5733        final Context context = this.getContext();
5734        final VCardComposer composer =
5735                new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false);
5736        composer.addHandler(composer.new HandlerForOutputStream(stream));
5737
5738        // No extra checks since composer always uses restricted views
5739        if (!composer.init(selection, selectionArgs)) {
5740            Log.w(TAG, "Failed to init VCardComposer");
5741            return;
5742        }
5743
5744        while (!composer.isAfterLast()) {
5745            if (!composer.createOneEntry()) {
5746                Log.w(TAG, "Failed to output a contact.");
5747            }
5748        }
5749        composer.terminate();
5750    }
5751
5752    @Override
5753    public String getType(Uri uri) {
5754        final int match = sUriMatcher.match(uri);
5755        switch (match) {
5756            case CONTACTS:
5757                return Contacts.CONTENT_TYPE;
5758            case CONTACTS_LOOKUP:
5759            case CONTACTS_ID:
5760            case CONTACTS_LOOKUP_ID:
5761                return Contacts.CONTENT_ITEM_TYPE;
5762            case CONTACTS_AS_VCARD:
5763            case CONTACTS_AS_MULTI_VCARD:
5764                return Contacts.CONTENT_VCARD_TYPE;
5765            case RAW_CONTACTS:
5766                return RawContacts.CONTENT_TYPE;
5767            case RAW_CONTACTS_ID:
5768                return RawContacts.CONTENT_ITEM_TYPE;
5769            case DATA_ID:
5770                return mDbHelper.getDataMimeType(ContentUris.parseId(uri));
5771            case PHONES:
5772                return Phone.CONTENT_TYPE;
5773            case PHONES_ID:
5774                return Phone.CONTENT_ITEM_TYPE;
5775            case PHONE_LOOKUP:
5776                return PhoneLookup.CONTENT_TYPE;
5777            case EMAILS:
5778                return Email.CONTENT_TYPE;
5779            case EMAILS_ID:
5780                return Email.CONTENT_ITEM_TYPE;
5781            case POSTALS:
5782                return StructuredPostal.CONTENT_TYPE;
5783            case POSTALS_ID:
5784                return StructuredPostal.CONTENT_ITEM_TYPE;
5785            case AGGREGATION_EXCEPTIONS:
5786                return AggregationExceptions.CONTENT_TYPE;
5787            case AGGREGATION_EXCEPTION_ID:
5788                return AggregationExceptions.CONTENT_ITEM_TYPE;
5789            case SETTINGS:
5790                return Settings.CONTENT_TYPE;
5791            case AGGREGATION_SUGGESTIONS:
5792                return Contacts.CONTENT_TYPE;
5793            case SEARCH_SUGGESTIONS:
5794                return SearchManager.SUGGEST_MIME_TYPE;
5795            case SEARCH_SHORTCUT:
5796                return SearchManager.SHORTCUT_MIME_TYPE;
5797
5798            default:
5799                return mLegacyApiSupport.getType(uri);
5800        }
5801    }
5802
5803    private void setDisplayName(long rawContactId, int displayNameSource,
5804            String displayNamePrimary, String displayNameAlternative, String phoneticName,
5805            int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) {
5806        mRawContactDisplayNameUpdate.bindLong(1, displayNameSource);
5807        bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary);
5808        bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative);
5809        bindString(mRawContactDisplayNameUpdate, 4, phoneticName);
5810        mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle);
5811        bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary);
5812        bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative);
5813        mRawContactDisplayNameUpdate.bindLong(8, rawContactId);
5814        mRawContactDisplayNameUpdate.execute();
5815    }
5816
5817    /**
5818     * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
5819     */
5820    private void setRawContactDirty(long rawContactId) {
5821        mDirtyRawContacts.add(rawContactId);
5822    }
5823
5824    /*
5825     * Sets the given dataId record in the "data" table to primary, and resets all data records of
5826     * the same mimetype and under the same contact to not be primary.
5827     *
5828     * @param dataId the id of the data record to be set to primary.
5829     */
5830    private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
5831        mSetPrimaryStatement.bindLong(1, dataId);
5832        mSetPrimaryStatement.bindLong(2, mimeTypeId);
5833        mSetPrimaryStatement.bindLong(3, rawContactId);
5834        mSetPrimaryStatement.execute();
5835    }
5836
5837    /*
5838     * Sets the given dataId record in the "data" table to "super primary", and resets all data
5839     * records of the same mimetype and under the same aggregate to not be "super primary".
5840     *
5841     * @param dataId the id of the data record to be set to primary.
5842     */
5843    private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
5844        mSetSuperPrimaryStatement.bindLong(1, dataId);
5845        mSetSuperPrimaryStatement.bindLong(2, mimeTypeId);
5846        mSetSuperPrimaryStatement.bindLong(3, rawContactId);
5847        mSetSuperPrimaryStatement.execute();
5848    }
5849
5850    public String insertNameLookupForEmail(long rawContactId, long dataId, String email) {
5851        if (TextUtils.isEmpty(email)) {
5852            return null;
5853        }
5854
5855        String address = mDbHelper.extractHandleFromEmailAddress(email);
5856        if (address == null) {
5857            return null;
5858        }
5859
5860        insertNameLookup(rawContactId, dataId,
5861                NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address));
5862        return address;
5863    }
5864
5865    /**
5866     * Normalizes the nickname and inserts it in the name lookup table.
5867     */
5868    public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) {
5869        if (TextUtils.isEmpty(nickname)) {
5870            return;
5871        }
5872
5873        insertNameLookup(rawContactId, dataId,
5874                NameLookupType.NICKNAME, NameNormalizer.normalize(nickname));
5875    }
5876
5877    public void insertNameLookupForOrganization(long rawContactId, long dataId, String company,
5878            String title) {
5879        if (!TextUtils.isEmpty(company)) {
5880            insertNameLookup(rawContactId, dataId,
5881                    NameLookupType.ORGANIZATION, NameNormalizer.normalize(company));
5882        }
5883        if (!TextUtils.isEmpty(title)) {
5884            insertNameLookup(rawContactId, dataId,
5885                    NameLookupType.ORGANIZATION, NameNormalizer.normalize(title));
5886        }
5887    }
5888
5889    public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name,
5890            int fullNameStyle) {
5891        mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle);
5892    }
5893
5894    private class StructuredNameLookupBuilder extends NameLookupBuilder {
5895
5896        public StructuredNameLookupBuilder(NameSplitter splitter) {
5897            super(splitter);
5898        }
5899
5900        @Override
5901        protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
5902                String name) {
5903            ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name);
5904        }
5905
5906        @Override
5907        protected String[] getCommonNicknameClusters(String normalizedName) {
5908            return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
5909        }
5910    }
5911
5912    public void insertNameLookupForPhoneticName(long rawContactId, long dataId,
5913            ContentValues values) {
5914        if (values.containsKey(StructuredName.PHONETIC_FAMILY_NAME)
5915                || values.containsKey(StructuredName.PHONETIC_GIVEN_NAME)
5916                || values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME)) {
5917            insertNameLookupForPhoneticName(rawContactId, dataId,
5918                    values.getAsString(StructuredName.PHONETIC_FAMILY_NAME),
5919                    values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME),
5920                    values.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
5921        }
5922    }
5923
5924    public void insertNameLookupForPhoneticName(long rawContactId, long dataId, String familyName,
5925            String middleName, String givenName) {
5926        mSb.setLength(0);
5927        if (familyName != null) {
5928            mSb.append(familyName.trim());
5929        }
5930        if (middleName != null) {
5931            mSb.append(middleName.trim());
5932        }
5933        if (givenName != null) {
5934            mSb.append(givenName.trim());
5935        }
5936
5937        if (mSb.length() > 0) {
5938            insertNameLookup(rawContactId, dataId, NameLookupType.NAME_COLLATION_KEY,
5939                    NameNormalizer.normalize(mSb.toString()));
5940        }
5941
5942        if (givenName != null) {
5943            // We want the phonetic given name to be used for search, but not for aggregation,
5944            // which is why we are using NAME_SHORTHAND rather than NAME_COLLATION_KEY
5945            insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND,
5946                    NameNormalizer.normalize(givenName.trim()));
5947        }
5948    }
5949
5950    /**
5951     * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
5952     */
5953    public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
5954        mNameLookupInsert.bindLong(1, rawContactId);
5955        mNameLookupInsert.bindLong(2, dataId);
5956        mNameLookupInsert.bindLong(3, lookupType);
5957        bindString(mNameLookupInsert, 4, name);
5958        mNameLookupInsert.executeInsert();
5959    }
5960
5961    /**
5962     * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
5963     */
5964    public void deleteNameLookup(long dataId) {
5965        mNameLookupDelete.bindLong(1, dataId);
5966        mNameLookupDelete.execute();
5967    }
5968
5969    public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
5970        sb.append("(" +
5971                "SELECT DISTINCT " + RawContacts.CONTACT_ID +
5972                " FROM " + Tables.RAW_CONTACTS +
5973                " JOIN " + Tables.NAME_LOOKUP +
5974                " ON(" + RawContactsColumns.CONCRETE_ID + "="
5975                        + NameLookupColumns.RAW_CONTACT_ID + ")" +
5976                " WHERE normalized_name GLOB '");
5977        sb.append(NameNormalizer.normalize(filterParam));
5978        sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
5979                    " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))");
5980    }
5981
5982    public String getRawContactsByFilterAsNestedQuery(String filterParam) {
5983        StringBuilder sb = new StringBuilder();
5984        appendRawContactsByFilterAsNestedQuery(sb, filterParam);
5985        return sb.toString();
5986    }
5987
5988    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) {
5989        appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true);
5990    }
5991
5992    private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
5993            boolean allowEmailMatch) {
5994        sb.append("(" +
5995                "SELECT " + NameLookupColumns.RAW_CONTACT_ID +
5996                " FROM " + Tables.NAME_LOOKUP +
5997                " WHERE " + NameLookupColumns.NORMALIZED_NAME +
5998                " GLOB '");
5999        sb.append(normalizedName);
6000        sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
6001                + NameLookupType.NAME_COLLATION_KEY + ","
6002                + NameLookupType.NICKNAME + ","
6003                + NameLookupType.NAME_SHORTHAND + ","
6004                + NameLookupType.ORGANIZATION + ","
6005                + NameLookupType.NAME_CONSONANTS);
6006        if (allowEmailMatch) {
6007            sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME);
6008        }
6009        sb.append("))");
6010    }
6011
6012    /**
6013     * Inserts an argument at the beginning of the selection arg list.
6014     */
6015    private String[] insertSelectionArg(String[] selectionArgs, String arg) {
6016        if (selectionArgs == null) {
6017            return new String[] {arg};
6018        } else {
6019            int newLength = selectionArgs.length + 1;
6020            String[] newSelectionArgs = new String[newLength];
6021            newSelectionArgs[0] = arg;
6022            System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
6023            return newSelectionArgs;
6024        }
6025    }
6026
6027    private String[] appendProjectionArg(String[] projection, String arg) {
6028        if (projection == null) {
6029            return null;
6030        }
6031        final int length = projection.length;
6032        String[] newProjection = new String[length + 1];
6033        System.arraycopy(projection, 0, newProjection, 0, length);
6034        newProjection[length] = arg;
6035        return newProjection;
6036    }
6037
6038    protected Account getDefaultAccount() {
6039        AccountManager accountManager = AccountManager.get(getContext());
6040        try {
6041            Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE,
6042                    new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult();
6043            if (accounts != null && accounts.length > 0) {
6044                return accounts[0];
6045            }
6046        } catch (Throwable e) {
6047            Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
6048        }
6049        return null;
6050    }
6051
6052    /**
6053     * Returns true if the specified account type is writable.
6054     */
6055    protected boolean isWritableAccount(String accountType) {
6056        if (accountType == null) {
6057            return true;
6058        }
6059
6060        Boolean writable = mAccountWritability.get(accountType);
6061        if (writable != null) {
6062            return writable;
6063        }
6064
6065        IContentService contentService = ContentResolver.getContentService();
6066        try {
6067            for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
6068                if (ContactsContract.AUTHORITY.equals(sync.authority) &&
6069                        accountType.equals(sync.accountType)) {
6070                    writable = sync.supportsUploading();
6071                    break;
6072                }
6073            }
6074        } catch (RemoteException e) {
6075            Log.e(TAG, "Could not acquire sync adapter types");
6076        }
6077
6078        if (writable == null) {
6079            writable = false;
6080        }
6081
6082        mAccountWritability.put(accountType, writable);
6083        return writable;
6084    }
6085
6086    /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
6087            boolean defaultValue) {
6088
6089        // Manually parse the query, which is much faster than calling uri.getQueryParameter
6090        String query = uri.getEncodedQuery();
6091        if (query == null) {
6092            return defaultValue;
6093        }
6094
6095        int index = query.indexOf(parameter);
6096        if (index == -1) {
6097            return defaultValue;
6098        }
6099
6100        index += parameter.length();
6101
6102        return !matchQueryParameter(query, index, "=0", false)
6103                && !matchQueryParameter(query, index, "=false", true);
6104    }
6105
6106    private static boolean matchQueryParameter(String query, int index, String value,
6107            boolean ignoreCase) {
6108        int length = value.length();
6109        return query.regionMatches(ignoreCase, index, value, 0, length)
6110                && (query.length() == index + length || query.charAt(index + length) == '&');
6111    }
6112
6113    /**
6114     * A fast re-implementation of {@link Uri#getQueryParameter}
6115     */
6116    /* package */ static String getQueryParameter(Uri uri, String parameter) {
6117        String query = uri.getEncodedQuery();
6118        if (query == null) {
6119            return null;
6120        }
6121
6122        int queryLength = query.length();
6123        int parameterLength = parameter.length();
6124
6125        String value;
6126        int index = 0;
6127        while (true) {
6128            index = query.indexOf(parameter, index);
6129            if (index == -1) {
6130                return null;
6131            }
6132
6133            index += parameterLength;
6134
6135            if (queryLength == index) {
6136                return null;
6137            }
6138
6139            if (query.charAt(index) == '=') {
6140                index++;
6141                break;
6142            }
6143        }
6144
6145        int ampIndex = query.indexOf('&', index);
6146        if (ampIndex == -1) {
6147            value = query.substring(index);
6148        } else {
6149            value = query.substring(index, ampIndex);
6150        }
6151
6152        return Uri.decode(value);
6153    }
6154
6155    private void bindString(SQLiteStatement stmt, int index, String value) {
6156        if (value == null) {
6157            stmt.bindNull(index);
6158        } else {
6159            stmt.bindString(index, value);
6160        }
6161    }
6162
6163    private void bindLong(SQLiteStatement stmt, int index, Number value) {
6164        if (value == null) {
6165            stmt.bindNull(index);
6166        } else {
6167            stmt.bindLong(index, value.longValue());
6168        }
6169    }
6170}
6171