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