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