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