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