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