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