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