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