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