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