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