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