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