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