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