ContactsProvider2.java revision 3826a44d8de41e9c148dd6a967392ea5af478085
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 boolean mProviderStatusUpdateNeeded;
1892    private long mEstimatedStorageRequirement = 0;
1893    private volatile CountDownLatch mAccessLatch;
1894
1895    private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap();
1896    private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
1897    private HashSet<Long> mDirtyRawContacts = Sets.newHashSet();
1898    private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap();
1899
1900    private boolean mVisibleTouched = false;
1901
1902    private boolean mSyncToNetwork;
1903
1904    private Locale mCurrentLocale;
1905    private int mContactsAccountCount;
1906
1907    private CountryMonitor mCountryMonitor;
1908
1909    @Override
1910    public boolean onCreate() {
1911        super.onCreate();
1912        try {
1913            return initialize();
1914        } catch (RuntimeException e) {
1915            Log.e(TAG, "Cannot start provider", e);
1916            return false;
1917        }
1918    }
1919
1920    private boolean initialize() {
1921        final Context context = getContext();
1922        mCountryMonitor = CountryMonitor.getInstance(context);
1923        mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
1924        mContactDirectoryManager = new ContactDirectoryManager(this);
1925        mGlobalSearchSupport = new GlobalSearchSupport(this);
1926        mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
1927        mDb = mDbHelper.getWritableDatabase();
1928
1929        initForDefaultLocale();
1930
1931        mSetPrimaryStatement = mDb.compileStatement(
1932                "UPDATE " + Tables.DATA +
1933                " SET " + Data.IS_PRIMARY + "=(_id=?)" +
1934                " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
1935                "   AND " + Data.RAW_CONTACT_ID + "=?");
1936
1937        mSetSuperPrimaryStatement = mDb.compileStatement(
1938                "UPDATE " + Tables.DATA +
1939                " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" +
1940                " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
1941                "   AND " + Data.RAW_CONTACT_ID + " IN (" +
1942                        "SELECT " + RawContacts._ID +
1943                        " FROM " + Tables.RAW_CONTACTS +
1944                        " WHERE " + RawContacts.CONTACT_ID + " =(" +
1945                                "SELECT " + RawContacts.CONTACT_ID +
1946                                " FROM " + Tables.RAW_CONTACTS +
1947                                " WHERE " + RawContacts._ID + "=?))");
1948
1949        mRawContactDisplayNameUpdate = mDb.compileStatement(
1950                "UPDATE " + Tables.RAW_CONTACTS +
1951                " SET " +
1952                        RawContacts.DISPLAY_NAME_SOURCE + "=?," +
1953                        RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
1954                        RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
1955                        RawContacts.PHONETIC_NAME + "=?," +
1956                        RawContacts.PHONETIC_NAME_STYLE + "=?," +
1957                        RawContacts.SORT_KEY_PRIMARY + "=?," +
1958                        RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
1959                " WHERE " + RawContacts._ID + "=?");
1960
1961        mNameLookupInsert = mDb.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
1962                + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + ","
1963                + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME
1964                + ") VALUES (?,?,?,?)");
1965        mNameLookupDelete = mDb.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
1966                + NameLookupColumns.DATA_ID + "=?");
1967
1968        mStatusUpdateInsert = mDb.compileStatement(
1969                "INSERT INTO " + Tables.STATUS_UPDATES + "("
1970                        + StatusUpdatesColumns.DATA_ID + ", "
1971                        + StatusUpdates.STATUS + ","
1972                        + StatusUpdates.STATUS_RES_PACKAGE + ","
1973                        + StatusUpdates.STATUS_ICON + ","
1974                        + StatusUpdates.STATUS_LABEL + ")" +
1975                " VALUES (?,?,?,?,?)");
1976
1977        mStatusUpdateReplace = mDb.compileStatement(
1978                "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "("
1979                        + StatusUpdatesColumns.DATA_ID + ", "
1980                        + StatusUpdates.STATUS_TIMESTAMP + ","
1981                        + StatusUpdates.STATUS + ","
1982                        + StatusUpdates.STATUS_RES_PACKAGE + ","
1983                        + StatusUpdates.STATUS_ICON + ","
1984                        + StatusUpdates.STATUS_LABEL + ")" +
1985                " VALUES (?,?,?,?,?,?)");
1986
1987        mStatusUpdateAutoTimestamp = mDb.compileStatement(
1988                "UPDATE " + Tables.STATUS_UPDATES +
1989                " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?,"
1990                        + StatusUpdates.STATUS + "=?" +
1991                " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"
1992                        + " AND " + StatusUpdates.STATUS + "!=?");
1993
1994        mStatusAttributionUpdate = mDb.compileStatement(
1995                "UPDATE " + Tables.STATUS_UPDATES +
1996                " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?,"
1997                        + StatusUpdates.STATUS_ICON + "=?,"
1998                        + StatusUpdates.STATUS_LABEL + "=?" +
1999                " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
2000
2001        mStatusUpdateDelete = mDb.compileStatement(
2002                "DELETE FROM " + Tables.STATUS_UPDATES +
2003                " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
2004
2005        // When setting NAME_VERIFIED to 1 on a raw contact, reset it to 0
2006        // on all other raw contacts in the same aggregate
2007        mResetNameVerifiedForOtherRawContacts = mDb.compileStatement(
2008                "UPDATE " + Tables.RAW_CONTACTS +
2009                " SET " + RawContacts.NAME_VERIFIED + "=0" +
2010                " WHERE " + RawContacts.CONTACT_ID + "=(" +
2011                        "SELECT " + RawContacts.CONTACT_ID +
2012                        " FROM " + Tables.RAW_CONTACTS +
2013                        " WHERE " + RawContacts._ID + "=?)" +
2014                " AND " + RawContacts._ID + "!=?");
2015
2016        mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
2017        mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
2018        mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
2019        mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
2020        mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE);
2021        mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
2022
2023        updateAccounts();
2024
2025        if (isLegacyContactImportNeeded()) {
2026            importLegacyContactsAsync();
2027        } else {
2028            verifyLocale();
2029        }
2030
2031        startContactDirectoryManager();
2032
2033        if (isAggregationUpgradeNeeded()) {
2034            upgradeAggregationAlgorithm();
2035        }
2036
2037        updateProviderStatus();
2038
2039        return (mDb != null);
2040    }
2041
2042    protected String getCurrentCountryIso() {
2043        return mCountryMonitor.getCountryIso();
2044    }
2045
2046    private void initDataRowHandlers() {
2047      mDataRowHandlers = new HashMap<String, DataRowHandler>();
2048
2049      mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
2050      mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
2051              new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
2052      mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
2053              StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
2054      mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
2055      mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
2056      mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler());
2057      mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
2058              new StructuredNameRowHandler(mNameSplitter));
2059      mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE,
2060              new StructuredPostalRowHandler(mPostalSplitter));
2061      mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler());
2062      mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler());
2063    }
2064
2065    /**
2066     * Visible for testing.
2067     */
2068    /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
2069        return new PhotoPriorityResolver(context);
2070    }
2071
2072    /**
2073     * (Re)allocates all locale-sensitive structures.
2074     */
2075    private void initForDefaultLocale() {
2076        mCurrentLocale = getLocale();
2077        mNameSplitter = mDbHelper.createNameSplitter();
2078        mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
2079        mPostalSplitter = new PostalSplitter(mCurrentLocale);
2080        mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase());
2081        ContactLocaleUtils.getIntance().setLocale(mCurrentLocale);
2082        mContactAggregator = new ContactAggregator(this, mDbHelper,
2083                createPhotoPriorityResolver(getContext()), mNameSplitter, mCommonNicknameCache);
2084        mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
2085
2086        initDataRowHandlers();
2087    }
2088
2089    public void onLocaleChanged() {
2090        if (mProviderStatus != ProviderStatus.STATUS_NORMAL
2091                && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
2092            return;
2093        }
2094
2095        initForDefaultLocale();
2096        verifyLocale();
2097    }
2098
2099    /**
2100     * Verifies that the contacts database is properly configured for the current locale.
2101     * If not, changes the database locale to the current locale using an asynchronous task.
2102     * This needs to be done asynchronously because the process involves rebuilding
2103     * large data structures (name lookup, sort keys), which can take minutes on
2104     * a large set of contacts.
2105     */
2106    protected void verifyLocale() {
2107
2108        // The process is already running - postpone the change
2109        if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) {
2110            return;
2111        }
2112
2113        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
2114        final String providerLocale = prefs.getString(PREF_LOCALE, null);
2115        final Locale currentLocale = mCurrentLocale;
2116        if (currentLocale.toString().equals(providerLocale)) {
2117            return;
2118        }
2119
2120        int providerStatus = mProviderStatus;
2121        setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
2122
2123        AsyncTask<Integer, Void, Void> task = new AsyncTask<Integer, Void, Void>() {
2124
2125            int savedProviderStatus;
2126
2127            @Override
2128            protected Void doInBackground(Integer... params) {
2129                savedProviderStatus = params[0];
2130                mDbHelper.setLocale(ContactsProvider2.this, currentLocale);
2131                return null;
2132            }
2133
2134            @Override
2135            protected void onPostExecute(Void result) {
2136                prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply();
2137                setProviderStatus(savedProviderStatus);
2138
2139                // Recursive invocation, needed to cover the case where locale
2140                // changes once and then changes again before the db upgrade is completed.
2141                verifyLocale();
2142            }
2143        };
2144
2145        task.execute(providerStatus);
2146    }
2147
2148    private void updateProviderStatus() {
2149        if (mProviderStatus != ProviderStatus.STATUS_NORMAL
2150                && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
2151            return;
2152        }
2153
2154        if (mContactsAccountCount == 0
2155                && DatabaseUtils.queryNumEntries(mDb, Tables.CONTACTS, null) == 0) {
2156            setProviderStatus(ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS);
2157        } else {
2158            setProviderStatus(ProviderStatus.STATUS_NORMAL);
2159        }
2160    }
2161
2162    /* Visible for testing */
2163    @Override
2164    protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
2165        return ContactsDatabaseHelper.getInstance(context);
2166    }
2167
2168    /* package */ NameSplitter getNameSplitter() {
2169        return mNameSplitter;
2170    }
2171
2172    /* Visible for testing */
2173    public ContactDirectoryManager getContactDirectoryManager() {
2174        return mContactDirectoryManager;
2175    }
2176
2177    /* Visible for testing */
2178    protected Locale getLocale() {
2179        return Locale.getDefault();
2180    }
2181
2182    /* Visible for testing */
2183    protected void startContactDirectoryManager() {
2184        getContactDirectoryManager().start();
2185    }
2186
2187    protected boolean isLegacyContactImportNeeded() {
2188        int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0"));
2189        return version < PROPERTY_CONTACTS_IMPORT_VERSION;
2190    }
2191
2192    protected LegacyContactImporter getLegacyContactImporter() {
2193        return new LegacyContactImporter(getContext(), this);
2194    }
2195
2196    /**
2197     * Imports legacy contacts in a separate thread.  As long as the import process is running
2198     * all other access to the contacts is blocked.
2199     */
2200    private void importLegacyContactsAsync() {
2201        Log.v(TAG, "Importing legacy contacts");
2202        setProviderStatus(ProviderStatus.STATUS_UPGRADING);
2203        if (mAccessLatch == null) {
2204            mAccessLatch = new CountDownLatch(1);
2205        }
2206
2207        Thread importThread = new Thread("LegacyContactImport") {
2208            @Override
2209            public void run() {
2210                final SharedPreferences prefs =
2211                    PreferenceManager.getDefaultSharedPreferences(getContext());
2212                mDbHelper.setLocale(ContactsProvider2.this, mCurrentLocale);
2213                prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit();
2214
2215                LegacyContactImporter importer = getLegacyContactImporter();
2216                if (importLegacyContacts(importer)) {
2217                    onLegacyContactImportSuccess();
2218                } else {
2219                    onLegacyContactImportFailure();
2220                }
2221            }
2222        };
2223
2224        importThread.start();
2225    }
2226
2227    /**
2228     * Unlocks the provider and declares that the import process is complete.
2229     */
2230    private void onLegacyContactImportSuccess() {
2231        NotificationManager nm =
2232            (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE);
2233        nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION);
2234
2235        // Store a property in the database indicating that the conversion process succeeded
2236        mDbHelper.setProperty(PROPERTY_CONTACTS_IMPORTED,
2237                String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION));
2238        setProviderStatus(ProviderStatus.STATUS_NORMAL);
2239        mAccessLatch.countDown();
2240        mAccessLatch = null;
2241        Log.v(TAG, "Completed import of legacy contacts");
2242    }
2243
2244    /**
2245     * Announces the provider status and keeps the provider locked.
2246     */
2247    private void onLegacyContactImportFailure() {
2248        Context context = getContext();
2249        NotificationManager nm =
2250            (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
2251
2252        // Show a notification
2253        Notification n = new Notification(android.R.drawable.stat_notify_error,
2254                context.getString(R.string.upgrade_out_of_memory_notification_ticker),
2255                System.currentTimeMillis());
2256        n.setLatestEventInfo(context,
2257                context.getString(R.string.upgrade_out_of_memory_notification_title),
2258                context.getString(R.string.upgrade_out_of_memory_notification_text),
2259                PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0));
2260        n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
2261
2262        nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n);
2263
2264        setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY);
2265        Log.v(TAG, "Failed to import legacy contacts");
2266    }
2267
2268    /* Visible for testing */
2269    /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
2270        boolean aggregatorEnabled = mContactAggregator.isEnabled();
2271        mContactAggregator.setEnabled(false);
2272        try {
2273            if (importer.importContacts()) {
2274
2275                // TODO aggregate all newly added raw contacts
2276                mContactAggregator.setEnabled(aggregatorEnabled);
2277                return true;
2278            }
2279        } catch (Throwable e) {
2280           Log.e(TAG, "Legacy contact import failed", e);
2281        }
2282        mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement();
2283        return false;
2284    }
2285
2286    /**
2287     * Wipes all data from the contacts database.
2288     */
2289    /* package */ void wipeData() {
2290        mDbHelper.wipeData();
2291        mProviderStatus = ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS;
2292    }
2293
2294    /**
2295     * While importing and aggregating contacts, this content provider will
2296     * block all attempts to change contacts data. In particular, it will hold
2297     * up all contact syncs. As soon as the import process is complete, all
2298     * processes waiting to write to the provider are unblocked and can proceed
2299     * to compete for the database transaction monitor.
2300     */
2301    private void waitForAccess() {
2302        CountDownLatch latch = mAccessLatch;
2303        if (latch != null) {
2304            while (true) {
2305                try {
2306                    latch.await();
2307                    mAccessLatch = null;
2308                    return;
2309                } catch (InterruptedException e) {
2310                    Thread.currentThread().interrupt();
2311                }
2312            }
2313        }
2314    }
2315
2316    @Override
2317    public Uri insert(Uri uri, ContentValues values) {
2318        waitForAccess();
2319        return super.insert(uri, values);
2320    }
2321
2322    @Override
2323    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
2324        if (mAccessLatch != null) {
2325            // We are stuck trying to upgrade contacts db.  The only update request
2326            // allowed in this case is an update of provider status, which will trigger
2327            // an attempt to upgrade contacts again.
2328            int match = sUriMatcher.match(uri);
2329            if (match == PROVIDER_STATUS && isLegacyContactImportNeeded()) {
2330                Integer newStatus = values.getAsInteger(ProviderStatus.STATUS);
2331                if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) {
2332                    importLegacyContactsAsync();
2333                    return 1;
2334                } else {
2335                    return 0;
2336                }
2337            }
2338        }
2339        waitForAccess();
2340        return super.update(uri, values, selection, selectionArgs);
2341    }
2342
2343    @Override
2344    public int delete(Uri uri, String selection, String[] selectionArgs) {
2345        waitForAccess();
2346        return super.delete(uri, selection, selectionArgs);
2347    }
2348
2349    @Override
2350    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
2351            throws OperationApplicationException {
2352        waitForAccess();
2353        return super.applyBatch(operations);
2354    }
2355
2356    @Override
2357    protected void onBeginTransaction() {
2358        if (VERBOSE_LOGGING) {
2359            Log.v(TAG, "onBeginTransaction");
2360        }
2361        super.onBeginTransaction();
2362        mContactAggregator.clearPendingAggregations();
2363        clearTransactionalChanges();
2364    }
2365
2366    private void clearTransactionalChanges() {
2367        mInsertedRawContacts.clear();
2368        mUpdatedRawContacts.clear();
2369        mUpdatedSyncStates.clear();
2370        mDirtyRawContacts.clear();
2371    }
2372
2373    @Override
2374    protected void beforeTransactionCommit() {
2375
2376        if (VERBOSE_LOGGING) {
2377            Log.v(TAG, "beforeTransactionCommit");
2378        }
2379        super.beforeTransactionCommit();
2380        flushTransactionalChanges();
2381        mContactAggregator.aggregateInTransaction(mDb);
2382        if (mVisibleTouched) {
2383            mVisibleTouched = false;
2384            mDbHelper.updateAllVisible();
2385        }
2386
2387        if (mProviderStatusUpdateNeeded) {
2388            updateProviderStatus();
2389            mProviderStatusUpdateNeeded = false;
2390        }
2391    }
2392
2393    private void flushTransactionalChanges() {
2394        if (VERBOSE_LOGGING) {
2395            Log.v(TAG, "flushTransactionChanges");
2396        }
2397
2398        for (long rawContactId : mInsertedRawContacts.keySet()) {
2399            updateRawContactDisplayName(mDb, rawContactId);
2400            mContactAggregator.onRawContactInsert(mDb, rawContactId);
2401        }
2402
2403        if (!mDirtyRawContacts.isEmpty()) {
2404            mSb.setLength(0);
2405            mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
2406            appendIds(mSb, mDirtyRawContacts);
2407            mSb.append(")");
2408            mDb.execSQL(mSb.toString());
2409        }
2410
2411        if (!mUpdatedRawContacts.isEmpty()) {
2412            mSb.setLength(0);
2413            mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
2414            appendIds(mSb, mUpdatedRawContacts);
2415            mSb.append(")");
2416            mDb.execSQL(mSb.toString());
2417        }
2418
2419        for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
2420            long id = entry.getKey();
2421            if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) {
2422                throw new IllegalStateException(
2423                        "unable to update sync state, does it still exist?");
2424            }
2425        }
2426
2427        clearTransactionalChanges();
2428    }
2429
2430    /**
2431     * Appends comma separated ids.
2432     * @param ids Should not be empty
2433     */
2434    private void appendIds(StringBuilder sb, HashSet<Long> ids) {
2435        for (long id : ids) {
2436            sb.append(id).append(',');
2437        }
2438
2439        sb.setLength(sb.length() - 1); // Yank the last comma
2440    }
2441
2442    @Override
2443    protected void notifyChange() {
2444        notifyChange(mSyncToNetwork);
2445        mSyncToNetwork = false;
2446    }
2447
2448    protected void notifyChange(boolean syncToNetwork) {
2449        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
2450                syncToNetwork);
2451    }
2452
2453    protected void setProviderStatus(int status) {
2454        if (mProviderStatus != status) {
2455            mProviderStatus = status;
2456            getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false);
2457        }
2458    }
2459
2460    private boolean isNewRawContact(long rawContactId) {
2461        return mInsertedRawContacts.containsKey(rawContactId);
2462    }
2463
2464    private DataRowHandler getDataRowHandler(final String mimeType) {
2465        DataRowHandler handler = mDataRowHandlers.get(mimeType);
2466        if (handler == null) {
2467            handler = new CustomDataRowHandler(mimeType);
2468            mDataRowHandlers.put(mimeType, handler);
2469        }
2470        return handler;
2471    }
2472
2473    @Override
2474    protected Uri insertInTransaction(Uri uri, ContentValues values) {
2475        if (VERBOSE_LOGGING) {
2476            Log.v(TAG, "insertInTransaction: " + uri + " " + values);
2477        }
2478
2479        final boolean callerIsSyncAdapter =
2480                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2481
2482        final int match = sUriMatcher.match(uri);
2483        long id = 0;
2484
2485        switch (match) {
2486            case SYNCSTATE:
2487                id = mDbHelper.getSyncState().insert(mDb, values);
2488                break;
2489
2490            case CONTACTS: {
2491                insertContact(values);
2492                break;
2493            }
2494
2495            case RAW_CONTACTS: {
2496                id = insertRawContact(uri, values, callerIsSyncAdapter);
2497                mSyncToNetwork |= !callerIsSyncAdapter;
2498                break;
2499            }
2500
2501            case RAW_CONTACTS_DATA: {
2502                values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
2503                id = insertData(values, callerIsSyncAdapter);
2504                mSyncToNetwork |= !callerIsSyncAdapter;
2505                break;
2506            }
2507
2508            case DATA: {
2509                id = insertData(values, callerIsSyncAdapter);
2510                mSyncToNetwork |= !callerIsSyncAdapter;
2511                break;
2512            }
2513
2514            case GROUPS: {
2515                id = insertGroup(uri, values, callerIsSyncAdapter);
2516                mSyncToNetwork |= !callerIsSyncAdapter;
2517                break;
2518            }
2519
2520            case SETTINGS: {
2521                id = insertSettings(uri, values);
2522                mSyncToNetwork |= !callerIsSyncAdapter;
2523                break;
2524            }
2525
2526            case STATUS_UPDATES: {
2527                id = insertStatusUpdate(values);
2528                break;
2529            }
2530
2531            default:
2532                mSyncToNetwork = true;
2533                return mLegacyApiSupport.insert(uri, values);
2534        }
2535
2536        if (id < 0) {
2537            return null;
2538        }
2539
2540        return ContentUris.withAppendedId(uri, id);
2541    }
2542
2543    /**
2544     * If account is non-null then store it in the values. If the account is
2545     * already specified in the values then it must be consistent with the
2546     * account, if it is non-null.
2547     *
2548     * @param uri Current {@link Uri} being operated on.
2549     * @param values {@link ContentValues} to read and possibly update.
2550     * @throws IllegalArgumentException when only one of
2551     *             {@link RawContacts#ACCOUNT_NAME} or
2552     *             {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
2553     *             other undefined.
2554     * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
2555     *             and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
2556     *             the given {@link Uri} and {@link ContentValues}.
2557     */
2558    private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
2559        String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
2560        String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
2561        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
2562
2563        String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
2564        String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
2565        final boolean partialValues = TextUtils.isEmpty(valueAccountName)
2566                ^ TextUtils.isEmpty(valueAccountType);
2567
2568        if (partialUri || partialValues) {
2569            // Throw when either account is incomplete
2570            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
2571                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
2572        }
2573
2574        // Accounts are valid by only checking one parameter, since we've
2575        // already ruled out partial accounts.
2576        final boolean validUri = !TextUtils.isEmpty(accountName);
2577        final boolean validValues = !TextUtils.isEmpty(valueAccountName);
2578
2579        if (validValues && validUri) {
2580            // Check that accounts match when both present
2581            final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
2582                    && TextUtils.equals(accountType, valueAccountType);
2583            if (!accountMatch) {
2584                throw new IllegalArgumentException(mDbHelper.exceptionMessage(
2585                        "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
2586            }
2587        } else if (validUri) {
2588            // Fill values from Uri when not present
2589            values.put(RawContacts.ACCOUNT_NAME, accountName);
2590            values.put(RawContacts.ACCOUNT_TYPE, accountType);
2591        } else if (validValues) {
2592            accountName = valueAccountName;
2593            accountType = valueAccountType;
2594        } else {
2595            return null;
2596        }
2597
2598        // Use cached Account object when matches, otherwise create
2599        if (mAccount == null
2600                || !mAccount.name.equals(accountName)
2601                || !mAccount.type.equals(accountType)) {
2602            mAccount = new Account(accountName, accountType);
2603        }
2604
2605        return mAccount;
2606    }
2607
2608    /**
2609     * Inserts an item in the contacts table
2610     *
2611     * @param values the values for the new row
2612     * @return the row ID of the newly created row
2613     */
2614    private long insertContact(ContentValues values) {
2615        throw new UnsupportedOperationException("Aggregate contacts are created automatically");
2616    }
2617
2618    /**
2619     * Inserts an item in the contacts table
2620     *
2621     * @param uri the values for the new row
2622     * @param values the account this contact should be associated with. may be null.
2623     * @param callerIsSyncAdapter
2624     * @return the row ID of the newly created row
2625     */
2626    private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
2627        mValues.clear();
2628        mValues.putAll(values);
2629        mValues.putNull(RawContacts.CONTACT_ID);
2630
2631        final Account account = resolveAccount(uri, mValues);
2632
2633        if (values.containsKey(RawContacts.DELETED)
2634                && values.getAsInteger(RawContacts.DELETED) != 0) {
2635            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
2636        }
2637
2638        long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues);
2639        int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
2640        if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) {
2641            aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE);
2642        }
2643        mContactAggregator.markNewForAggregation(rawContactId, aggregationMode);
2644
2645        // Trigger creation of a Contact based on this RawContact at the end of transaction
2646        mInsertedRawContacts.put(rawContactId, account);
2647
2648        if (!callerIsSyncAdapter) {
2649            addAutoAddMembership(rawContactId);
2650            final Long starred = values.getAsLong(RawContacts.STARRED);
2651            if (starred != null && starred != 0) {
2652                updateFavoritesMembership(rawContactId, starred != 0);
2653            }
2654        }
2655
2656        mProviderStatusUpdateNeeded = true;
2657        return rawContactId;
2658    }
2659
2660    private void addAutoAddMembership(long rawContactId) {
2661        final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID,
2662                rawContactId);
2663        if (groupId != null) {
2664            insertDataGroupMembership(rawContactId, groupId);
2665        }
2666    }
2667
2668    private Long findGroupByRawContactId(String selection, long rawContactId) {
2669        Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID,
2670                selection,
2671                new String[]{Long.toString(rawContactId)},
2672                null /* groupBy */, null /* having */, null /* orderBy */);
2673        try {
2674            while (c.moveToNext()) {
2675                return c.getLong(0);
2676            }
2677            return null;
2678        } finally {
2679            c.close();
2680        }
2681    }
2682
2683    private void updateFavoritesMembership(long rawContactId, boolean isStarred) {
2684        final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID,
2685                rawContactId);
2686        if (groupId != null) {
2687            if (isStarred) {
2688                insertDataGroupMembership(rawContactId, groupId);
2689            } else {
2690                deleteDataGroupMembership(rawContactId, groupId);
2691            }
2692        }
2693    }
2694
2695    private void insertDataGroupMembership(long rawContactId, long groupId) {
2696        ContentValues groupMembershipValues = new ContentValues();
2697        groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId);
2698        groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
2699        groupMembershipValues.put(DataColumns.MIMETYPE_ID,
2700                mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
2701        mDb.insert(Tables.DATA, null, groupMembershipValues);
2702    }
2703
2704    private void deleteDataGroupMembership(long rawContactId, long groupId) {
2705        final String[] selectionArgs = {
2706                Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)),
2707                Long.toString(groupId),
2708                Long.toString(rawContactId)};
2709        mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs);
2710    }
2711
2712    /**
2713     * Inserts an item in the data table
2714     *
2715     * @param values the values for the new row
2716     * @return the row ID of the newly created row
2717     */
2718    private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
2719        long id = 0;
2720        mValues.clear();
2721        mValues.putAll(values);
2722
2723        long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
2724
2725        // Replace package with internal mapping
2726        final String packageName = mValues.getAsString(Data.RES_PACKAGE);
2727        if (packageName != null) {
2728            mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
2729        }
2730        mValues.remove(Data.RES_PACKAGE);
2731
2732        // Replace mimetype with internal mapping
2733        final String mimeType = mValues.getAsString(Data.MIMETYPE);
2734        if (TextUtils.isEmpty(mimeType)) {
2735            throw new IllegalArgumentException(Data.MIMETYPE + " is required");
2736        }
2737
2738        mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
2739        mValues.remove(Data.MIMETYPE);
2740
2741        DataRowHandler rowHandler = getDataRowHandler(mimeType);
2742        id = rowHandler.insert(mDb, rawContactId, mValues);
2743        if (!callerIsSyncAdapter) {
2744            setRawContactDirty(rawContactId);
2745        }
2746        mUpdatedRawContacts.add(rawContactId);
2747        return id;
2748    }
2749
2750    private void triggerAggregation(long rawContactId) {
2751        if (!mContactAggregator.isEnabled()) {
2752            return;
2753        }
2754
2755        int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
2756        switch (aggregationMode) {
2757            case RawContacts.AGGREGATION_MODE_DISABLED:
2758                break;
2759
2760            case RawContacts.AGGREGATION_MODE_DEFAULT: {
2761                mContactAggregator.markForAggregation(rawContactId, aggregationMode, false);
2762                break;
2763            }
2764
2765            case RawContacts.AGGREGATION_MODE_SUSPENDED: {
2766                long contactId = mDbHelper.getContactId(rawContactId);
2767
2768                if (contactId != 0) {
2769                    mContactAggregator.updateAggregateData(contactId);
2770                }
2771                break;
2772            }
2773
2774            case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
2775                mContactAggregator.aggregateContact(mDb, rawContactId);
2776                break;
2777            }
2778        }
2779    }
2780
2781    /**
2782     * Returns the group id of the group with sourceId and the same account as rawContactId.
2783     * If the group doesn't already exist then it is first created,
2784     * @param db SQLiteDatabase to use for this operation
2785     * @param rawContactId the contact this group is associated with
2786     * @param sourceId the sourceIf of the group to query or create
2787     * @return the group id of the existing or created group
2788     * @throws IllegalArgumentException if the contact is not associated with an account
2789     * @throws IllegalStateException if a group needs to be created but the creation failed
2790     */
2791    private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId,
2792            Account account) {
2793
2794        if (account == null) {
2795            mSelectionArgs1[0] = String.valueOf(rawContactId);
2796            Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
2797                    RawContacts._ID + "=?", mSelectionArgs1, null, null, null);
2798            try {
2799                if (c.moveToFirst()) {
2800                    String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME);
2801                    String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
2802                    if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
2803                        account = new Account(accountName, accountType);
2804                    }
2805                }
2806            } finally {
2807                c.close();
2808            }
2809        }
2810
2811        if (account == null) {
2812            throw new IllegalArgumentException("if the groupmembership only "
2813                    + "has a sourceid the the contact must be associated with "
2814                    + "an account");
2815        }
2816
2817        ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId);
2818        if (entries == null) {
2819            entries = new ArrayList<GroupIdCacheEntry>(1);
2820            mGroupIdCache.put(sourceId, entries);
2821        }
2822
2823        int count = entries.size();
2824        for (int i = 0; i < count; i++) {
2825            GroupIdCacheEntry entry = entries.get(i);
2826            if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) {
2827                return entry.groupId;
2828            }
2829        }
2830
2831        GroupIdCacheEntry entry = new GroupIdCacheEntry();
2832        entry.accountName = account.name;
2833        entry.accountType = account.type;
2834        entry.sourceId = sourceId;
2835        entries.add(0, entry);
2836
2837        // look up the group that contains this sourceId and has the same account name and type
2838        // as the contact refered to by rawContactId
2839        Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
2840                Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
2841                new String[]{sourceId, account.name, account.type}, null, null, null);
2842        try {
2843            if (c.moveToFirst()) {
2844                entry.groupId = c.getLong(0);
2845            } else {
2846                ContentValues groupValues = new ContentValues();
2847                groupValues.put(Groups.ACCOUNT_NAME, account.name);
2848                groupValues.put(Groups.ACCOUNT_TYPE, account.type);
2849                groupValues.put(Groups.SOURCE_ID, sourceId);
2850                long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
2851                if (groupId < 0) {
2852                    throw new IllegalStateException("unable to create a new group with "
2853                            + "this sourceid: " + groupValues);
2854                }
2855                entry.groupId = groupId;
2856            }
2857        } finally {
2858            c.close();
2859        }
2860
2861        return entry.groupId;
2862    }
2863
2864    private interface DisplayNameQuery {
2865        public static final String RAW_SQL =
2866                "SELECT "
2867                        + DataColumns.MIMETYPE_ID + ","
2868                        + Data.IS_PRIMARY + ","
2869                        + Data.DATA1 + ","
2870                        + Data.DATA2 + ","
2871                        + Data.DATA3 + ","
2872                        + Data.DATA4 + ","
2873                        + Data.DATA5 + ","
2874                        + Data.DATA6 + ","
2875                        + Data.DATA7 + ","
2876                        + Data.DATA8 + ","
2877                        + Data.DATA9 + ","
2878                        + Data.DATA10 + ","
2879                        + Data.DATA11 +
2880                " FROM " + Tables.DATA +
2881                " WHERE " + Data.RAW_CONTACT_ID + "=?" +
2882                        " AND (" + Data.DATA1 + " NOT NULL OR " +
2883                                Organization.TITLE + " NOT NULL)";
2884
2885        public static final int MIMETYPE = 0;
2886        public static final int IS_PRIMARY = 1;
2887        public static final int DATA1 = 2;
2888        public static final int GIVEN_NAME = 3;                         // data2
2889        public static final int FAMILY_NAME = 4;                        // data3
2890        public static final int PREFIX = 5;                             // data4
2891        public static final int TITLE = 5;                              // data4
2892        public static final int MIDDLE_NAME = 6;                        // data5
2893        public static final int SUFFIX = 7;                             // data6
2894        public static final int PHONETIC_GIVEN_NAME = 8;                // data7
2895        public static final int PHONETIC_MIDDLE_NAME = 9;               // data8
2896        public static final int ORGANIZATION_PHONETIC_NAME = 9;         // data8
2897        public static final int PHONETIC_FAMILY_NAME = 10;              // data9
2898        public static final int FULL_NAME_STYLE = 11;                   // data10
2899        public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11;  // data10
2900        public static final int PHONETIC_NAME_STYLE = 12;               // data11
2901    }
2902
2903    /**
2904     * Updates a raw contact display name based on data rows, e.g. structured name,
2905     * organization, email etc.
2906     */
2907    public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
2908        int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
2909        NameSplitter.Name bestName = null;
2910        String bestDisplayName = null;
2911        String bestPhoneticName = null;
2912        int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
2913
2914        mSelectionArgs1[0] = String.valueOf(rawContactId);
2915        Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1);
2916        try {
2917            while (c.moveToNext()) {
2918                int mimeType = c.getInt(DisplayNameQuery.MIMETYPE);
2919                int source = getDisplayNameSource(mimeType);
2920                if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
2921                    continue;
2922                }
2923
2924                if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) {
2925                    continue;
2926                }
2927
2928                if (mimeType == mMimeTypeIdStructuredName) {
2929                    NameSplitter.Name name;
2930                    if (bestName != null) {
2931                        name = new NameSplitter.Name();
2932                    } else {
2933                        name = mName;
2934                        name.clear();
2935                    }
2936                    name.prefix = c.getString(DisplayNameQuery.PREFIX);
2937                    name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME);
2938                    name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME);
2939                    name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME);
2940                    name.suffix = c.getString(DisplayNameQuery.SUFFIX);
2941                    name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE)
2942                            ? FullNameStyle.UNDEFINED
2943                            : c.getInt(DisplayNameQuery.FULL_NAME_STYLE);
2944                    name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME);
2945                    name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME);
2946                    name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME);
2947                    name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE)
2948                            ? PhoneticNameStyle.UNDEFINED
2949                            : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE);
2950                    if (!name.isEmpty()) {
2951                        bestDisplayNameSource = source;
2952                        bestName = name;
2953                    }
2954                } else if (mimeType == mMimeTypeIdOrganization) {
2955                    mCharArrayBuffer.sizeCopied = 0;
2956                    c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
2957                    if (mCharArrayBuffer.sizeCopied != 0) {
2958                        bestDisplayNameSource = source;
2959                        bestDisplayName = new String(mCharArrayBuffer.data, 0,
2960                                mCharArrayBuffer.sizeCopied);
2961                        bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME);
2962                        bestPhoneticNameStyle =
2963                                c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE)
2964                                    ? PhoneticNameStyle.UNDEFINED
2965                                    : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE);
2966                    } else {
2967                        c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer);
2968                        if (mCharArrayBuffer.sizeCopied != 0) {
2969                            bestDisplayNameSource = source;
2970                            bestDisplayName = new String(mCharArrayBuffer.data, 0,
2971                                    mCharArrayBuffer.sizeCopied);
2972                            bestPhoneticName = null;
2973                            bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
2974                        }
2975                    }
2976                } else {
2977                    // Display name is at DATA1 in all other types.
2978                    // This is ensured in the constructor.
2979
2980                    mCharArrayBuffer.sizeCopied = 0;
2981                    c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
2982                    if (mCharArrayBuffer.sizeCopied != 0) {
2983                        bestDisplayNameSource = source;
2984                        bestDisplayName = new String(mCharArrayBuffer.data, 0,
2985                                mCharArrayBuffer.sizeCopied);
2986                        bestPhoneticName = null;
2987                        bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
2988                    }
2989                }
2990            }
2991
2992        } finally {
2993            c.close();
2994        }
2995
2996        String displayNamePrimary;
2997        String displayNameAlternative;
2998        String sortKeyPrimary = null;
2999        String sortKeyAlternative = null;
3000        int displayNameStyle = FullNameStyle.UNDEFINED;
3001
3002        if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) {
3003            displayNameStyle = bestName.fullNameStyle;
3004            if (displayNameStyle == FullNameStyle.CJK
3005                    || displayNameStyle == FullNameStyle.UNDEFINED) {
3006                displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
3007                bestName.fullNameStyle = displayNameStyle;
3008            }
3009
3010            displayNamePrimary = mNameSplitter.join(bestName, true);
3011            displayNameAlternative = mNameSplitter.join(bestName, false);
3012
3013            bestPhoneticName = mNameSplitter.joinPhoneticName(bestName);
3014            bestPhoneticNameStyle = bestName.phoneticNameStyle;
3015        } else {
3016            displayNamePrimary = displayNameAlternative = bestDisplayName;
3017        }
3018
3019        if (bestPhoneticName != null) {
3020            sortKeyPrimary = sortKeyAlternative = bestPhoneticName;
3021            if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
3022                bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName);
3023            }
3024        } else {
3025            if (displayNameStyle == FullNameStyle.UNDEFINED) {
3026                displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName);
3027                if (displayNameStyle == FullNameStyle.UNDEFINED
3028                        || displayNameStyle == FullNameStyle.CJK) {
3029                    displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle(
3030                            displayNameStyle, bestPhoneticNameStyle);
3031                }
3032                displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
3033            }
3034            if (displayNameStyle == FullNameStyle.CHINESE ||
3035                    displayNameStyle == FullNameStyle.CJK) {
3036                sortKeyPrimary = sortKeyAlternative =
3037                        ContactLocaleUtils.getIntance().getSortKey(
3038                                displayNamePrimary, displayNameStyle);
3039            }
3040        }
3041
3042        if (sortKeyPrimary == null) {
3043            sortKeyPrimary = displayNamePrimary;
3044            sortKeyAlternative = displayNameAlternative;
3045        }
3046
3047        setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary,
3048                displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle,
3049                sortKeyPrimary, sortKeyAlternative);
3050    }
3051
3052    private int getDisplayNameSource(int mimeTypeId) {
3053        if (mimeTypeId == mMimeTypeIdStructuredName) {
3054            return DisplayNameSources.STRUCTURED_NAME;
3055        } else if (mimeTypeId == mMimeTypeIdEmail) {
3056            return DisplayNameSources.EMAIL;
3057        } else if (mimeTypeId == mMimeTypeIdPhone) {
3058            return DisplayNameSources.PHONE;
3059        } else if (mimeTypeId == mMimeTypeIdOrganization) {
3060            return DisplayNameSources.ORGANIZATION;
3061        } else if (mimeTypeId == mMimeTypeIdNickname) {
3062            return DisplayNameSources.NICKNAME;
3063        } else {
3064            return DisplayNameSources.UNDEFINED;
3065        }
3066    }
3067
3068    /**
3069     * Delete data row by row so that fixing of primaries etc work correctly.
3070     */
3071    private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
3072        int count = 0;
3073
3074        // Note that the query will return data according to the access restrictions,
3075        // so we don't need to worry about deleting data we don't have permission to read.
3076        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null);
3077        try {
3078            while(c.moveToNext()) {
3079                long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
3080                String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
3081                DataRowHandler rowHandler = getDataRowHandler(mimeType);
3082                count += rowHandler.delete(mDb, c);
3083                if (!callerIsSyncAdapter) {
3084                    setRawContactDirty(rawContactId);
3085                }
3086            }
3087        } finally {
3088            c.close();
3089        }
3090
3091        return count;
3092    }
3093
3094    /**
3095     * Delete a data row provided that it is one of the allowed mime types.
3096     */
3097    public int deleteData(long dataId, String[] allowedMimeTypes) {
3098
3099        // Note that the query will return data according to the access restrictions,
3100        // so we don't need to worry about deleting data we don't have permission to read.
3101        mSelectionArgs1[0] = String.valueOf(dataId);
3102        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?",
3103                mSelectionArgs1, null);
3104
3105        try {
3106            if (!c.moveToFirst()) {
3107                return 0;
3108            }
3109
3110            String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
3111            boolean valid = false;
3112            for (int i = 0; i < allowedMimeTypes.length; i++) {
3113                if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
3114                    valid = true;
3115                    break;
3116                }
3117            }
3118
3119            if (!valid) {
3120                throw new IllegalArgumentException("Data type mismatch: expected "
3121                        + Lists.newArrayList(allowedMimeTypes));
3122            }
3123
3124            DataRowHandler rowHandler = getDataRowHandler(mimeType);
3125            return rowHandler.delete(mDb, c);
3126        } finally {
3127            c.close();
3128        }
3129    }
3130
3131    /**
3132     * Inserts an item in the groups table
3133     */
3134    private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
3135        mValues.clear();
3136        mValues.putAll(values);
3137
3138        final Account account = resolveAccount(uri, mValues);
3139
3140        // Replace package with internal mapping
3141        final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
3142        if (packageName != null) {
3143            mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
3144        }
3145        mValues.remove(Groups.RES_PACKAGE);
3146
3147        final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null
3148                ? mValues.getAsLong(Groups.FAVORITES) != 0
3149                : false;
3150
3151        if (!callerIsSyncAdapter) {
3152            mValues.put(Groups.DIRTY, 1);
3153        }
3154
3155        long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
3156
3157        if (!callerIsSyncAdapter && isFavoritesGroup) {
3158            // add all starred raw contacts to this group
3159            String selection;
3160            String[] selectionArgs;
3161            if (account == null) {
3162                selection = RawContacts.ACCOUNT_NAME + " IS NULL AND "
3163                        + RawContacts.ACCOUNT_TYPE + " IS NULL";
3164                selectionArgs = null;
3165            } else {
3166                selection = RawContacts.ACCOUNT_NAME + "=? AND "
3167                        + RawContacts.ACCOUNT_TYPE + "=?";
3168                selectionArgs = new String[]{account.name, account.type};
3169            }
3170            Cursor c = mDb.query(Tables.RAW_CONTACTS,
3171                    new String[]{RawContacts._ID, RawContacts.STARRED},
3172                    selection, selectionArgs, null, null, null);
3173            try {
3174                while (c.moveToNext()) {
3175                    if (c.getLong(1) != 0) {
3176                        final long rawContactId = c.getLong(0);
3177                        insertDataGroupMembership(rawContactId, result);
3178                        setRawContactDirty(rawContactId);
3179                    }
3180                }
3181            } finally {
3182                c.close();
3183            }
3184        }
3185
3186        if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
3187            mVisibleTouched = true;
3188        }
3189
3190        return result;
3191    }
3192
3193    private long insertSettings(Uri uri, ContentValues values) {
3194        final long id = mDb.insert(Tables.SETTINGS, null, values);
3195
3196        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
3197            mVisibleTouched = true;
3198        }
3199
3200        return id;
3201    }
3202
3203    /**
3204     * Inserts a status update.
3205     */
3206    public long insertStatusUpdate(ContentValues values) {
3207        final String handle = values.getAsString(StatusUpdates.IM_HANDLE);
3208        final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL);
3209        String customProtocol = null;
3210
3211        if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
3212            customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
3213            if (TextUtils.isEmpty(customProtocol)) {
3214                throw new IllegalArgumentException(
3215                        "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
3216            }
3217        }
3218
3219        long rawContactId = -1;
3220        long contactId = -1;
3221        Long dataId = values.getAsLong(StatusUpdates.DATA_ID);
3222        mSb.setLength(0);
3223        mSelectionArgs.clear();
3224        if (dataId != null) {
3225            // Lookup the contact info for the given data row.
3226
3227            mSb.append(Tables.DATA + "." + Data._ID + "=?");
3228            mSelectionArgs.add(String.valueOf(dataId));
3229        } else {
3230            // Lookup the data row to attach this presence update to
3231
3232            if (TextUtils.isEmpty(handle) || protocol == null) {
3233                throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
3234            }
3235
3236            // TODO: generalize to allow other providers to match against email
3237            boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
3238
3239            String mimeTypeIdIm = String.valueOf(mMimeTypeIdIm);
3240            if (matchEmail) {
3241                String mimeTypeIdEmail = String.valueOf(mMimeTypeIdEmail);
3242
3243                // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
3244                // the "OR" conjunction confuses it and it switches to a full scan of
3245                // the raw_contacts table.
3246
3247                // This code relies on the fact that Im.DATA and Email.DATA are in fact the same
3248                // column - Data.DATA1
3249                mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" +
3250                        " AND " + Data.DATA1 + "=?" +
3251                        " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?");
3252                mSelectionArgs.add(mimeTypeIdEmail);
3253                mSelectionArgs.add(mimeTypeIdIm);
3254                mSelectionArgs.add(handle);
3255                mSelectionArgs.add(mimeTypeIdIm);
3256                mSelectionArgs.add(String.valueOf(protocol));
3257                if (customProtocol != null) {
3258                    mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
3259                    mSelectionArgs.add(customProtocol);
3260                }
3261                mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))");
3262                mSelectionArgs.add(mimeTypeIdEmail);
3263            } else {
3264                mSb.append(DataColumns.MIMETYPE_ID + "=?" +
3265                        " AND " + Im.PROTOCOL + "=?" +
3266                        " AND " + Im.DATA + "=?");
3267                mSelectionArgs.add(mimeTypeIdIm);
3268                mSelectionArgs.add(String.valueOf(protocol));
3269                mSelectionArgs.add(handle);
3270                if (customProtocol != null) {
3271                    mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
3272                    mSelectionArgs.add(customProtocol);
3273                }
3274            }
3275
3276            if (values.containsKey(StatusUpdates.DATA_ID)) {
3277                mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?");
3278                mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID));
3279            }
3280        }
3281        mSb.append(" AND ").append(getContactsRestrictions());
3282
3283        Cursor cursor = null;
3284        try {
3285            cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
3286                    mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
3287                    Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID);
3288            if (cursor.moveToFirst()) {
3289                dataId = cursor.getLong(DataContactsQuery.DATA_ID);
3290                rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
3291                contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
3292            } else {
3293                // No contact found, return a null URI
3294                return -1;
3295            }
3296        } finally {
3297            if (cursor != null) {
3298                cursor.close();
3299            }
3300        }
3301
3302        if (values.containsKey(StatusUpdates.PRESENCE)) {
3303            if (customProtocol == null) {
3304                // We cannot allow a null in the custom protocol field, because SQLite3 does not
3305                // properly enforce uniqueness of null values
3306                customProtocol = "";
3307            }
3308
3309            mValues.clear();
3310            mValues.put(StatusUpdates.DATA_ID, dataId);
3311            mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
3312            mValues.put(PresenceColumns.CONTACT_ID, contactId);
3313            mValues.put(StatusUpdates.PROTOCOL, protocol);
3314            mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
3315            mValues.put(StatusUpdates.IM_HANDLE, handle);
3316            if (values.containsKey(StatusUpdates.IM_ACCOUNT)) {
3317                mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT));
3318            }
3319            mValues.put(StatusUpdates.PRESENCE,
3320                    values.getAsString(StatusUpdates.PRESENCE));
3321            mValues.put(StatusUpdates.CHAT_CAPABILITY,
3322                    values.getAsString(StatusUpdates.CHAT_CAPABILITY));
3323
3324            // Insert the presence update
3325            mDb.replace(Tables.PRESENCE, null, mValues);
3326        }
3327
3328
3329        if (values.containsKey(StatusUpdates.STATUS)) {
3330            String status = values.getAsString(StatusUpdates.STATUS);
3331            String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
3332            Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL);
3333
3334            if (TextUtils.isEmpty(resPackage)
3335                    && (labelResource == null || labelResource == 0)
3336                    && protocol != null) {
3337                labelResource = Im.getProtocolLabelResource(protocol);
3338            }
3339
3340            Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON);
3341            // TODO compute the default icon based on the protocol
3342
3343            if (TextUtils.isEmpty(status)) {
3344                mStatusUpdateDelete.bindLong(1, dataId);
3345                mStatusUpdateDelete.execute();
3346            } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) {
3347                long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
3348                mStatusUpdateReplace.bindLong(1, dataId);
3349                mStatusUpdateReplace.bindLong(2, timestamp);
3350                bindString(mStatusUpdateReplace, 3, status);
3351                bindString(mStatusUpdateReplace, 4, resPackage);
3352                bindLong(mStatusUpdateReplace, 5, iconResource);
3353                bindLong(mStatusUpdateReplace, 6, labelResource);
3354                mStatusUpdateReplace.execute();
3355            } else {
3356
3357                try {
3358                    mStatusUpdateInsert.bindLong(1, dataId);
3359                    bindString(mStatusUpdateInsert, 2, status);
3360                    bindString(mStatusUpdateInsert, 3, resPackage);
3361                    bindLong(mStatusUpdateInsert, 4, iconResource);
3362                    bindLong(mStatusUpdateInsert, 5, labelResource);
3363                    mStatusUpdateInsert.executeInsert();
3364                } catch (SQLiteConstraintException e) {
3365                    // The row already exists - update it
3366                    long timestamp = System.currentTimeMillis();
3367                    mStatusUpdateAutoTimestamp.bindLong(1, timestamp);
3368                    bindString(mStatusUpdateAutoTimestamp, 2, status);
3369                    mStatusUpdateAutoTimestamp.bindLong(3, dataId);
3370                    bindString(mStatusUpdateAutoTimestamp, 4, status);
3371                    mStatusUpdateAutoTimestamp.execute();
3372
3373                    bindString(mStatusAttributionUpdate, 1, resPackage);
3374                    bindLong(mStatusAttributionUpdate, 2, iconResource);
3375                    bindLong(mStatusAttributionUpdate, 3, labelResource);
3376                    mStatusAttributionUpdate.bindLong(4, dataId);
3377                    mStatusAttributionUpdate.execute();
3378                }
3379            }
3380        }
3381
3382        if (contactId != -1) {
3383            mContactAggregator.updateLastStatusUpdateId(contactId);
3384        }
3385
3386        return dataId;
3387    }
3388
3389    @Override
3390    protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
3391        if (VERBOSE_LOGGING) {
3392            Log.v(TAG, "deleteInTransaction: " + uri);
3393        }
3394        flushTransactionalChanges();
3395        final boolean callerIsSyncAdapter =
3396                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
3397        final int match = sUriMatcher.match(uri);
3398        switch (match) {
3399            case SYNCSTATE:
3400                return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
3401
3402            case SYNCSTATE_ID:
3403                String selectionWithId =
3404                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
3405                        + (selection == null ? "" : " AND (" + selection + ")");
3406                return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs);
3407
3408            case CONTACTS: {
3409                // TODO
3410                return 0;
3411            }
3412
3413            case CONTACTS_ID: {
3414                long contactId = ContentUris.parseId(uri);
3415                return deleteContact(contactId, callerIsSyncAdapter);
3416            }
3417
3418            case CONTACTS_LOOKUP: {
3419                final List<String> pathSegments = uri.getPathSegments();
3420                final int segmentCount = pathSegments.size();
3421                if (segmentCount < 3) {
3422                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
3423                            "Missing a lookup key", uri));
3424                }
3425                final String lookupKey = pathSegments.get(2);
3426                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
3427                return deleteContact(contactId, callerIsSyncAdapter);
3428            }
3429
3430            case CONTACTS_LOOKUP_ID: {
3431                // lookup contact by id and lookup key to see if they still match the actual record
3432                final List<String> pathSegments = uri.getPathSegments();
3433                final String lookupKey = pathSegments.get(2);
3434                SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
3435                setTablesAndProjectionMapForContacts(lookupQb, uri, null);
3436                long contactId = ContentUris.parseId(uri);
3437                String[] args;
3438                if (selectionArgs == null) {
3439                    args = new String[2];
3440                } else {
3441                    args = new String[selectionArgs.length + 2];
3442                    System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
3443                }
3444                args[0] = String.valueOf(contactId);
3445                args[1] = Uri.encode(lookupKey);
3446                lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
3447                final SQLiteDatabase db = mDbHelper.getReadableDatabase();
3448                Cursor c = query(db, lookupQb, null, selection, args, null, null, null);
3449                try {
3450                    if (c.getCount() == 1) {
3451                        // contact was unmodified so go ahead and delete it
3452                        return deleteContact(contactId, callerIsSyncAdapter);
3453                    } else {
3454                        // row was changed (e.g. the merging might have changed), we got multiple
3455                        // rows or the supplied selection filtered the record out
3456                        return 0;
3457                    }
3458                } finally {
3459                    c.close();
3460                }
3461            }
3462
3463            case RAW_CONTACTS: {
3464                int numDeletes = 0;
3465                Cursor c = mDb.query(Tables.RAW_CONTACTS,
3466                        new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
3467                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
3468                try {
3469                    while (c.moveToNext()) {
3470                        final long rawContactId = c.getLong(0);
3471                        long contactId = c.getLong(1);
3472                        numDeletes += deleteRawContact(rawContactId, contactId,
3473                                callerIsSyncAdapter);
3474                    }
3475                } finally {
3476                    c.close();
3477                }
3478                return numDeletes;
3479            }
3480
3481            case RAW_CONTACTS_ID: {
3482                final long rawContactId = ContentUris.parseId(uri);
3483                return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId),
3484                        callerIsSyncAdapter);
3485            }
3486
3487            case DATA: {
3488                mSyncToNetwork |= !callerIsSyncAdapter;
3489                return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
3490                        callerIsSyncAdapter);
3491            }
3492
3493            case DATA_ID:
3494            case PHONES_ID:
3495            case EMAILS_ID:
3496            case POSTALS_ID: {
3497                long dataId = ContentUris.parseId(uri);
3498                mSyncToNetwork |= !callerIsSyncAdapter;
3499                mSelectionArgs1[0] = String.valueOf(dataId);
3500                return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
3501            }
3502
3503            case GROUPS_ID: {
3504                mSyncToNetwork |= !callerIsSyncAdapter;
3505                return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
3506            }
3507
3508            case GROUPS: {
3509                int numDeletes = 0;
3510                Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
3511                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
3512                try {
3513                    while (c.moveToNext()) {
3514                        numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
3515                    }
3516                } finally {
3517                    c.close();
3518                }
3519                if (numDeletes > 0) {
3520                    mSyncToNetwork |= !callerIsSyncAdapter;
3521                }
3522                return numDeletes;
3523            }
3524
3525            case SETTINGS: {
3526                mSyncToNetwork |= !callerIsSyncAdapter;
3527                return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs);
3528            }
3529
3530            case STATUS_UPDATES: {
3531                return deleteStatusUpdates(selection, selectionArgs);
3532            }
3533
3534            default: {
3535                mSyncToNetwork = true;
3536                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
3537            }
3538        }
3539    }
3540
3541    public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
3542        mGroupIdCache.clear();
3543        final long groupMembershipMimetypeId = mDbHelper
3544                .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
3545        mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
3546                + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
3547                + groupId, null);
3548
3549        try {
3550            if (callerIsSyncAdapter) {
3551                return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
3552            } else {
3553                mValues.clear();
3554                mValues.put(Groups.DELETED, 1);
3555                mValues.put(Groups.DIRTY, 1);
3556                return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
3557            }
3558        } finally {
3559            mVisibleTouched = true;
3560        }
3561    }
3562
3563    private int deleteSettings(Uri uri, String selection, String[] selectionArgs) {
3564        final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs);
3565        mVisibleTouched = true;
3566        return count;
3567    }
3568
3569    private int deleteContact(long contactId, boolean callerIsSyncAdapter) {
3570        mSelectionArgs1[0] = Long.toString(contactId);
3571        Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
3572                RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
3573                null, null, null);
3574        try {
3575            while (c.moveToNext()) {
3576                long rawContactId = c.getLong(0);
3577                markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
3578            }
3579        } finally {
3580            c.close();
3581        }
3582
3583        mProviderStatusUpdateNeeded = true;
3584
3585        return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
3586    }
3587
3588    public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
3589        mContactAggregator.invalidateAggregationExceptionCache();
3590        mProviderStatusUpdateNeeded = true;
3591
3592        if (callerIsSyncAdapter) {
3593            mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
3594            int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
3595            mContactAggregator.updateDisplayNameForContact(mDb, contactId);
3596            return count;
3597        } else {
3598            mDbHelper.removeContactIfSingleton(rawContactId);
3599            return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
3600        }
3601    }
3602
3603    private int deleteStatusUpdates(String selection, String[] selectionArgs) {
3604      // delete from both tables: presence and status_updates
3605      // TODO should account type/name be appended to the where clause?
3606      if (VERBOSE_LOGGING) {
3607          Log.v(TAG, "deleting data from status_updates for " + selection);
3608      }
3609      mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
3610          selectionArgs);
3611      return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
3612    }
3613
3614    private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) {
3615        mSyncToNetwork = true;
3616
3617        mValues.clear();
3618        mValues.put(RawContacts.DELETED, 1);
3619        mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
3620        mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
3621        mValues.putNull(RawContacts.CONTACT_ID);
3622        mValues.put(RawContacts.DIRTY, 1);
3623        return updateRawContact(rawContactId, mValues, callerIsSyncAdapter);
3624    }
3625
3626    @Override
3627    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
3628            String[] selectionArgs) {
3629        if (VERBOSE_LOGGING) {
3630            Log.v(TAG, "updateInTransaction: " + uri);
3631        }
3632
3633        int count = 0;
3634
3635        final int match = sUriMatcher.match(uri);
3636        if (match == SYNCSTATE_ID && selection == null) {
3637            long rowId = ContentUris.parseId(uri);
3638            Object data = values.get(ContactsContract.SyncState.DATA);
3639            mUpdatedSyncStates.put(rowId, data);
3640            return 1;
3641        }
3642        flushTransactionalChanges();
3643        final boolean callerIsSyncAdapter =
3644                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
3645        switch(match) {
3646            case SYNCSTATE:
3647                return mDbHelper.getSyncState().update(mDb, values,
3648                        appendAccountToSelection(uri, selection), selectionArgs);
3649
3650            case SYNCSTATE_ID: {
3651                selection = appendAccountToSelection(uri, selection);
3652                String selectionWithId =
3653                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
3654                        + (selection == null ? "" : " AND (" + selection + ")");
3655                return mDbHelper.getSyncState().update(mDb, values,
3656                        selectionWithId, selectionArgs);
3657            }
3658
3659            case CONTACTS: {
3660                count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
3661                break;
3662            }
3663
3664            case CONTACTS_ID: {
3665                count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter);
3666                break;
3667            }
3668
3669            case CONTACTS_LOOKUP:
3670            case CONTACTS_LOOKUP_ID: {
3671                final List<String> pathSegments = uri.getPathSegments();
3672                final int segmentCount = pathSegments.size();
3673                if (segmentCount < 3) {
3674                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
3675                            "Missing a lookup key", uri));
3676                }
3677                final String lookupKey = pathSegments.get(2);
3678                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
3679                count = updateContactOptions(contactId, values, callerIsSyncAdapter);
3680                break;
3681            }
3682
3683            case RAW_CONTACTS_DATA: {
3684                final String rawContactId = uri.getPathSegments().get(1);
3685                String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
3686                    + (selection == null ? "" : " AND " + selection);
3687
3688                count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
3689
3690                break;
3691            }
3692
3693            case DATA: {
3694                count = updateData(uri, values, appendAccountToSelection(uri, selection),
3695                        selectionArgs, callerIsSyncAdapter);
3696                if (count > 0) {
3697                    mSyncToNetwork |= !callerIsSyncAdapter;
3698                }
3699                break;
3700            }
3701
3702            case DATA_ID:
3703            case PHONES_ID:
3704            case EMAILS_ID:
3705            case POSTALS_ID: {
3706                count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
3707                if (count > 0) {
3708                    mSyncToNetwork |= !callerIsSyncAdapter;
3709                }
3710                break;
3711            }
3712
3713            case RAW_CONTACTS: {
3714                selection = appendAccountToSelection(uri, selection);
3715                count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
3716                break;
3717            }
3718
3719            case RAW_CONTACTS_ID: {
3720                long rawContactId = ContentUris.parseId(uri);
3721                if (selection != null) {
3722                    selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
3723                    count = updateRawContacts(values, RawContacts._ID + "=?"
3724                                    + " AND(" + selection + ")", selectionArgs,
3725                            callerIsSyncAdapter);
3726                } else {
3727                    mSelectionArgs1[0] = String.valueOf(rawContactId);
3728                    count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
3729                            callerIsSyncAdapter);
3730                }
3731                break;
3732            }
3733
3734            case GROUPS: {
3735                count = updateGroups(uri, values, appendAccountToSelection(uri, selection),
3736                        selectionArgs, callerIsSyncAdapter);
3737                if (count > 0) {
3738                    mSyncToNetwork |= !callerIsSyncAdapter;
3739                }
3740                break;
3741            }
3742
3743            case GROUPS_ID: {
3744                long groupId = ContentUris.parseId(uri);
3745                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
3746                String selectionWithId = Groups._ID + "=? "
3747                        + (selection == null ? "" : " AND " + selection);
3748                count = updateGroups(uri, values, selectionWithId, selectionArgs,
3749                        callerIsSyncAdapter);
3750                if (count > 0) {
3751                    mSyncToNetwork |= !callerIsSyncAdapter;
3752                }
3753                break;
3754            }
3755
3756            case AGGREGATION_EXCEPTIONS: {
3757                count = updateAggregationException(mDb, values);
3758                break;
3759            }
3760
3761            case SETTINGS: {
3762                count = updateSettings(uri, values, appendAccountToSelection(uri, selection),
3763                        selectionArgs);
3764                mSyncToNetwork |= !callerIsSyncAdapter;
3765                break;
3766            }
3767
3768            case STATUS_UPDATES: {
3769                count = updateStatusUpdate(uri, values, selection, selectionArgs);
3770                break;
3771            }
3772
3773            case DIRECTORIES: {
3774                mContactDirectoryManager.scheduleDirectoryUpdateForCaller();
3775                count = 1;
3776                break;
3777            }
3778
3779            default: {
3780                mSyncToNetwork = true;
3781                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
3782            }
3783        }
3784
3785        return count;
3786    }
3787
3788    private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
3789        String[] selectionArgs) {
3790        // update status_updates table, if status is provided
3791        // TODO should account type/name be appended to the where clause?
3792        int updateCount = 0;
3793        ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
3794        if (settableValues.size() > 0) {
3795          updateCount = mDb.update(Tables.STATUS_UPDATES,
3796                    settableValues,
3797                    getWhereClauseForStatusUpdatesTable(selection),
3798                    selectionArgs);
3799        }
3800
3801        // now update the Presence table
3802        settableValues = getSettableColumnsForPresenceTable(values);
3803        if (settableValues.size() > 0) {
3804          updateCount = mDb.update(Tables.PRESENCE, settableValues,
3805                    selection, selectionArgs);
3806        }
3807        // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
3808        // potentially get updated in this method.
3809        return updateCount;
3810    }
3811
3812    /**
3813     * Build a where clause to select the rows to be updated in status_updates table.
3814     */
3815    private String getWhereClauseForStatusUpdatesTable(String selection) {
3816        mSb.setLength(0);
3817        mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
3818        mSb.append(selection);
3819        mSb.append(")");
3820        return mSb.toString();
3821    }
3822
3823    private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
3824        mValues.clear();
3825        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
3826            StatusUpdates.STATUS);
3827        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
3828            StatusUpdates.STATUS_TIMESTAMP);
3829        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
3830            StatusUpdates.STATUS_RES_PACKAGE);
3831        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
3832            StatusUpdates.STATUS_LABEL);
3833        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
3834            StatusUpdates.STATUS_ICON);
3835        return mValues;
3836    }
3837
3838    private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
3839        mValues.clear();
3840        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
3841            StatusUpdates.PRESENCE);
3842        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values,
3843                StatusUpdates.CHAT_CAPABILITY);
3844        return mValues;
3845    }
3846
3847    private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
3848            String[] selectionArgs, boolean callerIsSyncAdapter) {
3849
3850        mGroupIdCache.clear();
3851
3852        ContentValues updatedValues;
3853        if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
3854            updatedValues = mValues;
3855            updatedValues.clear();
3856            updatedValues.putAll(values);
3857            updatedValues.put(Groups.DIRTY, 1);
3858        } else {
3859            updatedValues = values;
3860        }
3861
3862        int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs);
3863        if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
3864            mVisibleTouched = true;
3865        }
3866        if (updatedValues.containsKey(Groups.SHOULD_SYNC)
3867                && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
3868            Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
3869                    Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null,
3870                    null, null);
3871            String accountName;
3872            String accountType;
3873            try {
3874                while (c.moveToNext()) {
3875                    accountName = c.getString(0);
3876                    accountType = c.getString(1);
3877                    if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
3878                        Account account = new Account(accountName, accountType);
3879                        ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
3880                                new Bundle());
3881                        break;
3882                    }
3883                }
3884            } finally {
3885                c.close();
3886            }
3887        }
3888        return count;
3889    }
3890
3891    private int updateSettings(Uri uri, ContentValues values, String selection,
3892            String[] selectionArgs) {
3893        final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
3894        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
3895            mVisibleTouched = true;
3896        }
3897        return count;
3898    }
3899
3900    private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs,
3901            boolean callerIsSyncAdapter) {
3902        if (values.containsKey(RawContacts.CONTACT_ID)) {
3903            throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
3904                    "in content values. Contact IDs are assigned automatically");
3905        }
3906
3907        if (!callerIsSyncAdapter) {
3908            selection = DatabaseUtils.concatenateWhere(selection,
3909                    RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0");
3910        }
3911
3912        int count = 0;
3913        Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
3914                new String[] { RawContacts._ID }, selection,
3915                selectionArgs, null, null, null);
3916        try {
3917            while (cursor.moveToNext()) {
3918                long rawContactId = cursor.getLong(0);
3919                updateRawContact(rawContactId, values, callerIsSyncAdapter);
3920                count++;
3921            }
3922        } finally {
3923            cursor.close();
3924        }
3925
3926        return count;
3927    }
3928
3929    private int updateRawContact(long rawContactId, ContentValues values,
3930            boolean callerIsSyncAdapter) {
3931        final String selection = RawContacts._ID + " = ?";
3932        mSelectionArgs1[0] = Long.toString(rawContactId);
3933        final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
3934                && values.getAsInteger(RawContacts.DELETED) == 0);
3935        int previousDeleted = 0;
3936        String accountType = null;
3937        String accountName = null;
3938        if (requestUndoDelete) {
3939            Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
3940                    mSelectionArgs1, null, null, null);
3941            try {
3942                if (cursor.moveToFirst()) {
3943                    previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
3944                    accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
3945                    accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
3946                }
3947            } finally {
3948                cursor.close();
3949            }
3950            values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
3951                    ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
3952        }
3953
3954        int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1);
3955        if (count != 0) {
3956            if (values.containsKey(RawContacts.AGGREGATION_MODE)) {
3957                int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE);
3958
3959                // As per ContactsContract documentation, changing aggregation mode
3960                // to DEFAULT should not trigger aggregation
3961                if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
3962                    mContactAggregator.markForAggregation(rawContactId, aggregationMode, false);
3963                }
3964            }
3965            if (values.containsKey(RawContacts.STARRED)) {
3966                if (!callerIsSyncAdapter) {
3967                    updateFavoritesMembership(rawContactId,
3968                            values.getAsLong(RawContacts.STARRED) != 0);
3969                }
3970                mContactAggregator.updateStarred(rawContactId);
3971            } else {
3972                // if this raw contact is being associated with an account, then update the
3973                // favorites group membership based on whether or not this contact is starred.
3974                // If it is starred, add a group membership, if one doesn't already exist
3975                // otherwise delete any matching group memberships.
3976                if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
3977                    boolean starred = 0 != DatabaseUtils.longForQuery(mDb,
3978                            SELECTION_STARRED_FROM_RAW_CONTACTS,
3979                            new String[]{Long.toString(rawContactId)});
3980                    updateFavoritesMembership(rawContactId, starred);
3981                }
3982            }
3983
3984            // if this raw contact is being associated with an account, then add a
3985            // group membership to the group marked as AutoAdd, if any.
3986            if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
3987                addAutoAddMembership(rawContactId);
3988            }
3989
3990            if (values.containsKey(RawContacts.SOURCE_ID)) {
3991                mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId);
3992            }
3993            if (values.containsKey(RawContacts.NAME_VERIFIED)) {
3994
3995                // If setting NAME_VERIFIED for this raw contact, reset it for all
3996                // other raw contacts in the same aggregate
3997                if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) {
3998                    mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId);
3999                    mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId);
4000                    mResetNameVerifiedForOtherRawContacts.execute();
4001                }
4002                mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId);
4003            }
4004            if (requestUndoDelete && previousDeleted == 1) {
4005                // undo delete, needs aggregation again.
4006                mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType));
4007            }
4008        }
4009        return count;
4010    }
4011
4012    private int updateData(Uri uri, ContentValues values, String selection,
4013            String[] selectionArgs, boolean callerIsSyncAdapter) {
4014        mValues.clear();
4015        mValues.putAll(values);
4016        mValues.remove(Data._ID);
4017        mValues.remove(Data.RAW_CONTACT_ID);
4018        mValues.remove(Data.MIMETYPE);
4019
4020        String packageName = values.getAsString(Data.RES_PACKAGE);
4021        if (packageName != null) {
4022            mValues.remove(Data.RES_PACKAGE);
4023            mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
4024        }
4025
4026        boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
4027        boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
4028
4029        // Remove primary or super primary values being set to 0. This is disallowed by the
4030        // content provider.
4031        if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
4032            containsIsSuperPrimary = false;
4033            mValues.remove(Data.IS_SUPER_PRIMARY);
4034        }
4035        if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
4036            containsIsPrimary = false;
4037            mValues.remove(Data.IS_PRIMARY);
4038        }
4039
4040        if (!callerIsSyncAdapter) {
4041            selection = DatabaseUtils.concatenateWhere(selection,
4042                    Data.IS_READ_ONLY + "=0");
4043        }
4044
4045        int count = 0;
4046
4047        // Note that the query will return data according to the access restrictions,
4048        // so we don't need to worry about updating data we don't have permission to read.
4049        Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null);
4050        try {
4051            while(c.moveToNext()) {
4052                count += updateData(mValues, c, callerIsSyncAdapter);
4053            }
4054        } finally {
4055            c.close();
4056        }
4057
4058        return count;
4059    }
4060
4061    private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
4062        if (values.size() == 0) {
4063            return 0;
4064        }
4065
4066        final String mimeType = c.getString(DataUpdateQuery.MIMETYPE);
4067        DataRowHandler rowHandler = getDataRowHandler(mimeType);
4068        if (rowHandler.update(mDb, values, c, callerIsSyncAdapter)) {
4069            return 1;
4070        } else {
4071            return 0;
4072        }
4073    }
4074
4075    private int updateContactOptions(ContentValues values, String selection,
4076            String[] selectionArgs, boolean callerIsSyncAdapter) {
4077        int count = 0;
4078        Cursor cursor = mDb.query(mDbHelper.getContactView(),
4079                new String[] { Contacts._ID }, selection,
4080                selectionArgs, null, null, null);
4081        try {
4082            while (cursor.moveToNext()) {
4083                long contactId = cursor.getLong(0);
4084                updateContactOptions(contactId, values, callerIsSyncAdapter);
4085                count++;
4086            }
4087        } finally {
4088            cursor.close();
4089        }
4090
4091        return count;
4092    }
4093
4094    private int updateContactOptions(long contactId, ContentValues values,
4095            boolean callerIsSyncAdapter) {
4096
4097        mValues.clear();
4098        ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
4099                values, Contacts.CUSTOM_RINGTONE);
4100        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
4101                values, Contacts.SEND_TO_VOICEMAIL);
4102        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
4103                values, Contacts.LAST_TIME_CONTACTED);
4104        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
4105                values, Contacts.TIMES_CONTACTED);
4106        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
4107                values, Contacts.STARRED);
4108
4109        // Nothing to update - just return
4110        if (mValues.size() == 0) {
4111            return 0;
4112        }
4113
4114        if (mValues.containsKey(RawContacts.STARRED)) {
4115            // Mark dirty when changing starred to trigger sync
4116            mValues.put(RawContacts.DIRTY, 1);
4117        }
4118
4119        mSelectionArgs1[0] = String.valueOf(contactId);
4120        mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?"
4121                + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1);
4122
4123        if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) {
4124            Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
4125                    new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?",
4126                    mSelectionArgs1, null, null, null);
4127            try {
4128                while (cursor.moveToNext()) {
4129                    long rawContactId = cursor.getLong(0);
4130                    updateFavoritesMembership(rawContactId,
4131                            mValues.getAsLong(RawContacts.STARRED) != 0);
4132                }
4133            } finally {
4134                cursor.close();
4135            }
4136        }
4137
4138        // Copy changeable values to prevent automatically managed fields from
4139        // being explicitly updated by clients.
4140        mValues.clear();
4141        ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
4142                values, Contacts.CUSTOM_RINGTONE);
4143        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
4144                values, Contacts.SEND_TO_VOICEMAIL);
4145        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
4146                values, Contacts.LAST_TIME_CONTACTED);
4147        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
4148                values, Contacts.TIMES_CONTACTED);
4149        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
4150                values, Contacts.STARRED);
4151
4152        int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1);
4153
4154        if (values.containsKey(Contacts.LAST_TIME_CONTACTED) &&
4155                !values.containsKey(Contacts.TIMES_CONTACTED)) {
4156            mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
4157            mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
4158        }
4159        return rslt;
4160    }
4161
4162    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
4163        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
4164        long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
4165        long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
4166
4167        long rawContactId1, rawContactId2;
4168        if (rcId1 < rcId2) {
4169            rawContactId1 = rcId1;
4170            rawContactId2 = rcId2;
4171        } else {
4172            rawContactId2 = rcId1;
4173            rawContactId1 = rcId2;
4174        }
4175
4176        if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
4177            mSelectionArgs2[0] = String.valueOf(rawContactId1);
4178            mSelectionArgs2[1] = String.valueOf(rawContactId2);
4179            db.delete(Tables.AGGREGATION_EXCEPTIONS,
4180                    AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
4181                    + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
4182        } else {
4183            ContentValues exceptionValues = new ContentValues(3);
4184            exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
4185            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
4186            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
4187            db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
4188                    exceptionValues);
4189        }
4190
4191        mContactAggregator.invalidateAggregationExceptionCache();
4192        mContactAggregator.markForAggregation(rawContactId1,
4193                RawContacts.AGGREGATION_MODE_DEFAULT, true);
4194        mContactAggregator.markForAggregation(rawContactId2,
4195                RawContacts.AGGREGATION_MODE_DEFAULT, true);
4196
4197        mContactAggregator.aggregateContact(db, rawContactId1);
4198        mContactAggregator.aggregateContact(db, rawContactId2);
4199
4200        // The return value is fake - we just confirm that we made a change, not count actual
4201        // rows changed.
4202        return 1;
4203    }
4204
4205    public void onAccountsUpdated(Account[] accounts) {
4206        boolean accountsChanged = updateAccounts(accounts);
4207        if (accountsChanged) {
4208            mContactDirectoryManager.scheduleScanAllPackages(true);
4209        }
4210    }
4211
4212    protected void updateAccounts() {
4213        AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
4214        Account[] accounts = AccountManager.get(getContext()).getAccounts();
4215        updateAccounts(accounts);
4216        updateContactsAccountCount(accounts);
4217    }
4218
4219    private boolean updateAccounts(Account[] accounts) {
4220        // TODO : Check the unit test.
4221        boolean accountsChanged = false;
4222        HashSet<Account> existingAccounts = new HashSet<Account>();
4223        mDb.beginTransaction();
4224        try {
4225            findValidAccounts(existingAccounts);
4226
4227            // Add a row to the ACCOUNTS table for each new account
4228            for (Account account : accounts) {
4229                if (!existingAccounts.contains(account)) {
4230                    accountsChanged = true;
4231                    mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
4232                            + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)",
4233                            new String[] {account.name, account.type});
4234                }
4235            }
4236
4237            // Remove all valid accounts from the existing account set. What is left
4238            // in the accountsToDelete set will be extra accounts whose data must be deleted.
4239            HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts);
4240            for (Account account : accounts) {
4241                accountsToDelete.remove(account);
4242            }
4243
4244            if (!accountsToDelete.isEmpty()) {
4245                accountsChanged = true;
4246                for (Account account : accountsToDelete) {
4247                    Log.d(TAG, "removing data for removed account " + account);
4248                    String[] params = new String[] {account.name, account.type};
4249                    mDb.execSQL(
4250                            "DELETE FROM " + Tables.GROUPS +
4251                            " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
4252                                    " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
4253                    mDb.execSQL(
4254                            "DELETE FROM " + Tables.PRESENCE +
4255                            " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
4256                                    "SELECT " + RawContacts._ID +
4257                                    " FROM " + Tables.RAW_CONTACTS +
4258                                    " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
4259                                    " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params);
4260                    mDb.execSQL(
4261                            "DELETE FROM " + Tables.RAW_CONTACTS +
4262                            " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
4263                            " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
4264                    mDb.execSQL(
4265                            "DELETE FROM " + Tables.SETTINGS +
4266                            " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
4267                            " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
4268                    mDb.execSQL(
4269                            "DELETE FROM " + Tables.ACCOUNTS +
4270                            " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
4271                            " AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
4272                    mDb.execSQL(
4273                            "DELETE FROM " + Tables.DIRECTORIES +
4274                            " WHERE " + Directory.ACCOUNT_NAME + "=?" +
4275                            " AND " + Directory.ACCOUNT_TYPE + "=?", params);
4276                    resetDirectoryCache();
4277                }
4278
4279                // Find all aggregated contacts that used to contain the raw contacts
4280                // we have just deleted and see if they are still referencing the deleted
4281                // names or photos.  If so, fix up those contacts.
4282                HashSet<Long> orphanContactIds = Sets.newHashSet();
4283                Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID +
4284                        " FROM " + Tables.CONTACTS +
4285                        " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
4286                                Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
4287                                        "(SELECT " + RawContacts._ID +
4288                                        " FROM " + Tables.RAW_CONTACTS + "))" +
4289                        " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
4290                                Contacts.PHOTO_ID + " NOT IN " +
4291                                        "(SELECT " + Data._ID +
4292                                        " FROM " + Tables.DATA + "))", null);
4293                try {
4294                    while (cursor.moveToNext()) {
4295                        orphanContactIds.add(cursor.getLong(0));
4296                    }
4297                } finally {
4298                    cursor.close();
4299                }
4300
4301                for (Long contactId : orphanContactIds) {
4302                    mContactAggregator.updateAggregateData(contactId);
4303                }
4304                mDbHelper.updateAllVisible();
4305            }
4306
4307            if (accountsChanged) {
4308                mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
4309            }
4310            mDb.setTransactionSuccessful();
4311        } finally {
4312            mDb.endTransaction();
4313        }
4314        mAccountWritability.clear();
4315
4316        if (accountsChanged) {
4317            updateContactsAccountCount(accounts);
4318            updateProviderStatus();
4319        }
4320
4321        return accountsChanged;
4322    }
4323
4324    private void updateContactsAccountCount(Account[] accounts) {
4325        int count = 0;
4326        for (Account account : accounts) {
4327            if (isContactsAccount(account)) {
4328                count++;
4329            }
4330        }
4331        mContactsAccountCount = count;
4332    }
4333
4334    protected boolean isContactsAccount(Account account) {
4335        final IContentService cs = ContentResolver.getContentService();
4336        try {
4337            return cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
4338        } catch (RemoteException e) {
4339            Log.e(TAG, "Cannot obtain sync flag for account: " + account, e);
4340            return false;
4341        }
4342    }
4343
4344    public void onPackageChanged(String packageName) {
4345        mContactDirectoryManager.onPackageChanged(packageName);
4346    }
4347
4348    /**
4349     * Finds all distinct accounts present in the specified table.
4350     */
4351    private void findValidAccounts(Set<Account> validAccounts) {
4352        Cursor c = mDb.rawQuery(
4353                "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE +
4354                " FROM " + Tables.ACCOUNTS, null);
4355        try {
4356            while (c.moveToNext()) {
4357                if (!c.isNull(0) || !c.isNull(1)) {
4358                    validAccounts.add(new Account(c.getString(0), c.getString(1)));
4359                }
4360            }
4361        } finally {
4362            c.close();
4363        }
4364    }
4365
4366    /**
4367     * Test all against {@link TextUtils#isEmpty(CharSequence)}.
4368     */
4369    private static boolean areAllEmpty(ContentValues values, String[] keys) {
4370        for (String key : keys) {
4371            if (!TextUtils.isEmpty(values.getAsString(key))) {
4372                return false;
4373            }
4374        }
4375        return true;
4376    }
4377
4378    /**
4379     * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
4380     */
4381    private static boolean areAnySpecified(ContentValues values, String[] keys) {
4382        for (String key : keys) {
4383            if (values.containsKey(key)) {
4384                return true;
4385            }
4386        }
4387        return false;
4388    }
4389
4390    @Override
4391    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
4392            String sortOrder) {
4393        String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY);
4394        if (directory == null) {
4395            return queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1);
4396        } else if (directory.equals("0")) {
4397            return queryLocal(uri, projection, selection, selectionArgs, sortOrder,
4398                    Directory.DEFAULT);
4399        } else if (directory.equals("1")) {
4400            return queryLocal(uri, projection, selection, selectionArgs, sortOrder,
4401                    Directory.LOCAL_INVISIBLE);
4402        }
4403
4404        DirectoryInfo directoryInfo = getDirectoryAuthority(directory);
4405        if (directoryInfo == null) {
4406            Log.e(TAG, "Invalid directory ID: " + uri);
4407            return null;
4408        }
4409
4410        Builder builder = new Uri.Builder();
4411        builder.scheme(ContentResolver.SCHEME_CONTENT);
4412        builder.authority(directoryInfo.authority);
4413        builder.encodedPath(uri.getEncodedPath());
4414        if (directoryInfo.accountName != null) {
4415            builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName);
4416        }
4417        if (directoryInfo.accountType != null) {
4418            builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType);
4419        }
4420
4421        String limit = getLimit(uri);
4422        if (limit != null) {
4423            builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, limit);
4424        }
4425
4426        Uri directoryUri = builder.build();
4427
4428        if (projection == null) {
4429            projection = getDefaultProjection(uri);
4430        }
4431
4432        Cursor cursor = getContext().getContentResolver().query(directoryUri, projection, selection,
4433                selectionArgs, sortOrder);
4434        while (cursor instanceof CursorWrapper) {
4435            cursor = ((CursorWrapper)cursor).getWrappedCursor();
4436        }
4437        return cursor;
4438    }
4439
4440    private static final class DirectoryQuery {
4441        public static final String[] COLUMNS = new String[] {
4442                Directory._ID,
4443                Directory.DIRECTORY_AUTHORITY,
4444                Directory.ACCOUNT_NAME,
4445                Directory.ACCOUNT_TYPE
4446        };
4447
4448        public static final int DIRECTORY_ID = 0;
4449        public static final int AUTHORITY = 1;
4450        public static final int ACCOUNT_NAME = 2;
4451        public static final int ACCOUNT_TYPE = 3;
4452    }
4453
4454    /**
4455     * Reads and caches directory information for the database.
4456     */
4457    private DirectoryInfo getDirectoryAuthority(String directoryId) {
4458        synchronized (mDirectoryCache) {
4459            if (!mDirectoryCacheValid) {
4460                mDirectoryCache.clear();
4461                Cursor cursor = mDb.query(Tables.DIRECTORIES,
4462                        DirectoryQuery.COLUMNS,
4463                        null, null, null, null, null);
4464                try {
4465                    while (cursor.moveToNext()) {
4466                        DirectoryInfo info = new DirectoryInfo();
4467                        String id = cursor.getString(DirectoryQuery.DIRECTORY_ID);
4468                        info.authority = cursor.getString(DirectoryQuery.AUTHORITY);
4469                        info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
4470                        info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
4471                        mDirectoryCache.put(id, info);
4472                    }
4473                } finally {
4474                    cursor.close();
4475                }
4476                mDirectoryCacheValid = true;
4477            }
4478
4479            return mDirectoryCache.get(directoryId);
4480        }
4481    }
4482
4483    public void resetDirectoryCache() {
4484        synchronized(mDirectoryCache) {
4485            mDirectoryCacheValid = false;
4486        }
4487    }
4488
4489    public Cursor queryLocal(Uri uri, String[] projection, String selection, String[] selectionArgs,
4490                String sortOrder, long directoryId) {
4491        if (VERBOSE_LOGGING) {
4492            Log.v(TAG, "query: " + uri);
4493        }
4494
4495        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
4496
4497        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
4498        String groupBy = null;
4499        String limit = getLimit(uri);
4500
4501        // TODO: Consider writing a test case for RestrictionExceptions when you
4502        // write a new query() block to make sure it protects restricted data.
4503        final int match = sUriMatcher.match(uri);
4504        switch (match) {
4505            case SYNCSTATE:
4506                return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
4507                        sortOrder);
4508
4509            case CONTACTS: {
4510                setTablesAndProjectionMapForContacts(qb, uri, projection);
4511                appendLocalDirectorySelectionIfNeeded(qb, directoryId);
4512                break;
4513            }
4514
4515            case CONTACTS_ID: {
4516                long contactId = ContentUris.parseId(uri);
4517                setTablesAndProjectionMapForContacts(qb, uri, projection);
4518                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4519                qb.appendWhere(Contacts._ID + "=?");
4520                break;
4521            }
4522
4523            case CONTACTS_LOOKUP:
4524            case CONTACTS_LOOKUP_ID: {
4525                List<String> pathSegments = uri.getPathSegments();
4526                int segmentCount = pathSegments.size();
4527                if (segmentCount < 3) {
4528                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
4529                            "Missing a lookup key", uri));
4530                }
4531
4532                String lookupKey = pathSegments.get(2);
4533                if (segmentCount == 4) {
4534                    long contactId = Long.parseLong(pathSegments.get(3));
4535                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
4536                    setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
4537
4538                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
4539                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
4540                            Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey);
4541                    if (c != null) {
4542                        return c;
4543                    }
4544                }
4545
4546                setTablesAndProjectionMapForContacts(qb, uri, projection);
4547                selectionArgs = insertSelectionArg(selectionArgs,
4548                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
4549                qb.appendWhere(Contacts._ID + "=?");
4550                break;
4551            }
4552
4553            case CONTACTS_LOOKUP_DATA:
4554            case CONTACTS_LOOKUP_ID_DATA: {
4555                List<String> pathSegments = uri.getPathSegments();
4556                int segmentCount = pathSegments.size();
4557                if (segmentCount < 4) {
4558                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
4559                            "Missing a lookup key", uri));
4560                }
4561                String lookupKey = pathSegments.get(2);
4562                if (segmentCount == 5) {
4563                    long contactId = Long.parseLong(pathSegments.get(3));
4564                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
4565                    setTablesAndProjectionMapForData(lookupQb, uri, projection, false);
4566                    lookupQb.appendWhere(" AND ");
4567                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
4568                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
4569                            Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey);
4570                    if (c != null) {
4571                        return c;
4572                    }
4573
4574                    // TODO see if the contact exists but has no data rows (rare)
4575                }
4576
4577                setTablesAndProjectionMapForData(qb, uri, projection, false);
4578                selectionArgs = insertSelectionArg(selectionArgs,
4579                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
4580                qb.appendWhere(" AND " + Data.CONTACT_ID + "=?");
4581                break;
4582            }
4583
4584            case CONTACTS_AS_VCARD: {
4585                // When reading as vCard always use restricted view
4586                final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
4587                qb.setTables(mDbHelper.getContactView(true /* require restricted */));
4588                qb.setProjectionMap(sContactsVCardProjectionMap);
4589                selectionArgs = insertSelectionArg(selectionArgs,
4590                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
4591                qb.appendWhere(Contacts._ID + "=?");
4592                break;
4593            }
4594
4595            case CONTACTS_AS_MULTI_VCARD: {
4596                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
4597                String currentDateString = dateFormat.format(new Date()).toString();
4598                return db.rawQuery(
4599                    "SELECT" +
4600                    " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," +
4601                    " NULL AS " + OpenableColumns.SIZE,
4602                    new String[] { currentDateString });
4603            }
4604
4605            case CONTACTS_FILTER: {
4606                String filterParam = "";
4607                if (uri.getPathSegments().size() > 2) {
4608                    filterParam = uri.getLastPathSegment();
4609                }
4610                setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam);
4611                appendLocalDirectorySelectionIfNeeded(qb, directoryId);
4612                break;
4613            }
4614
4615            case CONTACTS_STREQUENT_FILTER:
4616            case CONTACTS_STREQUENT: {
4617                String filterSql = null;
4618                if (match == CONTACTS_STREQUENT_FILTER
4619                        && uri.getPathSegments().size() > 3) {
4620                    String filterParam = uri.getLastPathSegment();
4621                    StringBuilder sb = new StringBuilder();
4622                    sb.append(Contacts._ID + " IN ");
4623                    appendContactFilterAsNestedQuery(sb, filterParam);
4624                    filterSql = sb.toString();
4625                }
4626
4627                setTablesAndProjectionMapForContacts(qb, uri, projection);
4628
4629                String[] starredProjection = null;
4630                String[] frequentProjection = null;
4631                if (projection != null) {
4632                    starredProjection =
4633                            appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN);
4634                    frequentProjection =
4635                            appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN);
4636                }
4637
4638                // Build the first query for starred
4639                if (filterSql != null) {
4640                    qb.appendWhere(filterSql);
4641                }
4642                qb.setProjectionMap(sStrequentStarredProjectionMap);
4643                final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1",
4644                        null, Contacts._ID, null, null, null);
4645
4646                // Build the second query for frequent
4647                qb = new SQLiteQueryBuilder();
4648                setTablesAndProjectionMapForContacts(qb, uri, projection);
4649                if (filterSql != null) {
4650                    qb.appendWhere(filterSql);
4651                }
4652                qb.setProjectionMap(sStrequentFrequentProjectionMap);
4653                final String frequentQuery = qb.buildQuery(frequentProjection,
4654                        Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
4655                        + " = 0 OR " + Contacts.STARRED + " IS NULL)",
4656                        null, Contacts._ID, null, null, null);
4657
4658                // Put them together
4659                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
4660                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
4661                Cursor c = db.rawQuery(query, null);
4662                if (c != null) {
4663                    c.setNotificationUri(getContext().getContentResolver(),
4664                            ContactsContract.AUTHORITY_URI);
4665                }
4666                return c;
4667            }
4668
4669            case CONTACTS_GROUP: {
4670                setTablesAndProjectionMapForContacts(qb, uri, projection);
4671                if (uri.getPathSegments().size() > 2) {
4672                    qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
4673                    selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4674                }
4675                break;
4676            }
4677
4678            case CONTACTS_ID_DATA: {
4679                long contactId = Long.parseLong(uri.getPathSegments().get(1));
4680                setTablesAndProjectionMapForData(qb, uri, projection, false);
4681                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4682                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
4683                break;
4684            }
4685
4686            case CONTACTS_ID_PHOTO: {
4687                long contactId = Long.parseLong(uri.getPathSegments().get(1));
4688                setTablesAndProjectionMapForData(qb, uri, projection, false);
4689                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4690                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
4691                qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
4692                break;
4693            }
4694
4695            case CONTACTS_ID_ENTITIES: {
4696                long contactId = Long.parseLong(uri.getPathSegments().get(1));
4697                setTablesAndProjectionMapForEntities(qb, uri, projection);
4698                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4699                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
4700                break;
4701            }
4702
4703            case CONTACTS_LOOKUP_ENTITIES:
4704            case CONTACTS_LOOKUP_ID_ENTITIES: {
4705                List<String> pathSegments = uri.getPathSegments();
4706                int segmentCount = pathSegments.size();
4707                if (segmentCount < 4) {
4708                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
4709                            "Missing a lookup key", uri));
4710                }
4711                String lookupKey = pathSegments.get(2);
4712                if (segmentCount == 5) {
4713                    long contactId = Long.parseLong(pathSegments.get(3));
4714                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
4715                    setTablesAndProjectionMapForEntities(lookupQb, uri, projection);
4716                    lookupQb.appendWhere(" AND ");
4717
4718                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
4719                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
4720                            Contacts.Entity.CONTACT_ID, contactId,
4721                            Contacts.Entity.LOOKUP_KEY, lookupKey);
4722                    if (c != null) {
4723                        return c;
4724                    }
4725                }
4726
4727                setTablesAndProjectionMapForEntities(qb, uri, projection);
4728                selectionArgs = insertSelectionArg(selectionArgs,
4729                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
4730                qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?");
4731                break;
4732            }
4733
4734            case PHONES: {
4735                setTablesAndProjectionMapForData(qb, uri, projection, false);
4736                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
4737                break;
4738            }
4739
4740            case PHONES_ID: {
4741                setTablesAndProjectionMapForData(qb, uri, projection, false);
4742                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4743                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
4744                qb.appendWhere(" AND " + Data._ID + "=?");
4745                break;
4746            }
4747
4748            case PHONES_FILTER: {
4749                setTablesAndProjectionMapForData(qb, uri, projection, true);
4750                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
4751                if (uri.getPathSegments().size() > 2) {
4752                    String filterParam = uri.getLastPathSegment();
4753                    StringBuilder sb = new StringBuilder();
4754                    sb.append(" AND (");
4755
4756                    boolean hasCondition = false;
4757                    boolean orNeeded = false;
4758                    String normalizedName = NameNormalizer.normalize(filterParam);
4759                    if (normalizedName.length() > 0) {
4760                        sb.append(Data.RAW_CONTACT_ID + " IN ");
4761                        appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
4762                        orNeeded = true;
4763                        hasCondition = true;
4764                    }
4765
4766                    String number = PhoneNumberUtils.normalizeNumber(filterParam);
4767                    if (!TextUtils.isEmpty(number)) {
4768                        if (orNeeded) {
4769                            sb.append(" OR ");
4770                        }
4771                        sb.append(Data._ID +
4772                                " IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID
4773                                + " FROM " + Tables.PHONE_LOOKUP
4774                                + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
4775                        sb.append(number);
4776                        sb.append("%')");
4777                        hasCondition = true;
4778                    }
4779
4780                    if (!hasCondition) {
4781                        // If it is neither a phone number nor a name, the query should return
4782                        // an empty cursor.  Let's ensure that.
4783                        sb.append("0");
4784                    }
4785                    sb.append(")");
4786                    qb.appendWhere(sb);
4787                }
4788                groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
4789                if (sortOrder == null) {
4790                    sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
4791                }
4792                break;
4793            }
4794
4795            case EMAILS: {
4796                setTablesAndProjectionMapForData(qb, uri, projection, false);
4797                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
4798                break;
4799            }
4800
4801            case EMAILS_ID: {
4802                setTablesAndProjectionMapForData(qb, uri, projection, false);
4803                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4804                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"
4805                        + " AND " + Data._ID + "=?");
4806                break;
4807            }
4808
4809            case EMAILS_LOOKUP: {
4810                setTablesAndProjectionMapForData(qb, uri, projection, false);
4811                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
4812                if (uri.getPathSegments().size() > 2) {
4813                    String email = uri.getLastPathSegment();
4814                    String address = mDbHelper.extractAddressFromEmailAddress(email);
4815                    selectionArgs = insertSelectionArg(selectionArgs, address);
4816                    qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)");
4817                }
4818                break;
4819            }
4820
4821            case EMAILS_FILTER: {
4822                setTablesAndProjectionMapForData(qb, uri, projection, true);
4823                String filterParam = null;
4824                if (uri.getPathSegments().size() > 3) {
4825                    filterParam = uri.getLastPathSegment();
4826                    if (TextUtils.isEmpty(filterParam)) {
4827                        filterParam = null;
4828                    }
4829                }
4830
4831                if (filterParam == null) {
4832                    // If the filter is unspecified, return nothing
4833                    qb.appendWhere(" AND 0");
4834                } else {
4835                    StringBuilder sb = new StringBuilder();
4836                    sb.append(" AND " + Data._ID + " IN (");
4837                    sb.append(
4838                            "SELECT " + Data._ID +
4839                            " FROM " + Tables.DATA +
4840                            " WHERE " + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
4841                            " AND " + Data.DATA1 + " LIKE ");
4842                    DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
4843                    if (!filterParam.contains("@")) {
4844                        String normalizedName = NameNormalizer.normalize(filterParam);
4845                        if (normalizedName.length() > 0) {
4846
4847                            /*
4848                             * Using a UNION instead of an "OR" to make SQLite use the right
4849                             * indexes. We need it to use the (mimetype,data1) index for the
4850                             * email lookup (see above), but not for the name lookup.
4851                             * SQLite is not smart enough to use the index on one side of an OR
4852                             * but not on the other. Using two separate nested queries
4853                             * and a UNION between them does the job.
4854                             */
4855                            sb.append(
4856                                    " UNION SELECT " + Data._ID +
4857                                    " FROM " + Tables.DATA +
4858                                    " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
4859                                    " AND " + Data.RAW_CONTACT_ID + " IN ");
4860                            appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
4861                        }
4862                    }
4863                    sb.append(")");
4864                    qb.appendWhere(sb);
4865                }
4866                groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
4867                if (sortOrder == null) {
4868                    sortOrder = EMAIL_FILTER_SORT_ORDER;
4869                }
4870                break;
4871            }
4872
4873            case POSTALS: {
4874                setTablesAndProjectionMapForData(qb, uri, projection, false);
4875                qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
4876                        + StructuredPostal.CONTENT_ITEM_TYPE + "'");
4877                break;
4878            }
4879
4880            case POSTALS_ID: {
4881                setTablesAndProjectionMapForData(qb, uri, projection, false);
4882                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4883                qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
4884                        + StructuredPostal.CONTENT_ITEM_TYPE + "'");
4885                qb.appendWhere(" AND " + Data._ID + "=?");
4886                break;
4887            }
4888
4889            case RAW_CONTACTS: {
4890                setTablesAndProjectionMapForRawContacts(qb, uri);
4891                break;
4892            }
4893
4894            case RAW_CONTACTS_ID: {
4895                long rawContactId = ContentUris.parseId(uri);
4896                setTablesAndProjectionMapForRawContacts(qb, uri);
4897                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
4898                qb.appendWhere(" AND " + RawContacts._ID + "=?");
4899                break;
4900            }
4901
4902            case RAW_CONTACTS_DATA: {
4903                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
4904                setTablesAndProjectionMapForData(qb, uri, projection, false);
4905                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
4906                qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
4907                break;
4908            }
4909
4910            case DATA: {
4911                setTablesAndProjectionMapForData(qb, uri, projection, false);
4912                break;
4913            }
4914
4915            case DATA_ID: {
4916                setTablesAndProjectionMapForData(qb, uri, projection, false);
4917                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4918                qb.appendWhere(" AND " + Data._ID + "=?");
4919                break;
4920            }
4921
4922            case PHONE_LOOKUP: {
4923
4924                if (TextUtils.isEmpty(sortOrder)) {
4925                    // Default the sort order to something reasonable so we get consistent
4926                    // results when callers don't request an ordering
4927                    sortOrder = " length(lookup.normalized_number) DESC";
4928                }
4929
4930                String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
4931                String numberE164 =
4932                        PhoneNumberUtils.formatNumberToE164(number, getCurrentCountryIso());
4933                String normalizedNumber =
4934                        PhoneNumberUtils.normalizeNumber(number);
4935                mDbHelper.buildPhoneLookupAndContactQuery(qb, normalizedNumber, numberE164);
4936                qb.setProjectionMap(sPhoneLookupProjectionMap);
4937                // Phone lookup cannot be combined with a selection
4938                selection = null;
4939                selectionArgs = null;
4940                break;
4941            }
4942
4943            case GROUPS: {
4944                qb.setTables(mDbHelper.getGroupView());
4945                qb.setProjectionMap(sGroupsProjectionMap);
4946                appendAccountFromParameter(qb, uri);
4947                break;
4948            }
4949
4950            case GROUPS_ID: {
4951                qb.setTables(mDbHelper.getGroupView());
4952                qb.setProjectionMap(sGroupsProjectionMap);
4953                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4954                qb.appendWhere(Groups._ID + "=?");
4955                break;
4956            }
4957
4958            case GROUPS_SUMMARY: {
4959                qb.setTables(mDbHelper.getGroupView() + " AS groups");
4960                qb.setProjectionMap(sGroupsSummaryProjectionMap);
4961                appendAccountFromParameter(qb, uri);
4962                groupBy = Groups._ID;
4963                break;
4964            }
4965
4966            case AGGREGATION_EXCEPTIONS: {
4967                qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
4968                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
4969                break;
4970            }
4971
4972            case AGGREGATION_SUGGESTIONS: {
4973                long contactId = Long.parseLong(uri.getPathSegments().get(1));
4974                String filter = null;
4975                if (uri.getPathSegments().size() > 3) {
4976                    filter = uri.getPathSegments().get(3);
4977                }
4978                final int maxSuggestions;
4979                if (limit != null) {
4980                    maxSuggestions = Integer.parseInt(limit);
4981                } else {
4982                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
4983                }
4984
4985                ArrayList<AggregationSuggestionParameter> parameters = null;
4986                List<String> query = uri.getQueryParameters("query");
4987                if (query != null && !query.isEmpty()) {
4988                    parameters = new ArrayList<AggregationSuggestionParameter>(query.size());
4989                    for (String parameter : query) {
4990                        int offset = parameter.indexOf(':');
4991                        parameters.add(offset == -1
4992                                ? new AggregationSuggestionParameter(
4993                                        AggregationSuggestions.PARAMETER_MATCH_NAME,
4994                                        parameter)
4995                                : new AggregationSuggestionParameter(
4996                                        parameter.substring(0, offset),
4997                                        parameter.substring(offset + 1)));
4998                    }
4999                }
5000
5001                setTablesAndProjectionMapForContacts(qb, uri, projection);
5002
5003                return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId,
5004                        maxSuggestions, filter, parameters);
5005            }
5006
5007            case SETTINGS: {
5008                qb.setTables(Tables.SETTINGS);
5009                qb.setProjectionMap(sSettingsProjectionMap);
5010                appendAccountFromParameter(qb, uri);
5011
5012                // When requesting specific columns, this query requires
5013                // late-binding of the GroupMembership MIME-type.
5014                final String groupMembershipMimetypeId = Long.toString(mDbHelper
5015                        .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
5016                if (projection != null && projection.length != 0 &&
5017                        mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) {
5018                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
5019                }
5020                if (projection != null && projection.length != 0 &&
5021                        mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) {
5022                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
5023                }
5024
5025                break;
5026            }
5027
5028            case STATUS_UPDATES: {
5029                setTableAndProjectionMapForStatusUpdates(qb, projection);
5030                break;
5031            }
5032
5033            case STATUS_UPDATES_ID: {
5034                setTableAndProjectionMapForStatusUpdates(qb, projection);
5035                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
5036                qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
5037                break;
5038            }
5039
5040            case SEARCH_SUGGESTIONS: {
5041                return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
5042            }
5043
5044            case SEARCH_SHORTCUT: {
5045                String lookupKey = uri.getLastPathSegment();
5046                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection);
5047            }
5048
5049            case LIVE_FOLDERS_CONTACTS:
5050                qb.setTables(mDbHelper.getContactView());
5051                qb.setProjectionMap(sLiveFoldersProjectionMap);
5052                break;
5053
5054            case LIVE_FOLDERS_CONTACTS_WITH_PHONES:
5055                qb.setTables(mDbHelper.getContactView());
5056                qb.setProjectionMap(sLiveFoldersProjectionMap);
5057                qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1");
5058                break;
5059
5060            case LIVE_FOLDERS_CONTACTS_FAVORITES:
5061                qb.setTables(mDbHelper.getContactView());
5062                qb.setProjectionMap(sLiveFoldersProjectionMap);
5063                qb.appendWhere(Contacts.STARRED + "=1");
5064                break;
5065
5066            case LIVE_FOLDERS_CONTACTS_GROUP_NAME:
5067                qb.setTables(mDbHelper.getContactView());
5068                qb.setProjectionMap(sLiveFoldersProjectionMap);
5069                qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
5070                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
5071                break;
5072
5073            case RAW_CONTACT_ENTITIES: {
5074                setTablesAndProjectionMapForRawEntities(qb, uri);
5075                break;
5076            }
5077
5078            case RAW_CONTACT_ENTITY_ID: {
5079                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
5080                setTablesAndProjectionMapForRawEntities(qb, uri);
5081                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
5082                qb.appendWhere(" AND " + RawContacts._ID + "=?");
5083                break;
5084            }
5085
5086            case PROVIDER_STATUS: {
5087                return queryProviderStatus(uri, projection);
5088            }
5089
5090            case DIRECTORIES : {
5091                qb.setTables(Tables.DIRECTORIES);
5092                qb.setProjectionMap(sDirectoryProjectionMap);
5093                break;
5094            }
5095
5096            case DIRECTORIES_ID : {
5097                long id = ContentUris.parseId(uri);
5098                qb.setTables(Tables.DIRECTORIES);
5099                qb.setProjectionMap(sDirectoryProjectionMap);
5100                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id));
5101                qb.appendWhere(Directory._ID + "=?");
5102                break;
5103            }
5104
5105            case COMPLETE_NAME: {
5106                return completeName(uri, projection);
5107            }
5108
5109            default:
5110                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
5111                        sortOrder, limit);
5112        }
5113
5114        qb.setStrictProjectionMap(true);
5115
5116        Cursor cursor =
5117                query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
5118        if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
5119            cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder);
5120        }
5121        return cursor;
5122    }
5123
5124    private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
5125            String selection, String[] selectionArgs, String sortOrder, String groupBy,
5126            String limit) {
5127        if (projection != null && projection.length == 1
5128                && BaseColumns._COUNT.equals(projection[0])) {
5129            qb.setProjectionMap(sCountProjectionMap);
5130        }
5131        final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
5132                sortOrder, limit);
5133        if (c != null) {
5134            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
5135        }
5136        return c;
5137    }
5138
5139    /**
5140     * Creates a single-row cursor containing the current status of the provider.
5141     */
5142    private Cursor queryProviderStatus(Uri uri, String[] projection) {
5143        MatrixCursor cursor = new MatrixCursor(projection);
5144        RowBuilder row = cursor.newRow();
5145        for (int i = 0; i < projection.length; i++) {
5146            if (ProviderStatus.STATUS.equals(projection[i])) {
5147                row.add(mProviderStatus);
5148            } else if (ProviderStatus.DATA1.equals(projection[i])) {
5149                row.add(mEstimatedStorageRequirement);
5150            }
5151        }
5152        return cursor;
5153    }
5154
5155    /**
5156     * Runs the query with the supplied contact ID and lookup ID.  If the query succeeds,
5157     * it returns the resulting cursor, otherwise it returns null and the calling
5158     * method needs to resolve the lookup key and rerun the query.
5159     */
5160    private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb,
5161            SQLiteDatabase db, Uri uri,
5162            String[] projection, String selection, String[] selectionArgs,
5163            String sortOrder, String groupBy, String limit,
5164            String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey) {
5165        String[] args;
5166        if (selectionArgs == null) {
5167            args = new String[2];
5168        } else {
5169            args = new String[selectionArgs.length + 2];
5170            System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
5171        }
5172        args[0] = String.valueOf(contactId);
5173        args[1] = Uri.encode(lookupKey);
5174        lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?");
5175        Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
5176                groupBy, limit);
5177        if (c.getCount() != 0) {
5178            return c;
5179        }
5180
5181        c.close();
5182        return null;
5183    }
5184
5185    private static final class AddressBookIndexQuery {
5186        public static final String LETTER = "letter";
5187        public static final String TITLE = "title";
5188        public static final String COUNT = "count";
5189
5190        public static final String[] COLUMNS = new String[] {
5191                LETTER, TITLE, COUNT
5192        };
5193
5194        public static final int COLUMN_LETTER = 0;
5195        public static final int COLUMN_TITLE = 1;
5196        public static final int COLUMN_COUNT = 2;
5197
5198        public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
5199    }
5200
5201    /**
5202     * Computes counts by the address book index titles and adds the resulting tally
5203     * to the returned cursor as a bundle of extras.
5204     */
5205    private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db,
5206            SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) {
5207        String sortKey;
5208
5209        // The sort order suffix could be something like "DESC".
5210        // We want to preserve it in the query even though we will change
5211        // the sort column itself.
5212        String sortOrderSuffix = "";
5213        if (sortOrder != null) {
5214            int spaceIndex = sortOrder.indexOf(' ');
5215            if (spaceIndex != -1) {
5216                sortKey = sortOrder.substring(0, spaceIndex);
5217                sortOrderSuffix = sortOrder.substring(spaceIndex);
5218            } else {
5219                sortKey = sortOrder;
5220            }
5221        } else {
5222            sortKey = Contacts.SORT_KEY_PRIMARY;
5223        }
5224
5225        String locale = getLocale().toString();
5226        HashMap<String, String> projectionMap = Maps.newHashMap();
5227        projectionMap.put(AddressBookIndexQuery.LETTER,
5228                "SUBSTR(" + sortKey + ",1,1) AS " + AddressBookIndexQuery.LETTER);
5229
5230        /**
5231         * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3,
5232         * to map the first letter of the sort key to a character that is traditionally
5233         * used in phonebooks to represent that letter.  For example, in Korean it will
5234         * be the first consonant in the letter; for Japanese it will be Hiragana rather
5235         * than Katakana.
5236         */
5237        projectionMap.put(AddressBookIndexQuery.TITLE,
5238                "GET_PHONEBOOK_INDEX(SUBSTR(" + sortKey + ",1,1),'" + locale + "')"
5239                        + " AS " + AddressBookIndexQuery.TITLE);
5240        projectionMap.put(AddressBookIndexQuery.COUNT,
5241                "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT);
5242        qb.setProjectionMap(projectionMap);
5243
5244        Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
5245                AddressBookIndexQuery.ORDER_BY, null /* having */,
5246                AddressBookIndexQuery.ORDER_BY + sortOrderSuffix);
5247
5248        try {
5249            int groupCount = indexCursor.getCount();
5250            String titles[] = new String[groupCount];
5251            int counts[] = new int[groupCount];
5252            int indexCount = 0;
5253            String currentTitle = null;
5254
5255            // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up
5256            // with multiple entries for the same title.  The following code
5257            // collapses those duplicates.
5258            for (int i = 0; i < groupCount; i++) {
5259                indexCursor.moveToNext();
5260                String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
5261                int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
5262                if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
5263                    titles[indexCount] = currentTitle = title;
5264                    counts[indexCount] = count;
5265                    indexCount++;
5266                } else {
5267                    counts[indexCount - 1] += count;
5268                }
5269            }
5270
5271            if (indexCount < groupCount) {
5272                String[] newTitles = new String[indexCount];
5273                System.arraycopy(titles, 0, newTitles, 0, indexCount);
5274                titles = newTitles;
5275
5276                int[] newCounts = new int[indexCount];
5277                System.arraycopy(counts, 0, newCounts, 0, indexCount);
5278                counts = newCounts;
5279            }
5280
5281            final Bundle bundle = new Bundle();
5282            bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles);
5283            bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts);
5284            return new CursorWrapper(cursor) {
5285
5286                @Override
5287                public Bundle getExtras() {
5288                    return bundle;
5289                }
5290            };
5291        } finally {
5292            indexCursor.close();
5293        }
5294    }
5295
5296    /**
5297     * Returns the contact Id for the contact identified by the lookupKey.
5298     * Robust against changes in the lookup key: if the key has changed, will
5299     * look up the contact by the raw contact IDs or name encoded in the lookup
5300     * key.
5301     */
5302    public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
5303        ContactLookupKey key = new ContactLookupKey();
5304        ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
5305
5306        long contactId = -1;
5307        if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) {
5308            contactId = lookupContactIdBySourceIds(db, segments);
5309            if (contactId != -1) {
5310                return contactId;
5311            }
5312        }
5313
5314        boolean hasRawContactIds =
5315                lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID);
5316        if (hasRawContactIds) {
5317            contactId = lookupContactIdByRawContactIds(db, segments);
5318            if (contactId != -1) {
5319                return contactId;
5320            }
5321        }
5322
5323        if (hasRawContactIds
5324                || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) {
5325            contactId = lookupContactIdByDisplayNames(db, segments);
5326        }
5327
5328        return contactId;
5329    }
5330
5331    private interface LookupBySourceIdQuery {
5332        String TABLE = Tables.RAW_CONTACTS;
5333
5334        String COLUMNS[] = {
5335                RawContacts.CONTACT_ID,
5336                RawContacts.ACCOUNT_TYPE,
5337                RawContacts.ACCOUNT_NAME,
5338                RawContacts.SOURCE_ID
5339        };
5340
5341        int CONTACT_ID = 0;
5342        int ACCOUNT_TYPE = 1;
5343        int ACCOUNT_NAME = 2;
5344        int SOURCE_ID = 3;
5345    }
5346
5347    private long lookupContactIdBySourceIds(SQLiteDatabase db,
5348                ArrayList<LookupKeySegment> segments) {
5349        StringBuilder sb = new StringBuilder();
5350        sb.append(RawContacts.SOURCE_ID + " IN (");
5351        for (int i = 0; i < segments.size(); i++) {
5352            LookupKeySegment segment = segments.get(i);
5353            if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) {
5354                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
5355                sb.append(",");
5356            }
5357        }
5358        sb.setLength(sb.length() - 1);      // Last comma
5359        sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
5360
5361        Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
5362                 sb.toString(), null, null, null, null);
5363        try {
5364            while (c.moveToNext()) {
5365                String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE);
5366                String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
5367                int accountHashCode =
5368                        ContactLookupKey.getAccountHashCode(accountType, accountName);
5369                String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
5370                for (int i = 0; i < segments.size(); i++) {
5371                    LookupKeySegment segment = segments.get(i);
5372                    if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID
5373                            && accountHashCode == segment.accountHashCode
5374                            && segment.key.equals(sourceId)) {
5375                        segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
5376                        break;
5377                    }
5378                }
5379            }
5380        } finally {
5381            c.close();
5382        }
5383
5384        return getMostReferencedContactId(segments);
5385    }
5386
5387    private interface LookupByRawContactIdQuery {
5388        String TABLE = Tables.RAW_CONTACTS;
5389
5390        String COLUMNS[] = {
5391                RawContacts.CONTACT_ID,
5392                RawContacts.ACCOUNT_TYPE,
5393                RawContacts.ACCOUNT_NAME,
5394                RawContacts._ID,
5395        };
5396
5397        int CONTACT_ID = 0;
5398        int ACCOUNT_TYPE = 1;
5399        int ACCOUNT_NAME = 2;
5400        int ID = 3;
5401    }
5402
5403    private long lookupContactIdByRawContactIds(SQLiteDatabase db,
5404            ArrayList<LookupKeySegment> segments) {
5405        StringBuilder sb = new StringBuilder();
5406        sb.append(RawContacts._ID + " IN (");
5407        for (int i = 0; i < segments.size(); i++) {
5408            LookupKeySegment segment = segments.get(i);
5409            if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
5410                sb.append(segment.rawContactId);
5411                sb.append(",");
5412            }
5413        }
5414        sb.setLength(sb.length() - 1);      // Last comma
5415        sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
5416
5417        Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS,
5418                 sb.toString(), null, null, null, null);
5419        try {
5420            while (c.moveToNext()) {
5421                String accountType = c.getString(LookupByRawContactIdQuery.ACCOUNT_TYPE);
5422                String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME);
5423                int accountHashCode =
5424                        ContactLookupKey.getAccountHashCode(accountType, accountName);
5425                String rawContactId = c.getString(LookupByRawContactIdQuery.ID);
5426                for (int i = 0; i < segments.size(); i++) {
5427                    LookupKeySegment segment = segments.get(i);
5428                    if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID
5429                            && accountHashCode == segment.accountHashCode
5430                            && segment.rawContactId.equals(rawContactId)) {
5431                        segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID);
5432                        break;
5433                    }
5434                }
5435            }
5436        } finally {
5437            c.close();
5438        }
5439
5440        return getMostReferencedContactId(segments);
5441    }
5442
5443    private interface LookupByDisplayNameQuery {
5444        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
5445
5446        String COLUMNS[] = {
5447                RawContacts.CONTACT_ID,
5448                RawContacts.ACCOUNT_TYPE,
5449                RawContacts.ACCOUNT_NAME,
5450                NameLookupColumns.NORMALIZED_NAME
5451        };
5452
5453        int CONTACT_ID = 0;
5454        int ACCOUNT_TYPE = 1;
5455        int ACCOUNT_NAME = 2;
5456        int NORMALIZED_NAME = 3;
5457    }
5458
5459    private long lookupContactIdByDisplayNames(SQLiteDatabase db,
5460                ArrayList<LookupKeySegment> segments) {
5461        StringBuilder sb = new StringBuilder();
5462        sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
5463        for (int i = 0; i < segments.size(); i++) {
5464            LookupKeySegment segment = segments.get(i);
5465            if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
5466                    || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
5467                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
5468                sb.append(",");
5469            }
5470        }
5471        sb.setLength(sb.length() - 1);      // Last comma
5472        sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
5473                + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
5474
5475        Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
5476                 sb.toString(), null, null, null, null);
5477        try {
5478            while (c.moveToNext()) {
5479                String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE);
5480                String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
5481                int accountHashCode =
5482                        ContactLookupKey.getAccountHashCode(accountType, accountName);
5483                String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
5484                for (int i = 0; i < segments.size(); i++) {
5485                    LookupKeySegment segment = segments.get(i);
5486                    if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
5487                            || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID)
5488                            && accountHashCode == segment.accountHashCode
5489                            && segment.key.equals(name)) {
5490                        segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
5491                        break;
5492                    }
5493                }
5494            }
5495        } finally {
5496            c.close();
5497        }
5498
5499        return getMostReferencedContactId(segments);
5500    }
5501
5502    private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) {
5503        for (int i = 0; i < segments.size(); i++) {
5504            LookupKeySegment segment = segments.get(i);
5505            if (segment.lookupType == lookupType) {
5506                return true;
5507            }
5508        }
5509
5510        return false;
5511    }
5512
5513    public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
5514        mContactAggregator.updateLookupKeyForRawContact(db, rawContactId);
5515    }
5516
5517    /**
5518     * Returns the contact ID that is mentioned the highest number of times.
5519     */
5520    private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
5521        Collections.sort(segments);
5522
5523        long bestContactId = -1;
5524        int bestRefCount = 0;
5525
5526        long contactId = -1;
5527        int count = 0;
5528
5529        int segmentCount = segments.size();
5530        for (int i = 0; i < segmentCount; i++) {
5531            LookupKeySegment segment = segments.get(i);
5532            if (segment.contactId != -1) {
5533                if (segment.contactId == contactId) {
5534                    count++;
5535                } else {
5536                    if (count > bestRefCount) {
5537                        bestContactId = contactId;
5538                        bestRefCount = count;
5539                    }
5540                    contactId = segment.contactId;
5541                    count = 1;
5542                }
5543            }
5544        }
5545        if (count > bestRefCount) {
5546            return contactId;
5547        } else {
5548            return bestContactId;
5549        }
5550    }
5551
5552    private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
5553            String[] projection) {
5554        StringBuilder sb = new StringBuilder();
5555        appendContactsTables(sb, uri, projection);
5556        qb.setTables(sb.toString());
5557        qb.setProjectionMap(sContactsProjectionMap);
5558    }
5559
5560    /**
5561     * Finds name lookup records matching the supplied filter, picks one arbitrary match per
5562     * contact and joins that with other contacts tables.
5563     */
5564    private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri,
5565            String[] projection, String filter) {
5566
5567        StringBuilder sb = new StringBuilder();
5568        appendContactsTables(sb, uri, projection);
5569
5570        sb.append(" JOIN (SELECT " +
5571                RawContacts.CONTACT_ID + " AS snippet_contact_id");
5572
5573        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA_ID)) {
5574            sb.append(", " + DataColumns.CONCRETE_ID + " AS "
5575                    + SearchSnippetColumns.SNIPPET_DATA_ID);
5576        }
5577
5578        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA1)) {
5579            sb.append(", " + Data.DATA1 + " AS " + SearchSnippetColumns.SNIPPET_DATA1);
5580        }
5581
5582        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA2)) {
5583            sb.append(", " + Data.DATA2 + " AS " + SearchSnippetColumns.SNIPPET_DATA2);
5584        }
5585
5586        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA3)) {
5587            sb.append(", " + Data.DATA3 + " AS " + SearchSnippetColumns.SNIPPET_DATA3);
5588        }
5589
5590        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA4)) {
5591            sb.append(", " + Data.DATA4 + " AS " + SearchSnippetColumns.SNIPPET_DATA4);
5592        }
5593
5594        if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_MIMETYPE)) {
5595            sb.append(", (" +
5596                    "SELECT " + MimetypesColumns.MIMETYPE +
5597                    " FROM " + Tables.MIMETYPES +
5598                    " WHERE " + MimetypesColumns._ID + "=" + DataColumns.MIMETYPE_ID +
5599                    ") AS " + SearchSnippetColumns.SNIPPET_MIMETYPE);
5600        }
5601
5602        sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS + " WHERE ");
5603
5604        if (!TextUtils.isEmpty(filter)) {
5605            String normalizedFilter = NameNormalizer.normalize(filter);
5606            if (!TextUtils.isEmpty(normalizedFilter)) {
5607                sb.append(DataColumns.CONCRETE_ID + " IN (");
5608
5609                // Construct a query that gives us exactly one data _id per matching contact.
5610                // MIN stands in for ANY in this context.
5611                sb.append(
5612                        "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" +
5613                        " FROM " + Tables.NAME_LOOKUP +
5614                        " JOIN " + Tables.RAW_CONTACTS +
5615                        " ON (" + RawContactsColumns.CONCRETE_ID
5616                                + "=" + Tables.NAME_LOOKUP + "."
5617                                        + NameLookupColumns.RAW_CONTACT_ID + ")" +
5618                        " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '");
5619                sb.append(normalizedFilter);
5620                sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
5621                            " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" +
5622                        " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID +
5623                        ")");
5624            } else {
5625                sb.append("0");     // Empty filter - return an empty set
5626            }
5627        } else {
5628            sb.append("0");     // Empty filter - return an empty set
5629        }
5630
5631        sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)");
5632
5633        qb.setTables(sb.toString());
5634        qb.setProjectionMap(sContactsProjectionWithSnippetMap);
5635    }
5636
5637    private void appendContactsTables(StringBuilder sb, Uri uri, String[] projection) {
5638        boolean excludeRestrictedData = false;
5639        String requestingPackage = getQueryParameter(uri,
5640                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5641        if (requestingPackage != null) {
5642            excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5643        }
5644        sb.append(mDbHelper.getContactView(excludeRestrictedData));
5645        appendContactPresenceJoin(sb, projection, Contacts._ID);
5646        appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
5647    }
5648
5649    private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
5650        StringBuilder sb = new StringBuilder();
5651        boolean excludeRestrictedData = false;
5652        String requestingPackage = getQueryParameter(uri,
5653                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5654        if (requestingPackage != null) {
5655            excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5656        }
5657        sb.append(mDbHelper.getRawContactView(excludeRestrictedData));
5658        qb.setTables(sb.toString());
5659        qb.setProjectionMap(sRawContactsProjectionMap);
5660        appendAccountFromParameter(qb, uri);
5661    }
5662
5663    private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) {
5664        qb.setTables(mDbHelper.getRawEntitiesView(shouldExcludeRestrictedData(uri)));
5665        qb.setProjectionMap(sRawEntityProjectionMap);
5666        appendAccountFromParameter(qb, uri);
5667    }
5668
5669    private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
5670            String[] projection, boolean distinct) {
5671        StringBuilder sb = new StringBuilder();
5672        sb.append(mDbHelper.getDataView(shouldExcludeRestrictedData(uri)));
5673        sb.append(" data");
5674
5675        appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID);
5676        appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
5677        appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
5678        appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
5679
5680        qb.setTables(sb.toString());
5681
5682        boolean useDistinct = distinct
5683                || !mDbHelper.isInProjection(projection, DISTINCT_DATA_PROHIBITING_COLUMNS);
5684        qb.setDistinct(useDistinct);
5685        qb.setProjectionMap(useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap);
5686        appendAccountFromParameter(qb, uri);
5687    }
5688
5689    private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
5690            String[] projection) {
5691        StringBuilder sb = new StringBuilder();
5692        sb.append(mDbHelper.getDataView());
5693        sb.append(" data");
5694        appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
5695        appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
5696
5697        qb.setTables(sb.toString());
5698        qb.setProjectionMap(sStatusUpdatesProjectionMap);
5699    }
5700
5701    private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri,
5702            String[] projection) {
5703        StringBuilder sb = new StringBuilder();
5704        sb.append(mDbHelper.getEntitiesView(shouldExcludeRestrictedData(uri)));
5705        sb.append(" data");
5706
5707        appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID);
5708        appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
5709        appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID);
5710        appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID);
5711
5712        qb.setTables(sb.toString());
5713        qb.setProjectionMap(sEntityProjectionMap);
5714        appendAccountFromParameter(qb, uri);
5715    }
5716
5717    private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection,
5718            String lastStatusUpdateIdColumn) {
5719        if (mDbHelper.isInProjection(projection,
5720                Contacts.CONTACT_STATUS,
5721                Contacts.CONTACT_STATUS_RES_PACKAGE,
5722                Contacts.CONTACT_STATUS_ICON,
5723                Contacts.CONTACT_STATUS_LABEL,
5724                Contacts.CONTACT_STATUS_TIMESTAMP)) {
5725            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
5726                    + ContactsStatusUpdatesColumns.ALIAS +
5727                    " ON (" + lastStatusUpdateIdColumn + "="
5728                            + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
5729        }
5730    }
5731
5732    private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection,
5733            String dataIdColumn) {
5734        if (mDbHelper.isInProjection(projection,
5735                StatusUpdates.STATUS,
5736                StatusUpdates.STATUS_RES_PACKAGE,
5737                StatusUpdates.STATUS_ICON,
5738                StatusUpdates.STATUS_LABEL,
5739                StatusUpdates.STATUS_TIMESTAMP)) {
5740            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
5741                    " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
5742                            + dataIdColumn + ")");
5743        }
5744    }
5745
5746    private void appendContactPresenceJoin(StringBuilder sb, String[] projection,
5747            String contactIdColumn) {
5748        if (mDbHelper.isInProjection(projection,
5749                Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) {
5750            sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
5751                    " ON (" + contactIdColumn + " = "
5752                            + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")");
5753        }
5754    }
5755
5756    private void appendDataPresenceJoin(StringBuilder sb, String[] projection,
5757            String dataIdColumn) {
5758        if (mDbHelper.isInProjection(projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) {
5759            sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
5760                    " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")");
5761        }
5762    }
5763
5764    private void appendLocalDirectorySelectionIfNeeded(SQLiteQueryBuilder qb, long directoryId) {
5765        if (directoryId == Directory.DEFAULT) {
5766            qb.appendWhere(Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY);
5767        } else if (directoryId == Directory.LOCAL_INVISIBLE){
5768            qb.appendWhere(Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY);
5769        }
5770    }
5771
5772    private boolean shouldExcludeRestrictedData(Uri uri) {
5773        // Note: currently, "export only" equals to "restricted", but may not in the future.
5774        boolean excludeRestrictedData = readBooleanQueryParameter(uri,
5775                Data.FOR_EXPORT_ONLY, false);
5776        if (excludeRestrictedData) {
5777            return true;
5778        }
5779
5780        String requestingPackage = getQueryParameter(uri,
5781                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5782        if (requestingPackage != null) {
5783            return !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5784        }
5785
5786        return false;
5787    }
5788
5789    private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
5790        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
5791        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
5792
5793        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
5794        if (partialUri) {
5795            // Throw when either account is incomplete
5796            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
5797                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
5798        }
5799
5800        // Accounts are valid by only checking one parameter, since we've
5801        // already ruled out partial accounts.
5802        final boolean validAccount = !TextUtils.isEmpty(accountName);
5803        if (validAccount) {
5804            qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
5805                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
5806                    + RawContacts.ACCOUNT_TYPE + "="
5807                    + DatabaseUtils.sqlEscapeString(accountType));
5808        } else {
5809            qb.appendWhere("1");
5810        }
5811    }
5812
5813    private String appendAccountToSelection(Uri uri, String selection) {
5814        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
5815        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
5816
5817        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
5818        if (partialUri) {
5819            // Throw when either account is incomplete
5820            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
5821                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
5822        }
5823
5824        // Accounts are valid by only checking one parameter, since we've
5825        // already ruled out partial accounts.
5826        final boolean validAccount = !TextUtils.isEmpty(accountName);
5827        if (validAccount) {
5828            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
5829                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
5830                    + RawContacts.ACCOUNT_TYPE + "="
5831                    + DatabaseUtils.sqlEscapeString(accountType));
5832            if (!TextUtils.isEmpty(selection)) {
5833                selectionSb.append(" AND (");
5834                selectionSb.append(selection);
5835                selectionSb.append(')');
5836            }
5837            return selectionSb.toString();
5838        } else {
5839            return selection;
5840        }
5841    }
5842
5843    /**
5844     * Gets the value of the "limit" URI query parameter.
5845     *
5846     * @return A string containing a non-negative integer, or <code>null</code> if
5847     *         the parameter is not set, or is set to an invalid value.
5848     */
5849    private String getLimit(Uri uri) {
5850        String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY);
5851        if (limitParam == null) {
5852            return null;
5853        }
5854        // make sure that the limit is a non-negative integer
5855        try {
5856            int l = Integer.parseInt(limitParam);
5857            if (l < 0) {
5858                Log.w(TAG, "Invalid limit parameter: " + limitParam);
5859                return null;
5860            }
5861            return String.valueOf(l);
5862        } catch (NumberFormatException ex) {
5863            Log.w(TAG, "Invalid limit parameter: " + limitParam);
5864            return null;
5865        }
5866    }
5867
5868    String getContactsRestrictions() {
5869        if (mDbHelper.hasAccessToRestrictedData()) {
5870            return "1";
5871        } else {
5872            return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0";
5873        }
5874    }
5875
5876    public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
5877        if (mDbHelper.hasAccessToRestrictedData()) {
5878            return "1";
5879        } else {
5880            return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
5881                    + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
5882        }
5883    }
5884
5885    @Override
5886    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
5887        int match = sUriMatcher.match(uri);
5888        switch (match) {
5889            case CONTACTS_ID_PHOTO: {
5890                return openPhotoAssetFile(uri, mode,
5891                        Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?",
5892                        new String[]{uri.getPathSegments().get(1)});
5893            }
5894
5895            case DATA_ID: {
5896                return openPhotoAssetFile(uri, mode,
5897                        Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'",
5898                        new String[]{uri.getPathSegments().get(1)});
5899            }
5900
5901            case CONTACTS_AS_VCARD: {
5902                final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
5903                mSelectionArgs1[0] = String.valueOf(lookupContactIdByLookupKey(mDb, lookupKey));
5904                final String selection = Contacts._ID + "=?";
5905
5906                // When opening a contact as file, we pass back contents as a
5907                // vCard-encoded stream. We build into a local buffer first,
5908                // then pipe into MemoryFile once the exact size is known.
5909                final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
5910                outputRawContactsAsVCard(localStream, selection, mSelectionArgs1);
5911                return buildAssetFileDescriptor(localStream);
5912            }
5913
5914            case CONTACTS_AS_MULTI_VCARD: {
5915                final String lookupKeys = uri.getPathSegments().get(2);
5916                final String[] loopupKeyList = lookupKeys.split(":");
5917                final StringBuilder inBuilder = new StringBuilder();
5918                int index = 0;
5919                // SQLite has limits on how many parameters can be used
5920                // so the IDs are concatenated to a query string here instead
5921                for (String lookupKey : loopupKeyList) {
5922                    if (index == 0) {
5923                        inBuilder.append("(");
5924                    } else {
5925                        inBuilder.append(",");
5926                    }
5927                    inBuilder.append(lookupContactIdByLookupKey(mDb, lookupKey));
5928                    index++;
5929                }
5930                inBuilder.append(')');
5931                final String selection = Contacts._ID + " IN " + inBuilder.toString();
5932
5933                // When opening a contact as file, we pass back contents as a
5934                // vCard-encoded stream. We build into a local buffer first,
5935                // then pipe into MemoryFile once the exact size is known.
5936                final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
5937                outputRawContactsAsVCard(localStream, selection, null);
5938                return buildAssetFileDescriptor(localStream);
5939            }
5940
5941            default:
5942                throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist",
5943                        uri));
5944        }
5945    }
5946
5947    private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection,
5948            String[] selectionArgs)
5949            throws FileNotFoundException {
5950        if (!"r".equals(mode)) {
5951            throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode
5952                    + " not supported.", uri));
5953        }
5954
5955        String sql =
5956                "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
5957                " WHERE " + selection;
5958        SQLiteDatabase db = mDbHelper.getReadableDatabase();
5959        try {
5960            return makeAssetFileDescriptor(
5961                    DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs));
5962        } catch (SQLiteDoneException e) {
5963            // this will happen if the DB query returns no rows (i.e. contact does not exist)
5964            throw new FileNotFoundException(uri.toString());
5965        }
5966    }
5967
5968    private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
5969
5970    /**
5971     * Returns an {@link AssetFileDescriptor} backed by the
5972     * contents of the given {@link ByteArrayOutputStream}.
5973     */
5974    private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
5975        try {
5976            stream.flush();
5977
5978            final byte[] byteData = stream.toByteArray();
5979
5980            return makeAssetFileDescriptor(
5981                    ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME),
5982                    byteData.length);
5983        } catch (IOException e) {
5984            Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString());
5985            return null;
5986        }
5987    }
5988
5989    private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd) {
5990        return makeAssetFileDescriptor(fd, AssetFileDescriptor.UNKNOWN_LENGTH);
5991    }
5992
5993    private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd, long length) {
5994        return fd != null ? new AssetFileDescriptor(fd, 0, length) : null;
5995    }
5996
5997    /**
5998     * Output {@link RawContacts} matching the requested selection in the vCard
5999     * format to the given {@link OutputStream}. This method returns silently if
6000     * any errors encountered.
6001     */
6002    private void outputRawContactsAsVCard(OutputStream stream, String selection,
6003            String[] selectionArgs) {
6004        final Context context = this.getContext();
6005        final VCardComposer composer =
6006                new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false);
6007        composer.addHandler(composer.new HandlerForOutputStream(stream));
6008
6009        // No extra checks since composer always uses restricted views
6010        if (!composer.init(selection, selectionArgs)) {
6011            Log.w(TAG, "Failed to init VCardComposer");
6012            return;
6013        }
6014
6015        while (!composer.isAfterLast()) {
6016            if (!composer.createOneEntry()) {
6017                Log.w(TAG, "Failed to output a contact.");
6018            }
6019        }
6020        composer.terminate();
6021    }
6022
6023    @Override
6024    public String getType(Uri uri) {
6025        final int match = sUriMatcher.match(uri);
6026        switch (match) {
6027            case CONTACTS:
6028                return Contacts.CONTENT_TYPE;
6029            case CONTACTS_LOOKUP:
6030            case CONTACTS_ID:
6031            case CONTACTS_LOOKUP_ID:
6032                return Contacts.CONTENT_ITEM_TYPE;
6033            case CONTACTS_AS_VCARD:
6034            case CONTACTS_AS_MULTI_VCARD:
6035                return Contacts.CONTENT_VCARD_TYPE;
6036            case CONTACTS_ID_PHOTO:
6037                return "image/png";
6038            case RAW_CONTACTS:
6039                return RawContacts.CONTENT_TYPE;
6040            case RAW_CONTACTS_ID:
6041                return RawContacts.CONTENT_ITEM_TYPE;
6042            case DATA:
6043                return Data.CONTENT_TYPE;
6044            case DATA_ID:
6045                return mDbHelper.getDataMimeType(ContentUris.parseId(uri));
6046            case PHONES:
6047                return Phone.CONTENT_TYPE;
6048            case PHONES_ID:
6049                return Phone.CONTENT_ITEM_TYPE;
6050            case PHONE_LOOKUP:
6051                return PhoneLookup.CONTENT_TYPE;
6052            case EMAILS:
6053                return Email.CONTENT_TYPE;
6054            case EMAILS_ID:
6055                return Email.CONTENT_ITEM_TYPE;
6056            case POSTALS:
6057                return StructuredPostal.CONTENT_TYPE;
6058            case POSTALS_ID:
6059                return StructuredPostal.CONTENT_ITEM_TYPE;
6060            case AGGREGATION_EXCEPTIONS:
6061                return AggregationExceptions.CONTENT_TYPE;
6062            case AGGREGATION_EXCEPTION_ID:
6063                return AggregationExceptions.CONTENT_ITEM_TYPE;
6064            case SETTINGS:
6065                return Settings.CONTENT_TYPE;
6066            case AGGREGATION_SUGGESTIONS:
6067                return Contacts.CONTENT_TYPE;
6068            case SEARCH_SUGGESTIONS:
6069                return SearchManager.SUGGEST_MIME_TYPE;
6070            case SEARCH_SHORTCUT:
6071                return SearchManager.SHORTCUT_MIME_TYPE;
6072            case DIRECTORIES:
6073                return Directory.CONTENT_TYPE;
6074            case DIRECTORIES_ID:
6075                return Directory.CONTENT_ITEM_TYPE;
6076            default:
6077                return mLegacyApiSupport.getType(uri);
6078        }
6079    }
6080
6081    public String[] getDefaultProjection(Uri uri) {
6082        final int match = sUriMatcher.match(uri);
6083        switch (match) {
6084            case CONTACTS:
6085            case CONTACTS_LOOKUP:
6086            case CONTACTS_ID:
6087            case CONTACTS_LOOKUP_ID:
6088            case AGGREGATION_SUGGESTIONS:
6089                return sContactsProjectionMap.getColumnNames();
6090
6091            case CONTACTS_ID_ENTITIES:
6092                return sEntityProjectionMap.getColumnNames();
6093
6094            case CONTACTS_AS_VCARD:
6095            case CONTACTS_AS_MULTI_VCARD:
6096                return sContactsVCardProjectionMap.getColumnNames();
6097
6098            case RAW_CONTACTS:
6099            case RAW_CONTACTS_ID:
6100                return sRawContactsProjectionMap.getColumnNames();
6101
6102            case DATA_ID:
6103            case PHONES:
6104            case PHONES_ID:
6105            case EMAILS:
6106            case EMAILS_ID:
6107            case POSTALS:
6108            case POSTALS_ID:
6109                return sDataProjectionMap.getColumnNames();
6110
6111            case PHONE_LOOKUP:
6112                return sPhoneLookupProjectionMap.getColumnNames();
6113
6114            case AGGREGATION_EXCEPTIONS:
6115            case AGGREGATION_EXCEPTION_ID:
6116                return sAggregationExceptionsProjectionMap.getColumnNames();
6117
6118            case SETTINGS:
6119                return sSettingsProjectionMap.getColumnNames();
6120
6121            case DIRECTORIES:
6122            case DIRECTORIES_ID:
6123                return sDirectoryProjectionMap.getColumnNames();
6124
6125            default:
6126                return null;
6127        }
6128    }
6129
6130    private void setDisplayName(long rawContactId, int displayNameSource,
6131            String displayNamePrimary, String displayNameAlternative, String phoneticName,
6132            int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) {
6133        mRawContactDisplayNameUpdate.bindLong(1, displayNameSource);
6134        bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary);
6135        bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative);
6136        bindString(mRawContactDisplayNameUpdate, 4, phoneticName);
6137        mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle);
6138        bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary);
6139        bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative);
6140        mRawContactDisplayNameUpdate.bindLong(8, rawContactId);
6141        mRawContactDisplayNameUpdate.execute();
6142    }
6143
6144    /**
6145     * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
6146     */
6147    private void setRawContactDirty(long rawContactId) {
6148        mDirtyRawContacts.add(rawContactId);
6149    }
6150
6151    /*
6152     * Sets the given dataId record in the "data" table to primary, and resets all data records of
6153     * the same mimetype and under the same contact to not be primary.
6154     *
6155     * @param dataId the id of the data record to be set to primary.
6156     */
6157    private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
6158        mSetPrimaryStatement.bindLong(1, dataId);
6159        mSetPrimaryStatement.bindLong(2, mimeTypeId);
6160        mSetPrimaryStatement.bindLong(3, rawContactId);
6161        mSetPrimaryStatement.execute();
6162    }
6163
6164    /*
6165     * Sets the given dataId record in the "data" table to "super primary", and resets all data
6166     * records of the same mimetype and under the same aggregate to not be "super primary".
6167     *
6168     * @param dataId the id of the data record to be set to primary.
6169     */
6170    private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
6171        mSetSuperPrimaryStatement.bindLong(1, dataId);
6172        mSetSuperPrimaryStatement.bindLong(2, mimeTypeId);
6173        mSetSuperPrimaryStatement.bindLong(3, rawContactId);
6174        mSetSuperPrimaryStatement.execute();
6175    }
6176
6177    public String insertNameLookupForEmail(long rawContactId, long dataId, String email) {
6178        if (TextUtils.isEmpty(email)) {
6179            return null;
6180        }
6181
6182        String address = mDbHelper.extractHandleFromEmailAddress(email);
6183        if (address == null) {
6184            return null;
6185        }
6186
6187        insertNameLookup(rawContactId, dataId,
6188                NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address));
6189        return address;
6190    }
6191
6192    /**
6193     * Normalizes the nickname and inserts it in the name lookup table.
6194     */
6195    public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) {
6196        if (TextUtils.isEmpty(nickname)) {
6197            return;
6198        }
6199
6200        insertNameLookup(rawContactId, dataId,
6201                NameLookupType.NICKNAME, NameNormalizer.normalize(nickname));
6202    }
6203
6204    public void insertNameLookupForOrganization(long rawContactId, long dataId, String company,
6205            String title) {
6206        if (!TextUtils.isEmpty(company)) {
6207            insertNameLookup(rawContactId, dataId,
6208                    NameLookupType.ORGANIZATION, NameNormalizer.normalize(company));
6209        }
6210        if (!TextUtils.isEmpty(title)) {
6211            insertNameLookup(rawContactId, dataId,
6212                    NameLookupType.ORGANIZATION, NameNormalizer.normalize(title));
6213        }
6214    }
6215
6216    public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name,
6217            int fullNameStyle) {
6218        mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle);
6219    }
6220
6221    private class StructuredNameLookupBuilder extends NameLookupBuilder {
6222
6223        public StructuredNameLookupBuilder(NameSplitter splitter) {
6224            super(splitter);
6225        }
6226
6227        @Override
6228        protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
6229                String name) {
6230            ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name);
6231        }
6232
6233        @Override
6234        protected String[] getCommonNicknameClusters(String normalizedName) {
6235            return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
6236        }
6237    }
6238
6239    public void insertNameLookupForPhoneticName(long rawContactId, long dataId,
6240            ContentValues values) {
6241        if (values.containsKey(StructuredName.PHONETIC_FAMILY_NAME)
6242                || values.containsKey(StructuredName.PHONETIC_GIVEN_NAME)
6243                || values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME)) {
6244            insertNameLookupForPhoneticName(rawContactId, dataId,
6245                    values.getAsString(StructuredName.PHONETIC_FAMILY_NAME),
6246                    values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME),
6247                    values.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
6248        }
6249    }
6250
6251    public void insertNameLookupForPhoneticName(long rawContactId, long dataId, String familyName,
6252            String middleName, String givenName) {
6253        mSb.setLength(0);
6254        if (familyName != null) {
6255            mSb.append(familyName.trim());
6256        }
6257        if (middleName != null) {
6258            mSb.append(middleName.trim());
6259        }
6260        if (givenName != null) {
6261            mSb.append(givenName.trim());
6262        }
6263
6264        if (mSb.length() > 0) {
6265            insertNameLookup(rawContactId, dataId, NameLookupType.NAME_COLLATION_KEY,
6266                    NameNormalizer.normalize(mSb.toString()));
6267        }
6268
6269        if (givenName != null) {
6270            // We want the phonetic given name to be used for search, but not for aggregation,
6271            // which is why we are using NAME_SHORTHAND rather than NAME_COLLATION_KEY
6272            insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND,
6273                    NameNormalizer.normalize(givenName.trim()));
6274        }
6275    }
6276
6277    /**
6278     * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
6279     */
6280    public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
6281        mNameLookupInsert.bindLong(1, rawContactId);
6282        mNameLookupInsert.bindLong(2, dataId);
6283        mNameLookupInsert.bindLong(3, lookupType);
6284        bindString(mNameLookupInsert, 4, name);
6285        mNameLookupInsert.executeInsert();
6286    }
6287
6288    /**
6289     * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
6290     */
6291    public void deleteNameLookup(long dataId) {
6292        mNameLookupDelete.bindLong(1, dataId);
6293        mNameLookupDelete.execute();
6294    }
6295
6296    public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
6297        sb.append("(" +
6298                "SELECT DISTINCT " + RawContacts.CONTACT_ID +
6299                " FROM " + Tables.RAW_CONTACTS +
6300                " JOIN " + Tables.NAME_LOOKUP +
6301                " ON(" + RawContactsColumns.CONCRETE_ID + "="
6302                        + NameLookupColumns.RAW_CONTACT_ID + ")" +
6303                " WHERE normalized_name GLOB '");
6304        sb.append(NameNormalizer.normalize(filterParam));
6305        sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
6306                    " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))");
6307    }
6308
6309    public String getRawContactsByFilterAsNestedQuery(String filterParam) {
6310        StringBuilder sb = new StringBuilder();
6311        appendRawContactsByFilterAsNestedQuery(sb, filterParam);
6312        return sb.toString();
6313    }
6314
6315    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) {
6316        appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true);
6317    }
6318
6319    private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
6320            boolean allowEmailMatch) {
6321        if (TextUtils.isEmpty(normalizedName)) {
6322            // Effectively an empty IN clause - SQL syntax does not allow an actual empty list here
6323            sb.append("(0)");
6324        } else {
6325            sb.append("(" +
6326                    "SELECT " + NameLookupColumns.RAW_CONTACT_ID +
6327                    " FROM " + Tables.NAME_LOOKUP +
6328                    " WHERE " + NameLookupColumns.NORMALIZED_NAME +
6329                    " GLOB '");
6330            // Should not use a "?" argument placeholder here, because
6331            // that would prevent the SQL optimizer from using the index on NORMALIZED_NAME.
6332            sb.append(normalizedName);
6333            sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
6334                    + NameLookupType.NAME_COLLATION_KEY + ","
6335                    + NameLookupType.NICKNAME + ","
6336                    + NameLookupType.NAME_SHORTHAND + ","
6337                    + NameLookupType.ORGANIZATION + ","
6338                    + NameLookupType.NAME_CONSONANTS);
6339            if (allowEmailMatch) {
6340                sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME);
6341            }
6342            sb.append("))");
6343        }
6344    }
6345
6346    /**
6347     * Takes components of a name from the query parameters and returns a cursor with those
6348     * components as well as all missing components.  There is no database activity involved
6349     * in this so the call can be made on the UI thread.
6350     */
6351    private Cursor completeName(Uri uri, String[] projection) {
6352        if (projection == null) {
6353            projection = sDataProjectionMap.getColumnNames();
6354        }
6355
6356        ContentValues values = new ContentValues();
6357        StructuredNameRowHandler handler =
6358                (StructuredNameRowHandler) getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE);
6359
6360        copyQueryParamsToContentValues(values, uri,
6361                StructuredName.DISPLAY_NAME,
6362                StructuredName.PREFIX,
6363                StructuredName.GIVEN_NAME,
6364                StructuredName.MIDDLE_NAME,
6365                StructuredName.FAMILY_NAME,
6366                StructuredName.SUFFIX,
6367                StructuredName.PHONETIC_NAME,
6368                StructuredName.PHONETIC_FAMILY_NAME,
6369                StructuredName.PHONETIC_MIDDLE_NAME,
6370                StructuredName.PHONETIC_GIVEN_NAME
6371        );
6372
6373        handler.fixStructuredNameComponents(values, values);
6374
6375        MatrixCursor cursor = new MatrixCursor(projection);
6376        Object[] row = new Object[projection.length];
6377        for (int i = 0; i < projection.length; i++) {
6378            row[i] = values.get(projection[i]);
6379        }
6380        cursor.addRow(row);
6381        return cursor;
6382    }
6383
6384    private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) {
6385        for (String column : columns) {
6386            String param = uri.getQueryParameter(column);
6387            if (param != null) {
6388                values.put(column, param);
6389            }
6390        }
6391    }
6392
6393
6394    /**
6395     * Inserts an argument at the beginning of the selection arg list.
6396     */
6397    private String[] insertSelectionArg(String[] selectionArgs, String arg) {
6398        if (selectionArgs == null) {
6399            return new String[] {arg};
6400        } else {
6401            int newLength = selectionArgs.length + 1;
6402            String[] newSelectionArgs = new String[newLength];
6403            newSelectionArgs[0] = arg;
6404            System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
6405            return newSelectionArgs;
6406        }
6407    }
6408
6409    private String[] appendProjectionArg(String[] projection, String arg) {
6410        if (projection == null) {
6411            return null;
6412        }
6413        final int length = projection.length;
6414        String[] newProjection = new String[length + 1];
6415        System.arraycopy(projection, 0, newProjection, 0, length);
6416        newProjection[length] = arg;
6417        return newProjection;
6418    }
6419
6420    protected Account getDefaultAccount() {
6421        AccountManager accountManager = AccountManager.get(getContext());
6422        try {
6423            Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE,
6424                    new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult();
6425            if (accounts != null && accounts.length > 0) {
6426                return accounts[0];
6427            }
6428        } catch (Throwable e) {
6429            Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
6430        }
6431        return null;
6432    }
6433
6434    /**
6435     * Returns true if the specified account type is writable.
6436     */
6437    protected boolean isWritableAccount(String accountType) {
6438        if (accountType == null) {
6439            return true;
6440        }
6441
6442        Boolean writable = mAccountWritability.get(accountType);
6443        if (writable != null) {
6444            return writable;
6445        }
6446
6447        IContentService contentService = ContentResolver.getContentService();
6448        try {
6449            for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
6450                if (ContactsContract.AUTHORITY.equals(sync.authority) &&
6451                        accountType.equals(sync.accountType)) {
6452                    writable = sync.supportsUploading();
6453                    break;
6454                }
6455            }
6456        } catch (RemoteException e) {
6457            Log.e(TAG, "Could not acquire sync adapter types");
6458        }
6459
6460        if (writable == null) {
6461            writable = false;
6462        }
6463
6464        mAccountWritability.put(accountType, writable);
6465        return writable;
6466    }
6467
6468
6469    /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
6470            boolean defaultValue) {
6471
6472        // Manually parse the query, which is much faster than calling uri.getQueryParameter
6473        String query = uri.getEncodedQuery();
6474        if (query == null) {
6475            return defaultValue;
6476        }
6477
6478        int index = query.indexOf(parameter);
6479        if (index == -1) {
6480            return defaultValue;
6481        }
6482
6483        index += parameter.length();
6484
6485        return !matchQueryParameter(query, index, "=0", false)
6486                && !matchQueryParameter(query, index, "=false", true);
6487    }
6488
6489    private static boolean matchQueryParameter(String query, int index, String value,
6490            boolean ignoreCase) {
6491        int length = value.length();
6492        return query.regionMatches(ignoreCase, index, value, 0, length)
6493                && (query.length() == index + length || query.charAt(index + length) == '&');
6494    }
6495
6496    /**
6497     * A fast re-implementation of {@link Uri#getQueryParameter}
6498     */
6499    /* package */ static String getQueryParameter(Uri uri, String parameter) {
6500        String query = uri.getEncodedQuery();
6501        if (query == null) {
6502            return null;
6503        }
6504
6505        int queryLength = query.length();
6506        int parameterLength = parameter.length();
6507
6508        String value;
6509        int index = 0;
6510        while (true) {
6511            index = query.indexOf(parameter, index);
6512            if (index == -1) {
6513                return null;
6514            }
6515
6516            index += parameterLength;
6517
6518            if (queryLength == index) {
6519                return null;
6520            }
6521
6522            if (query.charAt(index) == '=') {
6523                index++;
6524                break;
6525            }
6526        }
6527
6528        int ampIndex = query.indexOf('&', index);
6529        if (ampIndex == -1) {
6530            value = query.substring(index);
6531        } else {
6532            value = query.substring(index, ampIndex);
6533        }
6534
6535        return Uri.decode(value);
6536    }
6537
6538    private void bindString(SQLiteStatement stmt, int index, String value) {
6539        if (value == null) {
6540            stmt.bindNull(index);
6541        } else {
6542            stmt.bindString(index, value);
6543        }
6544    }
6545
6546    private void bindLong(SQLiteStatement stmt, int index, Number value) {
6547        if (value == null) {
6548            stmt.bindNull(index);
6549        } else {
6550            stmt.bindLong(index, value.longValue());
6551        }
6552    }
6553
6554    protected boolean isAggregationUpgradeNeeded() {
6555        if (!mContactAggregator.isEnabled()) {
6556            return false;
6557        }
6558
6559        int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_AGGREGATION_ALGORITHM, "1"));
6560        return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION;
6561    }
6562
6563    protected void upgradeAggregationAlgorithm() {
6564        // This upgrade will affect very few contacts, so it can be performed on the
6565        // main thread during the initial boot after an OTA
6566
6567        Log.i(TAG, "Upgrading aggregation algorithm");
6568        int count = 0;
6569        long start = SystemClock.currentThreadTimeMillis();
6570        try {
6571            mDb.beginTransaction();
6572            Cursor cursor = mDb.query(true,
6573                    Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2",
6574                    new String[]{"r1." + RawContacts._ID},
6575                    "r1." + RawContacts._ID + "!=r2." + RawContacts._ID +
6576                    " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID +
6577                    " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME +
6578                    " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE,
6579                    null, null, null, null, null);
6580            try {
6581                while (cursor.moveToNext()) {
6582                    long rawContactId = cursor.getLong(0);
6583                    mContactAggregator.markForAggregation(rawContactId,
6584                            RawContacts.AGGREGATION_MODE_DEFAULT, true);
6585                    count++;
6586                }
6587            } finally {
6588                cursor.close();
6589            }
6590            mContactAggregator.aggregateInTransaction(mDb);
6591            mDb.setTransactionSuccessful();
6592            mDbHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM,
6593                    String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
6594        } finally {
6595            mDb.endTransaction();
6596            long end = SystemClock.currentThreadTimeMillis();
6597            Log.i(TAG, "Aggregation algorithm upgraded for " + count
6598                    + " contacts, in " + (end - start) + "ms");
6599        }
6600    }
6601}
6602