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