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