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