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