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