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