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