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