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