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