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