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