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