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