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