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