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