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