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