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