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