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