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