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