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