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