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