ContactsProvider2.java revision bd9abbb6b03b4ec1e28ad3fa2fcba5d1eb8609ea
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.AggregatedPresenceColumns;
24import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
25import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
26import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
27import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
28import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
29import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
30import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
31import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
32import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
33import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
34import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
35import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
36import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns;
37import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
38import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
39import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
40import com.android.vcard.VCardComposer;
41import com.android.vcard.VCardConfig;
42import com.google.android.collect.Lists;
43import com.google.android.collect.Maps;
44import com.google.android.collect.Sets;
45
46import android.accounts.Account;
47import android.accounts.AccountManager;
48import android.accounts.OnAccountsUpdateListener;
49import android.app.Notification;
50import android.app.NotificationManager;
51import android.app.PendingIntent;
52import android.app.SearchManager;
53import android.content.ContentProviderOperation;
54import android.content.ContentProviderResult;
55import android.content.ContentResolver;
56import android.content.ContentUris;
57import android.content.ContentValues;
58import android.content.Context;
59import android.content.IContentService;
60import android.content.Intent;
61import android.content.OperationApplicationException;
62import android.content.SharedPreferences;
63import android.content.SyncAdapterType;
64import android.content.UriMatcher;
65import android.content.res.AssetFileDescriptor;
66import android.database.CrossProcessCursor;
67import android.database.Cursor;
68import android.database.CursorWindow;
69import android.database.CursorWrapper;
70import android.database.DatabaseUtils;
71import android.database.MatrixCursor;
72import android.database.MatrixCursor.RowBuilder;
73import android.database.sqlite.SQLiteDatabase;
74import android.database.sqlite.SQLiteDoneException;
75import android.database.sqlite.SQLiteQueryBuilder;
76import android.net.Uri;
77import android.net.Uri.Builder;
78import android.os.Binder;
79import android.os.Bundle;
80import android.os.Handler;
81import android.os.HandlerThread;
82import android.os.Message;
83import android.os.ParcelFileDescriptor;
84import android.os.Process;
85import android.os.RemoteException;
86import android.os.StrictMode;
87import android.os.SystemClock;
88import android.os.SystemProperties;
89import android.preference.PreferenceManager;
90import android.provider.BaseColumns;
91import android.provider.ContactsContract;
92import android.provider.ContactsContract.AggregationExceptions;
93import android.provider.ContactsContract.CommonDataKinds.Email;
94import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
95import android.provider.ContactsContract.CommonDataKinds.Im;
96import android.provider.ContactsContract.CommonDataKinds.Nickname;
97import android.provider.ContactsContract.CommonDataKinds.Note;
98import android.provider.ContactsContract.CommonDataKinds.Organization;
99import android.provider.ContactsContract.CommonDataKinds.Phone;
100import android.provider.ContactsContract.CommonDataKinds.Photo;
101import android.provider.ContactsContract.CommonDataKinds.StructuredName;
102import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
103import android.provider.ContactsContract.ContactCounts;
104import android.provider.ContactsContract.Contacts;
105import android.provider.ContactsContract.Contacts.AggregationSuggestions;
106import android.provider.ContactsContract.Data;
107import android.provider.ContactsContract.Directory;
108import android.provider.ContactsContract.Groups;
109import android.provider.ContactsContract.Intents;
110import android.provider.ContactsContract.PhoneLookup;
111import android.provider.ContactsContract.ProviderStatus;
112import android.provider.ContactsContract.RawContacts;
113import android.provider.ContactsContract.SearchSnippetColumns;
114import android.provider.ContactsContract.Settings;
115import android.provider.ContactsContract.StatusUpdates;
116import android.provider.LiveFolders;
117import android.provider.OpenableColumns;
118import android.provider.SyncStateContract;
119import android.telephony.PhoneNumberUtils;
120import android.telephony.TelephonyManager;
121import android.text.TextUtils;
122import android.util.Log;
123
124import java.io.ByteArrayOutputStream;
125import java.io.FileNotFoundException;
126import java.io.IOException;
127import java.io.OutputStream;
128import java.text.SimpleDateFormat;
129import java.util.ArrayList;
130import java.util.Collections;
131import java.util.Date;
132import java.util.HashMap;
133import java.util.HashSet;
134import java.util.List;
135import java.util.Locale;
136import java.util.Map;
137import java.util.Set;
138import java.util.concurrent.CountDownLatch;
139
140/**
141 * Contacts content provider. The contract between this provider and applications
142 * is defined in {@link ContactsContract}.
143 */
144public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {
145
146    private static final String TAG = "ContactsProvider";
147
148    private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
149
150    private static final int BACKGROUND_TASK_INITIALIZE = 0;
151    private static final int BACKGROUND_TASK_OPEN_WRITE_ACCESS = 1;
152    private static final int BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS = 2;
153    private static final int BACKGROUND_TASK_UPDATE_ACCOUNTS = 3;
154    private static final int BACKGROUND_TASK_UPDATE_LOCALE = 4;
155    private static final int BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM = 5;
156    private static final int BACKGROUND_TASK_UPDATE_SEARCH_INDEX = 6;
157    private static final int BACKGROUND_TASK_UPDATE_PROVIDER_STATUS = 7;
158    private static final int BACKGROUND_TASK_UPDATE_DIRECTORIES = 8;
159    private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9;
160
161    /** Default for the maximum number of returned aggregation suggestions. */
162    private static final int DEFAULT_MAX_SUGGESTIONS = 5;
163
164    /**
165     * Property key for the legacy contact import version. The need for a version
166     * as opposed to a boolean flag is that if we discover bugs in the contact import process,
167     * we can trigger re-import by incrementing the import version.
168     */
169    private static final String PROPERTY_CONTACTS_IMPORTED = "contacts_imported_v1";
170    private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1;
171    private static final String PREF_LOCALE = "locale";
172
173    private static final String PROPERTY_AGGREGATION_ALGORITHM = "aggregation_v2";
174    private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 2;
175
176    private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
177
178    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
179
180    private static final String TIMES_CONTACTED_SORT_COLUMN = "times_contacted_sort";
181
182    private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
183            + TIMES_CONTACTED_SORT_COLUMN + " DESC, "
184            + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
185    private static final String STREQUENT_LIMIT =
186            "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
187            + Contacts.STARRED + "=1) + 25";
188
189    /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
190            "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
191            " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
192            " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?";
193
194    /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
195            "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
196            " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
197            " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?";
198
199    /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
200
201    private static final int CONTACTS = 1000;
202    private static final int CONTACTS_ID = 1001;
203    private static final int CONTACTS_LOOKUP = 1002;
204    private static final int CONTACTS_LOOKUP_ID = 1003;
205    private static final int CONTACTS_ID_DATA = 1004;
206    private static final int CONTACTS_FILTER = 1005;
207    private static final int CONTACTS_STREQUENT = 1006;
208    private static final int CONTACTS_STREQUENT_FILTER = 1007;
209    private static final int CONTACTS_GROUP = 1008;
210    private static final int CONTACTS_ID_PHOTO = 1009;
211    private static final int CONTACTS_AS_VCARD = 1010;
212    private static final int CONTACTS_AS_MULTI_VCARD = 1011;
213    private static final int CONTACTS_LOOKUP_DATA = 1012;
214    private static final int CONTACTS_LOOKUP_ID_DATA = 1013;
215    private static final int CONTACTS_ID_ENTITIES = 1014;
216    private static final int CONTACTS_LOOKUP_ENTITIES = 1015;
217    private static final int CONTACTS_LOOKUP_ID_ENTITIES = 1016;
218
219    private static final int RAW_CONTACTS = 2002;
220    private static final int RAW_CONTACTS_ID = 2003;
221    private static final int RAW_CONTACTS_DATA = 2004;
222    private static final int RAW_CONTACT_ENTITY_ID = 2005;
223
224    private static final int DATA = 3000;
225    private static final int DATA_ID = 3001;
226    private static final int PHONES = 3002;
227    private static final int PHONES_ID = 3003;
228    private static final int PHONES_FILTER = 3004;
229    private static final int EMAILS = 3005;
230    private static final int EMAILS_ID = 3006;
231    private static final int EMAILS_LOOKUP = 3007;
232    private static final int EMAILS_FILTER = 3008;
233    private static final int POSTALS = 3009;
234    private static final int POSTALS_ID = 3010;
235
236    private static final int PHONE_LOOKUP = 4000;
237
238    private static final int AGGREGATION_EXCEPTIONS = 6000;
239    private static final int AGGREGATION_EXCEPTION_ID = 6001;
240
241    private static final int STATUS_UPDATES = 7000;
242    private static final int STATUS_UPDATES_ID = 7001;
243
244    private static final int AGGREGATION_SUGGESTIONS = 8000;
245
246    private static final int SETTINGS = 9000;
247
248    private static final int GROUPS = 10000;
249    private static final int GROUPS_ID = 10001;
250    private static final int GROUPS_SUMMARY = 10003;
251
252    private static final int SYNCSTATE = 11000;
253    private static final int SYNCSTATE_ID = 11001;
254
255    private static final int SEARCH_SUGGESTIONS = 12001;
256    private static final int SEARCH_SHORTCUT = 12002;
257
258    private static final int LIVE_FOLDERS_CONTACTS = 14000;
259    private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001;
260    private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002;
261    private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003;
262
263    private static final int RAW_CONTACT_ENTITIES = 15001;
264
265    private static final int PROVIDER_STATUS = 16001;
266
267    private static final int DIRECTORIES = 17001;
268    private static final int DIRECTORIES_ID = 17002;
269
270    private static final int COMPLETE_NAME = 18000;
271
272    private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
273            RawContactsColumns.CONCRETE_ID + "=? AND "
274                    + GroupsColumns.CONCRETE_ACCOUNT_NAME
275                    + "=" + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
276                    + GroupsColumns.CONCRETE_ACCOUNT_TYPE
277                    + "=" + RawContactsColumns.CONCRETE_ACCOUNT_TYPE
278                    + " AND " + Groups.FAVORITES + " != 0";
279
280    private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID =
281            RawContactsColumns.CONCRETE_ID + "=? AND "
282                    + GroupsColumns.CONCRETE_ACCOUNT_NAME + "="
283                    + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
284                    + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
285                    + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND "
286                    + Groups.AUTO_ADD + " != 0";
287
288    private static final String[] PROJECTION_GROUP_ID
289            = new String[]{Tables.GROUPS + "." + Groups._ID};
290
291    private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? "
292            + "AND " + GroupMembership.GROUP_ROW_ID + "=? "
293            + "AND " + GroupMembership.RAW_CONTACT_ID + "=?";
294
295    private static final String SELECTION_STARRED_FROM_RAW_CONTACTS =
296            "SELECT " + RawContacts.STARRED
297                    + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?";
298
299    public class AddressBookCursor extends CursorWrapper implements CrossProcessCursor {
300        private final CrossProcessCursor mCursor;
301        private final Bundle mBundle;
302
303        public AddressBookCursor(CrossProcessCursor cursor, String[] titles, int[] counts) {
304            super(cursor);
305            mCursor = cursor;
306            mBundle = new Bundle();
307            mBundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles);
308            mBundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts);
309        }
310
311        @Override
312        public Bundle getExtras() {
313            return mBundle;
314        }
315
316        @Override
317        public void fillWindow(int pos, CursorWindow window) {
318            mCursor.fillWindow(pos, window);
319        }
320
321        @Override
322        public CursorWindow getWindow() {
323            return mCursor.getWindow();
324        }
325
326        @Override
327        public boolean onMove(int oldPosition, int newPosition) {
328            return mCursor.onMove(oldPosition, newPosition);
329        }
330    }
331
332    private interface DataContactsQuery {
333        public static final String TABLE = "data "
334                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
335                + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
336
337        public static final String[] PROJECTION = new String[] {
338            RawContactsColumns.CONCRETE_ID,
339            DataColumns.CONCRETE_ID,
340            ContactsColumns.CONCRETE_ID
341        };
342
343        public static final int RAW_CONTACT_ID = 0;
344        public static final int DATA_ID = 1;
345        public static final int CONTACT_ID = 2;
346    }
347
348    interface RawContactsQuery {
349        String TABLE = Tables.RAW_CONTACTS;
350
351        String[] COLUMNS = new String[] {
352                RawContacts.DELETED,
353                RawContacts.ACCOUNT_TYPE,
354                RawContacts.ACCOUNT_NAME,
355        };
356
357        int DELETED = 0;
358        int ACCOUNT_TYPE = 1;
359        int ACCOUNT_NAME = 2;
360    }
361
362    public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
363
364    /** Sql where statement for filtering on groups. */
365    private static final String CONTACTS_IN_GROUP_SELECT =
366            Contacts._ID + " IN "
367                    + "(SELECT " + RawContacts.CONTACT_ID
368                    + " FROM " + Tables.RAW_CONTACTS
369                    + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
370                            + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
371                            + " FROM " + Tables.DATA_JOIN_MIMETYPES
372                            + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
373                                    + "' AND " + GroupMembership.GROUP_ROW_ID + "="
374                                    + "(SELECT " + Tables.GROUPS + "." + Groups._ID
375                                    + " FROM " + Tables.GROUPS
376                                    + " WHERE " + Groups.TITLE + "=?)))";
377
378    /** Sql for updating DIRTY flag on multiple raw contacts */
379    private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
380            "UPDATE " + Tables.RAW_CONTACTS +
381            " SET " + RawContacts.DIRTY + "=1" +
382            " WHERE " + RawContacts._ID + " IN (";
383
384    /** Sql for updating VERSION on multiple raw contacts */
385    private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
386            "UPDATE " + Tables.RAW_CONTACTS +
387            " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
388            " WHERE " + RawContacts._ID + " IN (";
389
390    // Current contacts - those contacted within the last 3 days (in seconds)
391    private static final long EMAIL_FILTER_CURRENT = 3 * 24 * 60 * 60;
392
393    // Recent contacts - those contacted within the last 30 days (in seconds)
394    private static final long EMAIL_FILTER_RECENT = 30 * 24 * 60 * 60;
395
396    private static final String TIME_SINCE_LAST_CONTACTED =
397            "(strftime('%s', 'now') - " + Contacts.LAST_TIME_CONTACTED + "/1000)";
398
399    /*
400     * Sorting order for email address suggestions: first starred, then the rest.
401     * Within the starred/unstarred groups - three buckets: very recently contacted, then fairly
402     * recently contacted, then the rest.  Within each of the bucket - descending count
403     * of times contacted. If all else fails, alphabetical.  (Super)primary email
404     * address is returned before other addresses for the same contact.
405     */
406    private static final String EMAIL_FILTER_SORT_ORDER =
407            "(CASE WHEN " + Contacts.STARRED + "=1 THEN 0 ELSE 1 END), "
408            + "(CASE WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + EMAIL_FILTER_CURRENT + " THEN 0 "
409            + " WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + EMAIL_FILTER_RECENT + " THEN 1 "
410            + " ELSE 2 END),"
411            + Contacts.TIMES_CONTACTED + " DESC, "
412            + Contacts.DISPLAY_NAME + ", "
413            + Data.CONTACT_ID + ", "
414            + Data.IS_SUPER_PRIMARY + " DESC";
415
416    /** Name lookup types used for contact filtering */
417    private static final String CONTACT_LOOKUP_NAME_TYPES =
418            NameLookupType.NAME_COLLATION_KEY + "," +
419            NameLookupType.EMAIL_BASED_NICKNAME + "," +
420            NameLookupType.NICKNAME + "," +
421            NameLookupType.NAME_SHORTHAND + "," +
422            NameLookupType.ORGANIZATION + "," +
423            NameLookupType.NAME_CONSONANTS;
424
425    /**
426     * If any of these columns are used in a Data projection, there is no point in
427     * using the DISTINCT keyword, which can negatively affect performance.
428     */
429    private static final String[] DISTINCT_DATA_PROHIBITING_COLUMNS = {
430            Data._ID,
431            Data.RAW_CONTACT_ID,
432            Data.NAME_RAW_CONTACT_ID,
433            RawContacts.ACCOUNT_NAME,
434            RawContacts.ACCOUNT_TYPE,
435            RawContacts.DIRTY,
436            RawContacts.NAME_VERIFIED,
437            RawContacts.SOURCE_ID,
438            RawContacts.VERSION,
439    };
440
441    private static final ProjectionMap sContactsColumns = ProjectionMap.builder()
442            .add(Contacts.CUSTOM_RINGTONE)
443            .add(Contacts.DISPLAY_NAME)
444            .add(Contacts.DISPLAY_NAME_ALTERNATIVE)
445            .add(Contacts.DISPLAY_NAME_SOURCE)
446            .add(Contacts.IN_VISIBLE_GROUP)
447            .add(Contacts.LAST_TIME_CONTACTED)
448            .add(Contacts.LOOKUP_KEY)
449            .add(Contacts.PHONETIC_NAME)
450            .add(Contacts.PHONETIC_NAME_STYLE)
451            .add(Contacts.PHOTO_ID)
452            .add(Contacts.PHOTO_URI)
453            .add(Contacts.PHOTO_THUMBNAIL_URI)
454            .add(Contacts.SEND_TO_VOICEMAIL)
455            .add(Contacts.SORT_KEY_ALTERNATIVE)
456            .add(Contacts.SORT_KEY_PRIMARY)
457            .add(Contacts.STARRED)
458            .add(Contacts.TIMES_CONTACTED)
459            .add(Contacts.HAS_PHONE_NUMBER)
460            .build();
461
462    private static final ProjectionMap sContactsPresenceColumns = ProjectionMap.builder()
463            .add(Contacts.CONTACT_PRESENCE,
464                    Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)
465            .add(Contacts.CONTACT_CHAT_CAPABILITY,
466                    Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
467            .add(Contacts.CONTACT_STATUS,
468                    ContactsStatusUpdatesColumns.CONCRETE_STATUS)
469            .add(Contacts.CONTACT_STATUS_TIMESTAMP,
470                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
471            .add(Contacts.CONTACT_STATUS_RES_PACKAGE,
472                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
473            .add(Contacts.CONTACT_STATUS_LABEL,
474                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
475            .add(Contacts.CONTACT_STATUS_ICON,
476                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
477            .build();
478
479    private static final ProjectionMap sSnippetColumns = ProjectionMap.builder()
480            .add(SearchSnippetColumns.SNIPPET)
481            .build();
482
483    private static final ProjectionMap sRawContactColumns = ProjectionMap.builder()
484            .add(RawContacts.ACCOUNT_NAME)
485            .add(RawContacts.ACCOUNT_TYPE)
486            .add(RawContacts.DIRTY)
487            .add(RawContacts.NAME_VERIFIED)
488            .add(RawContacts.SOURCE_ID)
489            .add(RawContacts.VERSION)
490            .build();
491
492    private static final ProjectionMap sRawContactSyncColumns = ProjectionMap.builder()
493            .add(RawContacts.SYNC1)
494            .add(RawContacts.SYNC2)
495            .add(RawContacts.SYNC3)
496            .add(RawContacts.SYNC4)
497            .build();
498
499    private static final ProjectionMap sDataColumns = ProjectionMap.builder()
500            .add(Data.DATA1)
501            .add(Data.DATA2)
502            .add(Data.DATA3)
503            .add(Data.DATA4)
504            .add(Data.DATA5)
505            .add(Data.DATA6)
506            .add(Data.DATA7)
507            .add(Data.DATA8)
508            .add(Data.DATA9)
509            .add(Data.DATA10)
510            .add(Data.DATA11)
511            .add(Data.DATA12)
512            .add(Data.DATA13)
513            .add(Data.DATA14)
514            .add(Data.DATA15)
515            .add(Data.DATA_VERSION)
516            .add(Data.IS_PRIMARY)
517            .add(Data.IS_SUPER_PRIMARY)
518            .add(Data.MIMETYPE)
519            .add(Data.RES_PACKAGE)
520            .add(Data.SYNC1)
521            .add(Data.SYNC2)
522            .add(Data.SYNC3)
523            .add(Data.SYNC4)
524            .add(GroupMembership.GROUP_SOURCE_ID)
525            .build();
526
527    private static final ProjectionMap sContactPresenceColumns = ProjectionMap.builder()
528            .add(Contacts.CONTACT_PRESENCE,
529                    Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.PRESENCE)
530            .add(Contacts.CONTACT_CHAT_CAPABILITY,
531                    Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.CHAT_CAPABILITY)
532            .add(Contacts.CONTACT_STATUS,
533                    ContactsStatusUpdatesColumns.CONCRETE_STATUS)
534            .add(Contacts.CONTACT_STATUS_TIMESTAMP,
535                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
536            .add(Contacts.CONTACT_STATUS_RES_PACKAGE,
537                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
538            .add(Contacts.CONTACT_STATUS_LABEL,
539                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
540            .add(Contacts.CONTACT_STATUS_ICON,
541                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
542            .build();
543
544    private static final ProjectionMap sDataPresenceColumns = ProjectionMap.builder()
545            .add(Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE)
546            .add(Data.CHAT_CAPABILITY, Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
547            .add(Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS)
548            .add(Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
549            .add(Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
550            .add(Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL)
551            .add(Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)
552            .build();
553
554    /** Contains just BaseColumns._COUNT */
555    private static final ProjectionMap sCountProjectionMap = ProjectionMap.builder()
556            .add(BaseColumns._COUNT, "COUNT(*)")
557            .build();
558
559    /** Contains just the contacts columns */
560    private static final ProjectionMap sContactsProjectionMap = ProjectionMap.builder()
561            .add(Contacts._ID)
562            .add(Contacts.HAS_PHONE_NUMBER)
563            .add(Contacts.NAME_RAW_CONTACT_ID)
564            .addAll(sContactsColumns)
565            .addAll(sContactsPresenceColumns)
566            .build();
567
568    /** Contains just the contacts columns */
569    private static final ProjectionMap sContactsProjectionWithSnippetMap = ProjectionMap.builder()
570            .addAll(sContactsProjectionMap)
571            .addAll(sSnippetColumns)
572            .build();
573
574    /** Used for pushing starred contacts to the top of a times contacted list **/
575    private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder()
576            .addAll(sContactsProjectionMap)
577            .add(TIMES_CONTACTED_SORT_COLUMN, String.valueOf(Long.MAX_VALUE))
578            .build();
579
580    private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder()
581            .addAll(sContactsProjectionMap)
582            .add(TIMES_CONTACTED_SORT_COLUMN, Contacts.TIMES_CONTACTED)
583            .build();
584
585    /** Contains just the contacts vCard columns */
586    private static final ProjectionMap sContactsVCardProjectionMap = ProjectionMap.builder()
587            .add(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf'")
588            .add(OpenableColumns.SIZE, "NULL")
589            .build();
590
591    /** Contains just the raw contacts columns */
592    private static final ProjectionMap sRawContactsProjectionMap = ProjectionMap.builder()
593            .add(RawContacts._ID)
594            .add(RawContacts.CONTACT_ID)
595            .add(RawContacts.DELETED)
596            .add(RawContacts.DISPLAY_NAME_PRIMARY)
597            .add(RawContacts.DISPLAY_NAME_ALTERNATIVE)
598            .add(RawContacts.DISPLAY_NAME_SOURCE)
599            .add(RawContacts.PHONETIC_NAME)
600            .add(RawContacts.PHONETIC_NAME_STYLE)
601            .add(RawContacts.SORT_KEY_PRIMARY)
602            .add(RawContacts.SORT_KEY_ALTERNATIVE)
603            .add(RawContacts.TIMES_CONTACTED)
604            .add(RawContacts.LAST_TIME_CONTACTED)
605            .add(RawContacts.CUSTOM_RINGTONE)
606            .add(RawContacts.SEND_TO_VOICEMAIL)
607            .add(RawContacts.STARRED)
608            .add(RawContacts.AGGREGATION_MODE)
609            .addAll(sRawContactColumns)
610            .addAll(sRawContactSyncColumns)
611            .build();
612
613    /** Contains the columns from the raw entity view*/
614    private static final ProjectionMap sRawEntityProjectionMap = ProjectionMap.builder()
615            .add(RawContacts._ID)
616            .add(RawContacts.CONTACT_ID)
617            .add(RawContacts.Entity.DATA_ID)
618            .add(RawContacts.IS_RESTRICTED)
619            .add(RawContacts.DELETED)
620            .add(RawContacts.STARRED)
621            .addAll(sRawContactColumns)
622            .addAll(sRawContactSyncColumns)
623            .addAll(sDataColumns)
624            .build();
625
626    /** Contains the columns from the contact entity view*/
627    private static final ProjectionMap sEntityProjectionMap = ProjectionMap.builder()
628            .add(Contacts.Entity._ID)
629            .add(Contacts.Entity.CONTACT_ID)
630            .add(Contacts.Entity.RAW_CONTACT_ID)
631            .add(Contacts.Entity.DATA_ID)
632            .add(Contacts.Entity.NAME_RAW_CONTACT_ID)
633            .add(Contacts.Entity.DELETED)
634            .add(Contacts.Entity.IS_RESTRICTED)
635            .addAll(sContactsColumns)
636            .addAll(sContactPresenceColumns)
637            .addAll(sRawContactColumns)
638            .addAll(sRawContactSyncColumns)
639            .addAll(sDataColumns)
640            .addAll(sDataPresenceColumns)
641            .build();
642
643    /** Contains columns from the data view */
644    private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder()
645            .add(Data._ID)
646            .add(Data.RAW_CONTACT_ID)
647            .add(Data.CONTACT_ID)
648            .add(Data.NAME_RAW_CONTACT_ID)
649            .addAll(sDataColumns)
650            .addAll(sDataPresenceColumns)
651            .addAll(sRawContactColumns)
652            .addAll(sContactsColumns)
653            .addAll(sContactPresenceColumns)
654            .build();
655
656    /** Contains columns from the data view */
657    private static final ProjectionMap sDistinctDataProjectionMap = ProjectionMap.builder()
658            .add(Data._ID, "MIN(" + Data._ID + ")")
659            .add(RawContacts.CONTACT_ID)
660            .addAll(sDataColumns)
661            .addAll(sDataPresenceColumns)
662            .addAll(sContactsColumns)
663            .addAll(sContactPresenceColumns)
664            .build();
665
666    /** Contains the data and contacts columns, for joined tables */
667    private static final ProjectionMap sPhoneLookupProjectionMap = ProjectionMap.builder()
668            .add(PhoneLookup._ID, "contacts_view." + Contacts._ID)
669            .add(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY)
670            .add(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME)
671            .add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED)
672            .add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED)
673            .add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED)
674            .add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP)
675            .add(PhoneLookup.PHOTO_ID, "contacts_view." + Contacts.PHOTO_ID)
676            .add(PhoneLookup.PHOTO_URI, "contacts_view." + Contacts.PHOTO_URI)
677            .add(PhoneLookup.PHOTO_THUMBNAIL_URI, "contacts_view." + Contacts.PHOTO_THUMBNAIL_URI)
678            .add(PhoneLookup.CUSTOM_RINGTONE, "contacts_view." + Contacts.CUSTOM_RINGTONE)
679            .add(PhoneLookup.HAS_PHONE_NUMBER, "contacts_view." + Contacts.HAS_PHONE_NUMBER)
680            .add(PhoneLookup.SEND_TO_VOICEMAIL, "contacts_view." + Contacts.SEND_TO_VOICEMAIL)
681            .add(PhoneLookup.NUMBER, Phone.NUMBER)
682            .add(PhoneLookup.TYPE, Phone.TYPE)
683            .add(PhoneLookup.LABEL, Phone.LABEL)
684            .add(PhoneLookup.NORMALIZED_NUMBER, Phone.NORMALIZED_NUMBER)
685            .build();
686
687    /** Contains the just the {@link Groups} columns */
688    private static final ProjectionMap sGroupsProjectionMap = ProjectionMap.builder()
689            .add(Groups._ID)
690            .add(Groups.ACCOUNT_NAME)
691            .add(Groups.ACCOUNT_TYPE)
692            .add(Groups.SOURCE_ID)
693            .add(Groups.DIRTY)
694            .add(Groups.VERSION)
695            .add(Groups.RES_PACKAGE)
696            .add(Groups.TITLE)
697            .add(Groups.TITLE_RES)
698            .add(Groups.GROUP_VISIBLE)
699            .add(Groups.SYSTEM_ID)
700            .add(Groups.DELETED)
701            .add(Groups.NOTES)
702            .add(Groups.SHOULD_SYNC)
703            .add(Groups.FAVORITES)
704            .add(Groups.AUTO_ADD)
705            .add(Groups.GROUP_IS_READ_ONLY)
706            .add(Groups.SYNC1)
707            .add(Groups.SYNC2)
708            .add(Groups.SYNC3)
709            .add(Groups.SYNC4)
710            .build();
711
712    /** Contains {@link Groups} columns along with summary details */
713    private static final ProjectionMap sGroupsSummaryProjectionMap = ProjectionMap.builder()
714            .addAll(sGroupsProjectionMap)
715            .add(Groups.SUMMARY_COUNT,
716                    "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
717                    + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS
718                    + " WHERE " + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP
719                    + " AND " + Clauses.BELONGS_TO_GROUP
720                    + ")")
721            .add(Groups.SUMMARY_WITH_PHONES,
722                    "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
723                    + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS
724                    + " WHERE " + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP
725                    + " AND " + Clauses.BELONGS_TO_GROUP
726                    + " AND " + Contacts.HAS_PHONE_NUMBER + ")")
727            .build();
728
729    /** Contains the agg_exceptions columns */
730    private static final ProjectionMap sAggregationExceptionsProjectionMap = ProjectionMap.builder()
731            .add(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id")
732            .add(AggregationExceptions.TYPE)
733            .add(AggregationExceptions.RAW_CONTACT_ID1)
734            .add(AggregationExceptions.RAW_CONTACT_ID2)
735            .build();
736
737    /** Contains the agg_exceptions columns */
738    private static final ProjectionMap sSettingsProjectionMap = ProjectionMap.builder()
739            .add(Settings.ACCOUNT_NAME)
740            .add(Settings.ACCOUNT_TYPE)
741            .add(Settings.UNGROUPED_VISIBLE)
742            .add(Settings.SHOULD_SYNC)
743            .add(Settings.ANY_UNSYNCED,
744                    "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
745                        + ",(SELECT "
746                                + "(CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL"
747                                + " THEN 1"
748                                + " ELSE MIN(" + Groups.SHOULD_SYNC + ")"
749                                + " END)"
750                            + " FROM " + Tables.GROUPS
751                            + " WHERE " + GroupsColumns.CONCRETE_ACCOUNT_NAME + "="
752                                    + SettingsColumns.CONCRETE_ACCOUNT_NAME
753                                + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
754                                    + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0"
755                    + " THEN 1"
756                    + " ELSE 0"
757                    + " END)")
758            .add(Settings.UNGROUPED_COUNT,
759                    "(SELECT COUNT(*)"
760                    + " FROM (SELECT 1"
761                            + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
762                            + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
763                            + " HAVING " + Clauses.HAVING_NO_GROUPS
764                    + "))")
765            .add(Settings.UNGROUPED_WITH_PHONES,
766                    "(SELECT COUNT(*)"
767                    + " FROM (SELECT 1"
768                            + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
769                            + " WHERE " + Contacts.HAS_PHONE_NUMBER
770                            + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
771                            + " HAVING " + Clauses.HAVING_NO_GROUPS
772                    + "))")
773            .build();
774
775    /** Contains StatusUpdates columns */
776    private static final ProjectionMap sStatusUpdatesProjectionMap = ProjectionMap.builder()
777            .add(PresenceColumns.RAW_CONTACT_ID)
778            .add(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID)
779            .add(StatusUpdates.IM_ACCOUNT)
780            .add(StatusUpdates.IM_HANDLE)
781            .add(StatusUpdates.PROTOCOL)
782            // We cannot allow a null in the custom protocol field, because SQLite3 does not
783            // properly enforce uniqueness of null values
784            .add(StatusUpdates.CUSTOM_PROTOCOL,
785                    "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL + "=''"
786                    + " THEN NULL"
787                    + " ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END)")
788            .add(StatusUpdates.PRESENCE)
789            .add(StatusUpdates.CHAT_CAPABILITY)
790            .add(StatusUpdates.STATUS)
791            .add(StatusUpdates.STATUS_TIMESTAMP)
792            .add(StatusUpdates.STATUS_RES_PACKAGE)
793            .add(StatusUpdates.STATUS_ICON)
794            .add(StatusUpdates.STATUS_LABEL)
795            .build();
796
797    /** Contains Live Folders columns */
798    private static final ProjectionMap sLiveFoldersProjectionMap = ProjectionMap.builder()
799            .add(LiveFolders._ID, Contacts._ID)
800            .add(LiveFolders.NAME, Contacts.DISPLAY_NAME)
801            // TODO: Put contact photo back when we have a way to display a default icon
802            // for contacts without a photo
803            // .add(LiveFolders.ICON_BITMAP, Photos.DATA)
804            .build();
805
806    /** Contains {@link Directory} columns */
807    private static final ProjectionMap sDirectoryProjectionMap = ProjectionMap.builder()
808            .add(Directory._ID)
809            .add(Directory.PACKAGE_NAME)
810            .add(Directory.TYPE_RESOURCE_ID)
811            .add(Directory.DISPLAY_NAME)
812            .add(Directory.DIRECTORY_AUTHORITY)
813            .add(Directory.ACCOUNT_TYPE)
814            .add(Directory.ACCOUNT_NAME)
815            .add(Directory.EXPORT_SUPPORT)
816            .add(Directory.SHORTCUT_SUPPORT)
817            .add(Directory.PHOTO_SUPPORT)
818            .build();
819
820    // where clause to update the status_updates table
821    private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
822            StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
823            " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
824            " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
825
826    private static final String[] EMPTY_STRING_ARRAY = new String[0];
827
828    /**
829     * Notification ID for failure to import contacts.
830     */
831    private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1;
832
833    private static final String DEFAULT_SNIPPET_ARG_START_MATCH = "[";
834    private static final String DEFAULT_SNIPPET_ARG_END_MATCH = "]";
835    private static final String DEFAULT_SNIPPET_ARG_ELLIPSIS = "...";
836    private static final int DEFAULT_SNIPPET_ARG_MAX_TOKENS = -10;
837
838    private boolean sIsPhoneInitialized;
839    private boolean sIsPhone;
840
841    private StringBuilder mSb = new StringBuilder();
842    private String[] mSelectionArgs1 = new String[1];
843    private String[] mSelectionArgs2 = new String[2];
844    private ArrayList<String> mSelectionArgs = Lists.newArrayList();
845
846    private Account mAccount;
847
848    static {
849        // Contacts URI matching table
850        final UriMatcher matcher = sUriMatcher;
851        matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
852        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
853        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_ID_DATA);
854        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/entities", CONTACTS_ID_ENTITIES);
855        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
856                AGGREGATION_SUGGESTIONS);
857        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
858                AGGREGATION_SUGGESTIONS);
859        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO);
860        matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER);
861        matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
862        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
863        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA);
864        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
865        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data",
866                CONTACTS_LOOKUP_ID_DATA);
867        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/entities",
868                CONTACTS_LOOKUP_ENTITIES);
869        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/entities",
870                CONTACTS_LOOKUP_ID_ENTITIES);
871        matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
872        matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
873                CONTACTS_AS_MULTI_VCARD);
874        matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT);
875        matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*",
876                CONTACTS_STREQUENT_FILTER);
877        matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP);
878
879        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
880        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
881        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
882        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID);
883
884        matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
885
886        matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
887        matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
888        matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
889        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
890        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
891        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
892        matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
893        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
894        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup", EMAILS_LOOKUP);
895        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
896        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
897        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
898        matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
899        matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
900
901        matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
902        matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
903        matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
904
905        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
906        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#",
907                SYNCSTATE_ID);
908
909        matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
910        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
911                AGGREGATION_EXCEPTIONS);
912        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
913                AGGREGATION_EXCEPTION_ID);
914
915        matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);
916
917        matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
918        matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID);
919
920        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
921                SEARCH_SUGGESTIONS);
922        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
923                SEARCH_SUGGESTIONS);
924        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
925                SEARCH_SHORTCUT);
926
927        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts",
928                LIVE_FOLDERS_CONTACTS);
929        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*",
930                LIVE_FOLDERS_CONTACTS_GROUP_NAME);
931        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones",
932                LIVE_FOLDERS_CONTACTS_WITH_PHONES);
933        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites",
934                LIVE_FOLDERS_CONTACTS_FAVORITES);
935
936        matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
937
938        matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES);
939        matcher.addURI(ContactsContract.AUTHORITY, "directories/#", DIRECTORIES_ID);
940
941        matcher.addURI(ContactsContract.AUTHORITY, "complete_name", COMPLETE_NAME);
942    }
943
944    private static class DirectoryInfo {
945        String authority;
946        String accountName;
947        String accountType;
948    }
949
950    /**
951     * Cached information about contact directories.
952     */
953    private HashMap<String, DirectoryInfo> mDirectoryCache = new HashMap<String, DirectoryInfo>();
954    private boolean mDirectoryCacheValid = false;
955
956    /**
957     * An entry in group id cache. It maps the combination of (account type, account name
958     * and source id) to group row id.
959     */
960    public static class GroupIdCacheEntry {
961        String accountType;
962        String accountName;
963        String sourceId;
964        long groupId;
965    }
966
967    // We don't need a soft cache for groups - the assumption is that there will only
968    // be a small number of contact groups. The cache is keyed off source id.  The value
969    // is a list of groups with this group id.
970    private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
971
972    private HashMap<String, DataRowHandler> mDataRowHandlers;
973    private ContactsDatabaseHelper mDbHelper;
974
975    private NameSplitter mNameSplitter;
976    private NameLookupBuilder mNameLookupBuilder;
977
978    private PostalSplitter mPostalSplitter;
979
980    private ContactDirectoryManager mContactDirectoryManager;
981    private ContactAggregator mContactAggregator;
982    private LegacyApiSupport mLegacyApiSupport;
983    private GlobalSearchSupport mGlobalSearchSupport;
984    private CommonNicknameCache mCommonNicknameCache;
985    private SearchIndexManager mSearchIndexManager;
986
987    private ContentValues mValues = new ContentValues();
988    private HashMap<String, Boolean> mAccountWritability = Maps.newHashMap();
989
990    private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
991    private boolean mProviderStatusUpdateNeeded;
992    private long mEstimatedStorageRequirement = 0;
993    private volatile CountDownLatch mReadAccessLatch;
994    private volatile CountDownLatch mWriteAccessLatch;
995    private boolean mAccountUpdateListenerRegistered;
996    private boolean mOkToOpenAccess = true;
997
998    private TransactionContext mTransactionContext = new TransactionContext();
999
1000    private boolean mVisibleTouched = false;
1001
1002    private boolean mSyncToNetwork;
1003
1004    private Locale mCurrentLocale;
1005    private int mContactsAccountCount;
1006
1007    private HandlerThread mBackgroundThread;
1008    private Handler mBackgroundHandler;
1009
1010    @Override
1011    public boolean onCreate() {
1012        super.onCreate();
1013        try {
1014            return initialize();
1015        } catch (RuntimeException e) {
1016            Log.e(TAG, "Cannot start provider", e);
1017            return false;
1018        }
1019    }
1020
1021    private boolean initialize() {
1022        StrictMode.setThreadPolicy(
1023                new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
1024
1025        mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
1026        mContactDirectoryManager = new ContactDirectoryManager(this);
1027        mGlobalSearchSupport = new GlobalSearchSupport(this);
1028
1029        // The provider is closed for business until fully initialized
1030        mReadAccessLatch = new CountDownLatch(1);
1031        mWriteAccessLatch = new CountDownLatch(1);
1032
1033        mBackgroundThread = new HandlerThread("ContactsProviderWorker",
1034                Process.THREAD_PRIORITY_BACKGROUND);
1035        mBackgroundThread.start();
1036        mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
1037            @Override
1038            public void handleMessage(Message msg) {
1039                performBackgroundTask(msg.what, msg.obj);
1040            }
1041        };
1042
1043        scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE);
1044        scheduleBackgroundTask(BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS);
1045        scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
1046        scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_LOCALE);
1047        scheduleBackgroundTask(BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM);
1048        scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_SEARCH_INDEX);
1049        scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_PROVIDER_STATUS);
1050        scheduleBackgroundTask(BACKGROUND_TASK_OPEN_WRITE_ACCESS);
1051
1052        return true;
1053    }
1054
1055    /**
1056     * (Re)allocates all locale-sensitive structures.
1057     */
1058    private void initForDefaultLocale() {
1059        Context context = getContext();
1060        mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
1061        mCurrentLocale = getLocale();
1062        mNameSplitter = mDbHelper.createNameSplitter();
1063        mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
1064        mPostalSplitter = new PostalSplitter(mCurrentLocale);
1065        mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase());
1066        ContactLocaleUtils.getIntance().setLocale(mCurrentLocale);
1067        mContactAggregator = new ContactAggregator(this, mDbHelper,
1068                createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
1069        mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
1070        mSearchIndexManager = new SearchIndexManager(this);
1071
1072        mDataRowHandlers = new HashMap<String, DataRowHandler>();
1073
1074        mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE,
1075                new DataRowHandlerForEmail(context, mDbHelper, mContactAggregator));
1076        mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
1077                new DataRowHandlerForIm(context, mDbHelper, mContactAggregator));
1078        mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE,
1079                new DataRowHandlerForOrganization(context, mDbHelper, mContactAggregator));
1080        mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE,
1081                new DataRowHandlerForPhoneNumber(context, mDbHelper, mContactAggregator));
1082        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE,
1083                new DataRowHandlerForNickname(context, mDbHelper, mContactAggregator));
1084        mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
1085                new DataRowHandlerForStructuredName(context, mDbHelper, mContactAggregator,
1086                        mNameSplitter, mNameLookupBuilder));
1087        mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE,
1088                new DataRowHandlerForStructuredPostal(context, mDbHelper, mContactAggregator,
1089                        mPostalSplitter));
1090        mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE,
1091                new DataRowHandlerForGroupMembership(context, mDbHelper, mContactAggregator,
1092                        mGroupIdCache));
1093        mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE,
1094                new DataRowHandlerForPhoto(context, mDbHelper, mContactAggregator));
1095        mDataRowHandlers.put(Note.CONTENT_ITEM_TYPE,
1096                new DataRowHandlerForNote(context, mDbHelper, mContactAggregator));
1097    }
1098
1099    /**
1100     * Visible for testing.
1101     */
1102    /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
1103        return new PhotoPriorityResolver(context);
1104    }
1105
1106    protected void scheduleBackgroundTask(int task) {
1107        mBackgroundHandler.sendEmptyMessage(task);
1108    }
1109
1110    protected void scheduleBackgroundTask(int task, Object arg) {
1111        mBackgroundHandler.sendMessage(mBackgroundHandler.obtainMessage(task, arg));
1112    }
1113
1114    protected void performBackgroundTask(int task, Object arg) {
1115        switch (task) {
1116            case BACKGROUND_TASK_INITIALIZE: {
1117                initForDefaultLocale();
1118                mReadAccessLatch.countDown();
1119                mReadAccessLatch = null;
1120                break;
1121            }
1122
1123            case BACKGROUND_TASK_OPEN_WRITE_ACCESS: {
1124                if (mOkToOpenAccess) {
1125                    mWriteAccessLatch.countDown();
1126                    mWriteAccessLatch = null;
1127                }
1128                break;
1129            }
1130
1131            case BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS: {
1132                if (isLegacyContactImportNeeded()) {
1133                    importLegacyContactsInBackground();
1134                }
1135                break;
1136            }
1137
1138            case BACKGROUND_TASK_UPDATE_ACCOUNTS: {
1139                Context context = getContext();
1140                if (!mAccountUpdateListenerRegistered) {
1141                    AccountManager.get(context).addOnAccountsUpdatedListener(this, null, false);
1142                    mAccountUpdateListenerRegistered = true;
1143                }
1144
1145                Account[] accounts = AccountManager.get(context).getAccounts();
1146                boolean accountsChanged = updateAccountsInBackground(accounts);
1147                updateContactsAccountCount(accounts);
1148                updateDirectoriesInBackground(accountsChanged);
1149                break;
1150            }
1151
1152            case BACKGROUND_TASK_UPDATE_LOCALE: {
1153                updateLocaleInBackground();
1154                break;
1155            }
1156
1157            case BACKGROUND_TASK_CHANGE_LOCALE: {
1158                changeLocaleInBackground();
1159                break;
1160            }
1161
1162            case BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM: {
1163                if (isAggregationUpgradeNeeded()) {
1164                    upgradeAggregationAlgorithmInBackground();
1165                }
1166                break;
1167            }
1168
1169            case BACKGROUND_TASK_UPDATE_SEARCH_INDEX: {
1170                updateSearchIndexInBackground();
1171                break;
1172            }
1173
1174            case BACKGROUND_TASK_UPDATE_PROVIDER_STATUS: {
1175                updateProviderStatus();
1176                break;
1177            }
1178
1179            case BACKGROUND_TASK_UPDATE_DIRECTORIES: {
1180                if (arg != null) {
1181                    mContactDirectoryManager.onPackageChanged((String) arg);
1182                }
1183                break;
1184            }
1185        }
1186    }
1187
1188    public void onLocaleChanged() {
1189        if (mProviderStatus != ProviderStatus.STATUS_NORMAL
1190                && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
1191            return;
1192        }
1193
1194        scheduleBackgroundTask(BACKGROUND_TASK_CHANGE_LOCALE);
1195    }
1196
1197    /**
1198     * Verifies that the contacts database is properly configured for the current locale.
1199     * If not, changes the database locale to the current locale using an asynchronous task.
1200     * This needs to be done asynchronously because the process involves rebuilding
1201     * large data structures (name lookup, sort keys), which can take minutes on
1202     * a large set of contacts.
1203     */
1204    protected void updateLocaleInBackground() {
1205
1206        // The process is already running - postpone the change
1207        if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) {
1208            return;
1209        }
1210
1211        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1212        final String providerLocale = prefs.getString(PREF_LOCALE, null);
1213        final Locale currentLocale = mCurrentLocale;
1214        if (currentLocale.toString().equals(providerLocale)) {
1215            return;
1216        }
1217
1218        int providerStatus = mProviderStatus;
1219        setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
1220        mDbHelper.setLocale(this, currentLocale);
1221        prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply();
1222        setProviderStatus(providerStatus);
1223    }
1224
1225    /**
1226     * Reinitializes the provider for a new locale.
1227     */
1228    private void changeLocaleInBackground() {
1229        // Re-initializing the provider without stopping it.
1230        // Locking the database will prevent inserts/updates/deletes from
1231        // running at the same time, but queries may still be running
1232        // on other threads. Those queries may return inconsistent results.
1233        SQLiteDatabase db = mDbHelper.getWritableDatabase();
1234        db.beginTransaction();
1235        try {
1236            initForDefaultLocale();
1237            db.setTransactionSuccessful();
1238        } finally {
1239            db.endTransaction();
1240        }
1241
1242        updateLocaleInBackground();
1243    }
1244
1245    protected void updateSearchIndexInBackground() {
1246        mSearchIndexManager.updateIndex();
1247    }
1248
1249    protected void updateDirectoriesInBackground(boolean rescan) {
1250        mContactDirectoryManager.scanAllPackages(rescan);
1251    }
1252
1253    private void updateProviderStatus() {
1254        if (mProviderStatus != ProviderStatus.STATUS_NORMAL
1255                && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
1256            return;
1257        }
1258
1259        if (mContactsAccountCount == 0
1260                && DatabaseUtils.queryNumEntries(mDbHelper.getReadableDatabase(),
1261                        Tables.CONTACTS, null) == 0) {
1262            setProviderStatus(ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS);
1263        } else {
1264            setProviderStatus(ProviderStatus.STATUS_NORMAL);
1265        }
1266    }
1267
1268    /* Visible for testing */
1269    @Override
1270    protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
1271        return ContactsDatabaseHelper.getInstance(context);
1272    }
1273
1274    /* package */ NameSplitter getNameSplitter() {
1275        return mNameSplitter;
1276    }
1277
1278    /* package */ NameLookupBuilder getNameLookupBuilder() {
1279        return mNameLookupBuilder;
1280    }
1281
1282    /* Visible for testing */
1283    public ContactDirectoryManager getContactDirectoryManagerForTest() {
1284        return mContactDirectoryManager;
1285    }
1286
1287    /* Visible for testing */
1288    protected Locale getLocale() {
1289        return Locale.getDefault();
1290    }
1291
1292    protected boolean isLegacyContactImportNeeded() {
1293        int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0"));
1294        return version < PROPERTY_CONTACTS_IMPORT_VERSION;
1295    }
1296
1297    protected LegacyContactImporter getLegacyContactImporter() {
1298        return new LegacyContactImporter(getContext(), this);
1299    }
1300
1301    /**
1302     * Imports legacy contacts as a background task.
1303     */
1304    private void importLegacyContactsInBackground() {
1305        Log.v(TAG, "Importing legacy contacts");
1306        setProviderStatus(ProviderStatus.STATUS_UPGRADING);
1307
1308        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1309        mDbHelper.setLocale(this, mCurrentLocale);
1310        prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit();
1311
1312        LegacyContactImporter importer = getLegacyContactImporter();
1313        if (importLegacyContacts(importer)) {
1314            onLegacyContactImportSuccess();
1315        } else {
1316            onLegacyContactImportFailure();
1317        }
1318    }
1319
1320    /**
1321     * Unlocks the provider and declares that the import process is complete.
1322     */
1323    private void onLegacyContactImportSuccess() {
1324        NotificationManager nm =
1325            (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE);
1326        nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION);
1327
1328        // Store a property in the database indicating that the conversion process succeeded
1329        mDbHelper.setProperty(PROPERTY_CONTACTS_IMPORTED,
1330                String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION));
1331        setProviderStatus(ProviderStatus.STATUS_NORMAL);
1332        Log.v(TAG, "Completed import of legacy contacts");
1333    }
1334
1335    /**
1336     * Announces the provider status and keeps the provider locked.
1337     */
1338    private void onLegacyContactImportFailure() {
1339        Context context = getContext();
1340        NotificationManager nm =
1341            (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
1342
1343        // Show a notification
1344        Notification n = new Notification(android.R.drawable.stat_notify_error,
1345                context.getString(R.string.upgrade_out_of_memory_notification_ticker),
1346                System.currentTimeMillis());
1347        n.setLatestEventInfo(context,
1348                context.getString(R.string.upgrade_out_of_memory_notification_title),
1349                context.getString(R.string.upgrade_out_of_memory_notification_text),
1350                PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0));
1351        n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
1352
1353        nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n);
1354
1355        setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY);
1356        Log.v(TAG, "Failed to import legacy contacts");
1357
1358        // Do not let any database changes until this issue is resolved.
1359        mOkToOpenAccess = false;
1360    }
1361
1362    /* Visible for testing */
1363    /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
1364        boolean aggregatorEnabled = mContactAggregator.isEnabled();
1365        mContactAggregator.setEnabled(false);
1366        try {
1367            if (importer.importContacts()) {
1368
1369                // TODO aggregate all newly added raw contacts
1370                mContactAggregator.setEnabled(aggregatorEnabled);
1371                return true;
1372            }
1373        } catch (Throwable e) {
1374           Log.e(TAG, "Legacy contact import failed", e);
1375        }
1376        mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement();
1377        return false;
1378    }
1379
1380    /**
1381     * Wipes all data from the contacts database.
1382     */
1383    /* package */ void wipeData() {
1384        mDbHelper.wipeData();
1385        mProviderStatus = ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS;
1386    }
1387
1388    /**
1389     * During intialization, this content provider will
1390     * block all attempts to change contacts data. In particular, it will hold
1391     * up all contact syncs. As soon as the import process is complete, all
1392     * processes waiting to write to the provider are unblocked and can proceed
1393     * to compete for the database transaction monitor.
1394     */
1395    private void waitForAccess(CountDownLatch latch) {
1396        if (latch == null) {
1397            return;
1398        }
1399
1400        while (true) {
1401            try {
1402                latch.await();
1403                return;
1404            } catch (InterruptedException e) {
1405                Thread.currentThread().interrupt();
1406            }
1407        }
1408    }
1409
1410    @Override
1411    public Uri insert(Uri uri, ContentValues values) {
1412        waitForAccess(mWriteAccessLatch);
1413        return super.insert(uri, values);
1414    }
1415
1416    @Override
1417    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1418        if (mWriteAccessLatch != null) {
1419            // We are stuck trying to upgrade contacts db.  The only update request
1420            // allowed in this case is an update of provider status, which will trigger
1421            // an attempt to upgrade contacts again.
1422            int match = sUriMatcher.match(uri);
1423            if (match == PROVIDER_STATUS) {
1424                Integer newStatus = values.getAsInteger(ProviderStatus.STATUS);
1425                if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) {
1426                    scheduleBackgroundTask(BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS);
1427                    return 1;
1428                } else {
1429                    return 0;
1430                }
1431            }
1432        }
1433        waitForAccess(mWriteAccessLatch);
1434        return super.update(uri, values, selection, selectionArgs);
1435    }
1436
1437    @Override
1438    public int delete(Uri uri, String selection, String[] selectionArgs) {
1439        waitForAccess(mWriteAccessLatch);
1440        return super.delete(uri, selection, selectionArgs);
1441    }
1442
1443    @Override
1444    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
1445            throws OperationApplicationException {
1446        waitForAccess(mWriteAccessLatch);
1447        return super.applyBatch(operations);
1448    }
1449
1450    @Override
1451    public int bulkInsert(Uri uri, ContentValues[] values) {
1452        waitForAccess(mWriteAccessLatch);
1453        return super.bulkInsert(uri, values);
1454    }
1455
1456    @Override
1457    protected void onBeginTransaction() {
1458        if (VERBOSE_LOGGING) {
1459            Log.v(TAG, "onBeginTransaction");
1460        }
1461        super.onBeginTransaction();
1462        mContactAggregator.clearPendingAggregations();
1463        mTransactionContext.clear();
1464    }
1465
1466
1467    @Override
1468    protected void beforeTransactionCommit() {
1469
1470        if (VERBOSE_LOGGING) {
1471            Log.v(TAG, "beforeTransactionCommit");
1472        }
1473        super.beforeTransactionCommit();
1474        flushTransactionalChanges();
1475        mContactAggregator.aggregateInTransaction(mTransactionContext, mDb);
1476        if (mVisibleTouched) {
1477            mVisibleTouched = false;
1478            mDbHelper.updateAllVisible();
1479        }
1480
1481        updateSearchIndexInTransaction();
1482
1483        if (mProviderStatusUpdateNeeded) {
1484            updateProviderStatus();
1485            mProviderStatusUpdateNeeded = false;
1486        }
1487    }
1488
1489    private void updateSearchIndexInTransaction() {
1490        Set<Long> staleContacts = mTransactionContext.getStaleSearchIndexContactIds();
1491        Set<Long> staleRawContacts = mTransactionContext.getStaleSearchIndexRawContactIds();
1492        if (!staleContacts.isEmpty() || !staleRawContacts.isEmpty()) {
1493            mSearchIndexManager.updateIndexForRawContacts(staleContacts, staleRawContacts);
1494            mTransactionContext.clearSearchIndexUpdates();
1495        }
1496    }
1497
1498    private void flushTransactionalChanges() {
1499        if (VERBOSE_LOGGING) {
1500            Log.v(TAG, "flushTransactionChanges");
1501        }
1502
1503        for (long rawContactId : mTransactionContext.getInsertedRawContactIds()) {
1504            mContactAggregator.updateRawContactDisplayName(mDb, rawContactId);
1505            mContactAggregator.onRawContactInsert(mTransactionContext, mDb, rawContactId);
1506        }
1507
1508        Set<Long> dirtyRawContacts = mTransactionContext.getDirtyRawContactIds();
1509        if (!dirtyRawContacts.isEmpty()) {
1510            mSb.setLength(0);
1511            mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
1512            appendIds(mSb, dirtyRawContacts);
1513            mSb.append(")");
1514            mDb.execSQL(mSb.toString());
1515        }
1516
1517        Set<Long> updatedRawContacts = mTransactionContext.getUpdatedRawContactIds();
1518        if (!updatedRawContacts.isEmpty()) {
1519            mSb.setLength(0);
1520            mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
1521            appendIds(mSb, updatedRawContacts);
1522            mSb.append(")");
1523            mDb.execSQL(mSb.toString());
1524        }
1525
1526        for (Map.Entry<Long, Object> entry : mTransactionContext.getUpdatedSyncStates()) {
1527            long id = entry.getKey();
1528            if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) {
1529                throw new IllegalStateException(
1530                        "unable to update sync state, does it still exist?");
1531            }
1532        }
1533
1534        mTransactionContext.clear();
1535    }
1536
1537    /**
1538     * Appends comma separated ids.
1539     * @param ids Should not be empty
1540     */
1541    private void appendIds(StringBuilder sb, Set<Long> ids) {
1542        for (long id : ids) {
1543            sb.append(id).append(',');
1544        }
1545
1546        sb.setLength(sb.length() - 1); // Yank the last comma
1547    }
1548
1549    @Override
1550    protected void notifyChange() {
1551        notifyChange(mSyncToNetwork);
1552        mSyncToNetwork = false;
1553    }
1554
1555    protected void notifyChange(boolean syncToNetwork) {
1556        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
1557                syncToNetwork);
1558    }
1559
1560    protected void setProviderStatus(int status) {
1561        if (mProviderStatus != status) {
1562            mProviderStatus = status;
1563            getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false);
1564        }
1565    }
1566
1567    public DataRowHandler getDataRowHandler(final String mimeType) {
1568        DataRowHandler handler = mDataRowHandlers.get(mimeType);
1569        if (handler == null) {
1570            handler = new DataRowHandlerForCustomMimetype(
1571                    getContext(), mDbHelper, mContactAggregator, mimeType);
1572            mDataRowHandlers.put(mimeType, handler);
1573        }
1574        return handler;
1575    }
1576
1577    @Override
1578    protected Uri insertInTransaction(Uri uri, ContentValues values) {
1579        if (VERBOSE_LOGGING) {
1580            Log.v(TAG, "insertInTransaction: " + uri + " " + values);
1581        }
1582
1583        final boolean callerIsSyncAdapter =
1584                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
1585
1586        final int match = sUriMatcher.match(uri);
1587        long id = 0;
1588
1589        switch (match) {
1590            case SYNCSTATE:
1591                id = mDbHelper.getSyncState().insert(mDb, values);
1592                break;
1593
1594            case CONTACTS: {
1595                insertContact(values);
1596                break;
1597            }
1598
1599            case RAW_CONTACTS: {
1600                id = insertRawContact(uri, values, callerIsSyncAdapter);
1601                mSyncToNetwork |= !callerIsSyncAdapter;
1602                break;
1603            }
1604
1605            case RAW_CONTACTS_DATA: {
1606                values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
1607                id = insertData(values, callerIsSyncAdapter);
1608                mSyncToNetwork |= !callerIsSyncAdapter;
1609                break;
1610            }
1611
1612            case DATA: {
1613                id = insertData(values, callerIsSyncAdapter);
1614                mSyncToNetwork |= !callerIsSyncAdapter;
1615                break;
1616            }
1617
1618            case GROUPS: {
1619                id = insertGroup(uri, values, callerIsSyncAdapter);
1620                mSyncToNetwork |= !callerIsSyncAdapter;
1621                break;
1622            }
1623
1624            case SETTINGS: {
1625                id = insertSettings(uri, values);
1626                mSyncToNetwork |= !callerIsSyncAdapter;
1627                break;
1628            }
1629
1630            case STATUS_UPDATES: {
1631                id = insertStatusUpdate(values);
1632                break;
1633            }
1634
1635            default:
1636                mSyncToNetwork = true;
1637                return mLegacyApiSupport.insert(uri, values);
1638        }
1639
1640        if (id < 0) {
1641            return null;
1642        }
1643
1644        return ContentUris.withAppendedId(uri, id);
1645    }
1646
1647    /**
1648     * If account is non-null then store it in the values. If the account is
1649     * already specified in the values then it must be consistent with the
1650     * account, if it is non-null.
1651     *
1652     * @param uri Current {@link Uri} being operated on.
1653     * @param values {@link ContentValues} to read and possibly update.
1654     * @throws IllegalArgumentException when only one of
1655     *             {@link RawContacts#ACCOUNT_NAME} or
1656     *             {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
1657     *             other undefined.
1658     * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
1659     *             and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
1660     *             the given {@link Uri} and {@link ContentValues}.
1661     */
1662    private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
1663        String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
1664        String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
1665        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
1666
1667        String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
1668        String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
1669        final boolean partialValues = TextUtils.isEmpty(valueAccountName)
1670                ^ TextUtils.isEmpty(valueAccountType);
1671
1672        if (partialUri || partialValues) {
1673            // Throw when either account is incomplete
1674            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
1675                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
1676        }
1677
1678        // Accounts are valid by only checking one parameter, since we've
1679        // already ruled out partial accounts.
1680        final boolean validUri = !TextUtils.isEmpty(accountName);
1681        final boolean validValues = !TextUtils.isEmpty(valueAccountName);
1682
1683        if (validValues && validUri) {
1684            // Check that accounts match when both present
1685            final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
1686                    && TextUtils.equals(accountType, valueAccountType);
1687            if (!accountMatch) {
1688                throw new IllegalArgumentException(mDbHelper.exceptionMessage(
1689                        "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
1690            }
1691        } else if (validUri) {
1692            // Fill values from Uri when not present
1693            values.put(RawContacts.ACCOUNT_NAME, accountName);
1694            values.put(RawContacts.ACCOUNT_TYPE, accountType);
1695        } else if (validValues) {
1696            accountName = valueAccountName;
1697            accountType = valueAccountType;
1698        } else {
1699            return null;
1700        }
1701
1702        // Use cached Account object when matches, otherwise create
1703        if (mAccount == null
1704                || !mAccount.name.equals(accountName)
1705                || !mAccount.type.equals(accountType)) {
1706            mAccount = new Account(accountName, accountType);
1707        }
1708
1709        return mAccount;
1710    }
1711
1712    /**
1713     * Inserts an item in the contacts table
1714     *
1715     * @param values the values for the new row
1716     * @return the row ID of the newly created row
1717     */
1718    private long insertContact(ContentValues values) {
1719        throw new UnsupportedOperationException("Aggregate contacts are created automatically");
1720    }
1721
1722    /**
1723     * Inserts an item in the contacts table
1724     *
1725     * @param uri the values for the new row
1726     * @param values the account this contact should be associated with. may be null.
1727     * @param callerIsSyncAdapter
1728     * @return the row ID of the newly created row
1729     */
1730    private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
1731        mValues.clear();
1732        mValues.putAll(values);
1733        mValues.putNull(RawContacts.CONTACT_ID);
1734
1735        final Account account = resolveAccount(uri, mValues);
1736
1737        if (values.containsKey(RawContacts.DELETED)
1738                && values.getAsInteger(RawContacts.DELETED) != 0) {
1739            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
1740        }
1741
1742        long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues);
1743        int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
1744        if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) {
1745            aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE);
1746        }
1747        mContactAggregator.markNewForAggregation(rawContactId, aggregationMode);
1748
1749        // Trigger creation of a Contact based on this RawContact at the end of transaction
1750        mTransactionContext.rawContactInserted(rawContactId, account);
1751
1752        if (!callerIsSyncAdapter) {
1753            addAutoAddMembership(rawContactId);
1754            final Long starred = values.getAsLong(RawContacts.STARRED);
1755            if (starred != null && starred != 0) {
1756                updateFavoritesMembership(rawContactId, starred != 0);
1757            }
1758        }
1759
1760        mProviderStatusUpdateNeeded = true;
1761        return rawContactId;
1762    }
1763
1764    private void addAutoAddMembership(long rawContactId) {
1765        final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID,
1766                rawContactId);
1767        if (groupId != null) {
1768            insertDataGroupMembership(rawContactId, groupId);
1769        }
1770    }
1771
1772    private Long findGroupByRawContactId(String selection, long rawContactId) {
1773        Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID,
1774                selection,
1775                new String[]{Long.toString(rawContactId)},
1776                null /* groupBy */, null /* having */, null /* orderBy */);
1777        try {
1778            while (c.moveToNext()) {
1779                return c.getLong(0);
1780            }
1781            return null;
1782        } finally {
1783            c.close();
1784        }
1785    }
1786
1787    private void updateFavoritesMembership(long rawContactId, boolean isStarred) {
1788        final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID,
1789                rawContactId);
1790        if (groupId != null) {
1791            if (isStarred) {
1792                insertDataGroupMembership(rawContactId, groupId);
1793            } else {
1794                deleteDataGroupMembership(rawContactId, groupId);
1795            }
1796        }
1797    }
1798
1799    private void insertDataGroupMembership(long rawContactId, long groupId) {
1800        ContentValues groupMembershipValues = new ContentValues();
1801        groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId);
1802        groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
1803        groupMembershipValues.put(DataColumns.MIMETYPE_ID,
1804                mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
1805        mDb.insert(Tables.DATA, null, groupMembershipValues);
1806    }
1807
1808    private void deleteDataGroupMembership(long rawContactId, long groupId) {
1809        final String[] selectionArgs = {
1810                Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)),
1811                Long.toString(groupId),
1812                Long.toString(rawContactId)};
1813        mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs);
1814    }
1815
1816    /**
1817     * Inserts an item in the data table
1818     *
1819     * @param values the values for the new row
1820     * @return the row ID of the newly created row
1821     */
1822    private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
1823        long id = 0;
1824        mValues.clear();
1825        mValues.putAll(values);
1826
1827        long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
1828
1829        // Replace package with internal mapping
1830        final String packageName = mValues.getAsString(Data.RES_PACKAGE);
1831        if (packageName != null) {
1832            mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
1833        }
1834        mValues.remove(Data.RES_PACKAGE);
1835
1836        // Replace mimetype with internal mapping
1837        final String mimeType = mValues.getAsString(Data.MIMETYPE);
1838        if (TextUtils.isEmpty(mimeType)) {
1839            throw new IllegalArgumentException(Data.MIMETYPE + " is required");
1840        }
1841
1842        mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
1843        mValues.remove(Data.MIMETYPE);
1844
1845        DataRowHandler rowHandler = getDataRowHandler(mimeType);
1846        id = rowHandler.insert(mDb, mTransactionContext, rawContactId, mValues);
1847        if (!callerIsSyncAdapter) {
1848            mTransactionContext.markRawContactDirty(rawContactId);
1849        }
1850        mTransactionContext.rawContactUpdated(rawContactId);
1851        return id;
1852    }
1853
1854    public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
1855        mContactAggregator.updateRawContactDisplayName(db, rawContactId);
1856    }
1857
1858    /**
1859     * Delete data row by row so that fixing of primaries etc work correctly.
1860     */
1861    private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
1862        int count = 0;
1863
1864        // Note that the query will return data according to the access restrictions,
1865        // so we don't need to worry about deleting data we don't have permission to read.
1866        Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS,
1867                selection, selectionArgs, null);
1868        try {
1869            while(c.moveToNext()) {
1870                long rawContactId = c.getLong(DataRowHandler.DataDeleteQuery.RAW_CONTACT_ID);
1871                String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE);
1872                DataRowHandler rowHandler = getDataRowHandler(mimeType);
1873                count += rowHandler.delete(mDb, mTransactionContext, c);
1874                if (!callerIsSyncAdapter) {
1875                    mTransactionContext.markRawContactDirty(rawContactId);
1876                }
1877            }
1878        } finally {
1879            c.close();
1880        }
1881
1882        return count;
1883    }
1884
1885    /**
1886     * Delete a data row provided that it is one of the allowed mime types.
1887     */
1888    public int deleteData(long dataId, String[] allowedMimeTypes) {
1889
1890        // Note that the query will return data according to the access restrictions,
1891        // so we don't need to worry about deleting data we don't have permission to read.
1892        mSelectionArgs1[0] = String.valueOf(dataId);
1893        Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, Data._ID + "=?",
1894                mSelectionArgs1, null);
1895
1896        try {
1897            if (!c.moveToFirst()) {
1898                return 0;
1899            }
1900
1901            String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE);
1902            boolean valid = false;
1903            for (int i = 0; i < allowedMimeTypes.length; i++) {
1904                if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
1905                    valid = true;
1906                    break;
1907                }
1908            }
1909
1910            if (!valid) {
1911                throw new IllegalArgumentException("Data type mismatch: expected "
1912                        + Lists.newArrayList(allowedMimeTypes));
1913            }
1914
1915            DataRowHandler rowHandler = getDataRowHandler(mimeType);
1916            return rowHandler.delete(mDb, mTransactionContext, c);
1917        } finally {
1918            c.close();
1919        }
1920    }
1921
1922    /**
1923     * Inserts an item in the groups table
1924     */
1925    private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
1926        mValues.clear();
1927        mValues.putAll(values);
1928
1929        final Account account = resolveAccount(uri, mValues);
1930
1931        // Replace package with internal mapping
1932        final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
1933        if (packageName != null) {
1934            mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
1935        }
1936        mValues.remove(Groups.RES_PACKAGE);
1937
1938        final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null
1939                ? mValues.getAsLong(Groups.FAVORITES) != 0
1940                : false;
1941
1942        if (!callerIsSyncAdapter) {
1943            mValues.put(Groups.DIRTY, 1);
1944        }
1945
1946        long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
1947
1948        if (!callerIsSyncAdapter && isFavoritesGroup) {
1949            // add all starred raw contacts to this group
1950            String selection;
1951            String[] selectionArgs;
1952            if (account == null) {
1953                selection = RawContacts.ACCOUNT_NAME + " IS NULL AND "
1954                        + RawContacts.ACCOUNT_TYPE + " IS NULL";
1955                selectionArgs = null;
1956            } else {
1957                selection = RawContacts.ACCOUNT_NAME + "=? AND "
1958                        + RawContacts.ACCOUNT_TYPE + "=?";
1959                selectionArgs = new String[]{account.name, account.type};
1960            }
1961            Cursor c = mDb.query(Tables.RAW_CONTACTS,
1962                    new String[]{RawContacts._ID, RawContacts.STARRED},
1963                    selection, selectionArgs, null, null, null);
1964            try {
1965                while (c.moveToNext()) {
1966                    if (c.getLong(1) != 0) {
1967                        final long rawContactId = c.getLong(0);
1968                        insertDataGroupMembership(rawContactId, result);
1969                        mTransactionContext.markRawContactDirty(rawContactId);
1970                    }
1971                }
1972            } finally {
1973                c.close();
1974            }
1975        }
1976
1977        if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
1978            mVisibleTouched = true;
1979        }
1980
1981        return result;
1982    }
1983
1984    private long insertSettings(Uri uri, ContentValues values) {
1985        final long id = mDb.insert(Tables.SETTINGS, null, values);
1986
1987        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
1988            mVisibleTouched = true;
1989        }
1990
1991        return id;
1992    }
1993
1994    /**
1995     * Inserts a status update.
1996     */
1997    public long insertStatusUpdate(ContentValues values) {
1998        final String handle = values.getAsString(StatusUpdates.IM_HANDLE);
1999        final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL);
2000        String customProtocol = null;
2001
2002        if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
2003            customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
2004            if (TextUtils.isEmpty(customProtocol)) {
2005                throw new IllegalArgumentException(
2006                        "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
2007            }
2008        }
2009
2010        long rawContactId = -1;
2011        long contactId = -1;
2012        Long dataId = values.getAsLong(StatusUpdates.DATA_ID);
2013        mSb.setLength(0);
2014        mSelectionArgs.clear();
2015        if (dataId != null) {
2016            // Lookup the contact info for the given data row.
2017
2018            mSb.append(Tables.DATA + "." + Data._ID + "=?");
2019            mSelectionArgs.add(String.valueOf(dataId));
2020        } else {
2021            // Lookup the data row to attach this presence update to
2022
2023            if (TextUtils.isEmpty(handle) || protocol == null) {
2024                throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
2025            }
2026
2027            // TODO: generalize to allow other providers to match against email
2028            boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
2029
2030            String mimeTypeIdIm = String.valueOf(mDbHelper.getMimeTypeIdForIm());
2031            if (matchEmail) {
2032                String mimeTypeIdEmail = String.valueOf(mDbHelper.getMimeTypeIdForEmail());
2033
2034                // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
2035                // the "OR" conjunction confuses it and it switches to a full scan of
2036                // the raw_contacts table.
2037
2038                // This code relies on the fact that Im.DATA and Email.DATA are in fact the same
2039                // column - Data.DATA1
2040                mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" +
2041                        " AND " + Data.DATA1 + "=?" +
2042                        " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?");
2043                mSelectionArgs.add(mimeTypeIdEmail);
2044                mSelectionArgs.add(mimeTypeIdIm);
2045                mSelectionArgs.add(handle);
2046                mSelectionArgs.add(mimeTypeIdIm);
2047                mSelectionArgs.add(String.valueOf(protocol));
2048                if (customProtocol != null) {
2049                    mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
2050                    mSelectionArgs.add(customProtocol);
2051                }
2052                mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))");
2053                mSelectionArgs.add(mimeTypeIdEmail);
2054            } else {
2055                mSb.append(DataColumns.MIMETYPE_ID + "=?" +
2056                        " AND " + Im.PROTOCOL + "=?" +
2057                        " AND " + Im.DATA + "=?");
2058                mSelectionArgs.add(mimeTypeIdIm);
2059                mSelectionArgs.add(String.valueOf(protocol));
2060                mSelectionArgs.add(handle);
2061                if (customProtocol != null) {
2062                    mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
2063                    mSelectionArgs.add(customProtocol);
2064                }
2065            }
2066
2067            if (values.containsKey(StatusUpdates.DATA_ID)) {
2068                mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?");
2069                mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID));
2070            }
2071        }
2072        mSb.append(" AND ").append(getContactsRestrictions());
2073
2074        Cursor cursor = null;
2075        try {
2076            cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
2077                    mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
2078                    Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID);
2079            if (cursor.moveToFirst()) {
2080                dataId = cursor.getLong(DataContactsQuery.DATA_ID);
2081                rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
2082                contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
2083            } else {
2084                // No contact found, return a null URI
2085                return -1;
2086            }
2087        } finally {
2088            if (cursor != null) {
2089                cursor.close();
2090            }
2091        }
2092
2093        if (values.containsKey(StatusUpdates.PRESENCE)) {
2094            if (customProtocol == null) {
2095                // We cannot allow a null in the custom protocol field, because SQLite3 does not
2096                // properly enforce uniqueness of null values
2097                customProtocol = "";
2098            }
2099
2100            mValues.clear();
2101            mValues.put(StatusUpdates.DATA_ID, dataId);
2102            mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
2103            mValues.put(PresenceColumns.CONTACT_ID, contactId);
2104            mValues.put(StatusUpdates.PROTOCOL, protocol);
2105            mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
2106            mValues.put(StatusUpdates.IM_HANDLE, handle);
2107            if (values.containsKey(StatusUpdates.IM_ACCOUNT)) {
2108                mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT));
2109            }
2110            mValues.put(StatusUpdates.PRESENCE,
2111                    values.getAsString(StatusUpdates.PRESENCE));
2112            mValues.put(StatusUpdates.CHAT_CAPABILITY,
2113                    values.getAsString(StatusUpdates.CHAT_CAPABILITY));
2114
2115            // Insert the presence update
2116            mDb.replace(Tables.PRESENCE, null, mValues);
2117        }
2118
2119
2120        if (values.containsKey(StatusUpdates.STATUS)) {
2121            String status = values.getAsString(StatusUpdates.STATUS);
2122            String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
2123            Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL);
2124
2125            if (TextUtils.isEmpty(resPackage)
2126                    && (labelResource == null || labelResource == 0)
2127                    && protocol != null) {
2128                labelResource = Im.getProtocolLabelResource(protocol);
2129            }
2130
2131            Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON);
2132            // TODO compute the default icon based on the protocol
2133
2134            if (TextUtils.isEmpty(status)) {
2135                mDbHelper.deleteStatusUpdate(dataId);
2136            } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) {
2137                long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
2138                mDbHelper.replaceStatusUpdate(dataId, timestamp, status, resPackage, iconResource,
2139                        labelResource);
2140            } else {
2141                mDbHelper.insertStatusUpdate(dataId, status, resPackage, iconResource,
2142                        labelResource);
2143            }
2144        }
2145
2146        if (contactId != -1) {
2147            mContactAggregator.updateLastStatusUpdateId(contactId);
2148        }
2149
2150        return dataId;
2151    }
2152
2153    @Override
2154    protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
2155        if (VERBOSE_LOGGING) {
2156            Log.v(TAG, "deleteInTransaction: " + uri);
2157        }
2158        flushTransactionalChanges();
2159        final boolean callerIsSyncAdapter =
2160                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2161        final int match = sUriMatcher.match(uri);
2162        switch (match) {
2163            case SYNCSTATE:
2164                return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
2165
2166            case SYNCSTATE_ID:
2167                String selectionWithId =
2168                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
2169                        + (selection == null ? "" : " AND (" + selection + ")");
2170                return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs);
2171
2172            case CONTACTS: {
2173                // TODO
2174                return 0;
2175            }
2176
2177            case CONTACTS_ID: {
2178                long contactId = ContentUris.parseId(uri);
2179                return deleteContact(contactId, callerIsSyncAdapter);
2180            }
2181
2182            case CONTACTS_LOOKUP: {
2183                final List<String> pathSegments = uri.getPathSegments();
2184                final int segmentCount = pathSegments.size();
2185                if (segmentCount < 3) {
2186                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
2187                            "Missing a lookup key", uri));
2188                }
2189                final String lookupKey = pathSegments.get(2);
2190                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
2191                return deleteContact(contactId, callerIsSyncAdapter);
2192            }
2193
2194            case CONTACTS_LOOKUP_ID: {
2195                // lookup contact by id and lookup key to see if they still match the actual record
2196                final List<String> pathSegments = uri.getPathSegments();
2197                final String lookupKey = pathSegments.get(2);
2198                SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
2199                setTablesAndProjectionMapForContacts(lookupQb, uri, null);
2200                long contactId = ContentUris.parseId(uri);
2201                String[] args;
2202                if (selectionArgs == null) {
2203                    args = new String[2];
2204                } else {
2205                    args = new String[selectionArgs.length + 2];
2206                    System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
2207                }
2208                args[0] = String.valueOf(contactId);
2209                args[1] = Uri.encode(lookupKey);
2210                lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
2211                final SQLiteDatabase db = mDbHelper.getReadableDatabase();
2212                Cursor c = query(db, lookupQb, null, selection, args, null, null, null);
2213                try {
2214                    if (c.getCount() == 1) {
2215                        // contact was unmodified so go ahead and delete it
2216                        return deleteContact(contactId, callerIsSyncAdapter);
2217                    } else {
2218                        // row was changed (e.g. the merging might have changed), we got multiple
2219                        // rows or the supplied selection filtered the record out
2220                        return 0;
2221                    }
2222                } finally {
2223                    c.close();
2224                }
2225            }
2226
2227            case RAW_CONTACTS: {
2228                int numDeletes = 0;
2229                Cursor c = mDb.query(Tables.RAW_CONTACTS,
2230                        new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
2231                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
2232                try {
2233                    while (c.moveToNext()) {
2234                        final long rawContactId = c.getLong(0);
2235                        long contactId = c.getLong(1);
2236                        numDeletes += deleteRawContact(rawContactId, contactId,
2237                                callerIsSyncAdapter);
2238                    }
2239                } finally {
2240                    c.close();
2241                }
2242                return numDeletes;
2243            }
2244
2245            case RAW_CONTACTS_ID: {
2246                final long rawContactId = ContentUris.parseId(uri);
2247                return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId),
2248                        callerIsSyncAdapter);
2249            }
2250
2251            case DATA: {
2252                mSyncToNetwork |= !callerIsSyncAdapter;
2253                return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
2254                        callerIsSyncAdapter);
2255            }
2256
2257            case DATA_ID:
2258            case PHONES_ID:
2259            case EMAILS_ID:
2260            case POSTALS_ID: {
2261                long dataId = ContentUris.parseId(uri);
2262                mSyncToNetwork |= !callerIsSyncAdapter;
2263                mSelectionArgs1[0] = String.valueOf(dataId);
2264                return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
2265            }
2266
2267            case GROUPS_ID: {
2268                mSyncToNetwork |= !callerIsSyncAdapter;
2269                return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
2270            }
2271
2272            case GROUPS: {
2273                int numDeletes = 0;
2274                Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
2275                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
2276                try {
2277                    while (c.moveToNext()) {
2278                        numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
2279                    }
2280                } finally {
2281                    c.close();
2282                }
2283                if (numDeletes > 0) {
2284                    mSyncToNetwork |= !callerIsSyncAdapter;
2285                }
2286                return numDeletes;
2287            }
2288
2289            case SETTINGS: {
2290                mSyncToNetwork |= !callerIsSyncAdapter;
2291                return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs);
2292            }
2293
2294            case STATUS_UPDATES: {
2295                return deleteStatusUpdates(selection, selectionArgs);
2296            }
2297
2298            default: {
2299                mSyncToNetwork = true;
2300                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
2301            }
2302        }
2303    }
2304
2305    public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
2306        mGroupIdCache.clear();
2307        final long groupMembershipMimetypeId = mDbHelper
2308                .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
2309        mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
2310                + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
2311                + groupId, null);
2312
2313        try {
2314            if (callerIsSyncAdapter) {
2315                return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
2316            } else {
2317                mValues.clear();
2318                mValues.put(Groups.DELETED, 1);
2319                mValues.put(Groups.DIRTY, 1);
2320                return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
2321            }
2322        } finally {
2323            mVisibleTouched = true;
2324        }
2325    }
2326
2327    private int deleteSettings(Uri uri, String selection, String[] selectionArgs) {
2328        final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs);
2329        mVisibleTouched = true;
2330        return count;
2331    }
2332
2333    private int deleteContact(long contactId, boolean callerIsSyncAdapter) {
2334        mSelectionArgs1[0] = Long.toString(contactId);
2335        Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
2336                RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
2337                null, null, null);
2338        try {
2339            while (c.moveToNext()) {
2340                long rawContactId = c.getLong(0);
2341                markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
2342            }
2343        } finally {
2344            c.close();
2345        }
2346
2347        mProviderStatusUpdateNeeded = true;
2348
2349        return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
2350    }
2351
2352    public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
2353        mContactAggregator.invalidateAggregationExceptionCache();
2354        mProviderStatusUpdateNeeded = true;
2355
2356        if (callerIsSyncAdapter) {
2357            mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
2358            int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
2359            mContactAggregator.updateDisplayNameForContact(mDb, contactId);
2360            return count;
2361        } else {
2362            mDbHelper.removeContactIfSingleton(rawContactId);
2363            return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
2364        }
2365    }
2366
2367    private int deleteStatusUpdates(String selection, String[] selectionArgs) {
2368      // delete from both tables: presence and status_updates
2369      // TODO should account type/name be appended to the where clause?
2370      if (VERBOSE_LOGGING) {
2371          Log.v(TAG, "deleting data from status_updates for " + selection);
2372      }
2373      mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
2374          selectionArgs);
2375      return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
2376    }
2377
2378    private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) {
2379        mSyncToNetwork = true;
2380
2381        mValues.clear();
2382        mValues.put(RawContacts.DELETED, 1);
2383        mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
2384        mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
2385        mValues.putNull(RawContacts.CONTACT_ID);
2386        mValues.put(RawContacts.DIRTY, 1);
2387        return updateRawContact(rawContactId, mValues, callerIsSyncAdapter);
2388    }
2389
2390    @Override
2391    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
2392            String[] selectionArgs) {
2393        if (VERBOSE_LOGGING) {
2394            Log.v(TAG, "updateInTransaction: " + uri);
2395        }
2396
2397        int count = 0;
2398
2399        final int match = sUriMatcher.match(uri);
2400        if (match == SYNCSTATE_ID && selection == null) {
2401            long rowId = ContentUris.parseId(uri);
2402            Object data = values.get(ContactsContract.SyncState.DATA);
2403            mTransactionContext.syncStateUpdated(rowId, data);
2404            return 1;
2405        }
2406        flushTransactionalChanges();
2407        final boolean callerIsSyncAdapter =
2408                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2409        switch(match) {
2410            case SYNCSTATE:
2411                return mDbHelper.getSyncState().update(mDb, values,
2412                        appendAccountToSelection(uri, selection), selectionArgs);
2413
2414            case SYNCSTATE_ID: {
2415                selection = appendAccountToSelection(uri, selection);
2416                String selectionWithId =
2417                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
2418                        + (selection == null ? "" : " AND (" + selection + ")");
2419                return mDbHelper.getSyncState().update(mDb, values,
2420                        selectionWithId, selectionArgs);
2421            }
2422
2423            case CONTACTS: {
2424                count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
2425                break;
2426            }
2427
2428            case CONTACTS_ID: {
2429                count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter);
2430                break;
2431            }
2432
2433            case CONTACTS_LOOKUP:
2434            case CONTACTS_LOOKUP_ID: {
2435                final List<String> pathSegments = uri.getPathSegments();
2436                final int segmentCount = pathSegments.size();
2437                if (segmentCount < 3) {
2438                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
2439                            "Missing a lookup key", uri));
2440                }
2441                final String lookupKey = pathSegments.get(2);
2442                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
2443                count = updateContactOptions(contactId, values, callerIsSyncAdapter);
2444                break;
2445            }
2446
2447            case RAW_CONTACTS_DATA: {
2448                final String rawContactId = uri.getPathSegments().get(1);
2449                String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
2450                    + (selection == null ? "" : " AND " + selection);
2451
2452                count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
2453
2454                break;
2455            }
2456
2457            case DATA: {
2458                count = updateData(uri, values, appendAccountToSelection(uri, selection),
2459                        selectionArgs, callerIsSyncAdapter);
2460                if (count > 0) {
2461                    mSyncToNetwork |= !callerIsSyncAdapter;
2462                }
2463                break;
2464            }
2465
2466            case DATA_ID:
2467            case PHONES_ID:
2468            case EMAILS_ID:
2469            case POSTALS_ID: {
2470                count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
2471                if (count > 0) {
2472                    mSyncToNetwork |= !callerIsSyncAdapter;
2473                }
2474                break;
2475            }
2476
2477            case RAW_CONTACTS: {
2478                selection = appendAccountToSelection(uri, selection);
2479                count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
2480                break;
2481            }
2482
2483            case RAW_CONTACTS_ID: {
2484                long rawContactId = ContentUris.parseId(uri);
2485                if (selection != null) {
2486                    selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
2487                    count = updateRawContacts(values, RawContacts._ID + "=?"
2488                                    + " AND(" + selection + ")", selectionArgs,
2489                            callerIsSyncAdapter);
2490                } else {
2491                    mSelectionArgs1[0] = String.valueOf(rawContactId);
2492                    count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
2493                            callerIsSyncAdapter);
2494                }
2495                break;
2496            }
2497
2498            case GROUPS: {
2499                count = updateGroups(uri, values, appendAccountToSelection(uri, selection),
2500                        selectionArgs, callerIsSyncAdapter);
2501                if (count > 0) {
2502                    mSyncToNetwork |= !callerIsSyncAdapter;
2503                }
2504                break;
2505            }
2506
2507            case GROUPS_ID: {
2508                long groupId = ContentUris.parseId(uri);
2509                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
2510                String selectionWithId = Groups._ID + "=? "
2511                        + (selection == null ? "" : " AND " + selection);
2512                count = updateGroups(uri, values, selectionWithId, selectionArgs,
2513                        callerIsSyncAdapter);
2514                if (count > 0) {
2515                    mSyncToNetwork |= !callerIsSyncAdapter;
2516                }
2517                break;
2518            }
2519
2520            case AGGREGATION_EXCEPTIONS: {
2521                count = updateAggregationException(mDb, values);
2522                break;
2523            }
2524
2525            case SETTINGS: {
2526                count = updateSettings(uri, values, appendAccountToSelection(uri, selection),
2527                        selectionArgs);
2528                mSyncToNetwork |= !callerIsSyncAdapter;
2529                break;
2530            }
2531
2532            case STATUS_UPDATES: {
2533                count = updateStatusUpdate(uri, values, selection, selectionArgs);
2534                break;
2535            }
2536
2537            case DIRECTORIES: {
2538                mContactDirectoryManager.scanPackagesByUid(Binder.getCallingUid());
2539                count = 1;
2540                break;
2541            }
2542
2543            default: {
2544                mSyncToNetwork = true;
2545                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
2546            }
2547        }
2548
2549        return count;
2550    }
2551
2552    private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
2553        String[] selectionArgs) {
2554        // update status_updates table, if status is provided
2555        // TODO should account type/name be appended to the where clause?
2556        int updateCount = 0;
2557        ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
2558        if (settableValues.size() > 0) {
2559          updateCount = mDb.update(Tables.STATUS_UPDATES,
2560                    settableValues,
2561                    getWhereClauseForStatusUpdatesTable(selection),
2562                    selectionArgs);
2563        }
2564
2565        // now update the Presence table
2566        settableValues = getSettableColumnsForPresenceTable(values);
2567        if (settableValues.size() > 0) {
2568          updateCount = mDb.update(Tables.PRESENCE, settableValues,
2569                    selection, selectionArgs);
2570        }
2571        // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
2572        // potentially get updated in this method.
2573        return updateCount;
2574    }
2575
2576    /**
2577     * Build a where clause to select the rows to be updated in status_updates table.
2578     */
2579    private String getWhereClauseForStatusUpdatesTable(String selection) {
2580        mSb.setLength(0);
2581        mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
2582        mSb.append(selection);
2583        mSb.append(")");
2584        return mSb.toString();
2585    }
2586
2587    private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
2588        mValues.clear();
2589        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
2590            StatusUpdates.STATUS);
2591        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
2592            StatusUpdates.STATUS_TIMESTAMP);
2593        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
2594            StatusUpdates.STATUS_RES_PACKAGE);
2595        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
2596            StatusUpdates.STATUS_LABEL);
2597        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
2598            StatusUpdates.STATUS_ICON);
2599        return mValues;
2600    }
2601
2602    private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
2603        mValues.clear();
2604        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
2605            StatusUpdates.PRESENCE);
2606        ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values,
2607                StatusUpdates.CHAT_CAPABILITY);
2608        return mValues;
2609    }
2610
2611    private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
2612            String[] selectionArgs, boolean callerIsSyncAdapter) {
2613
2614        mGroupIdCache.clear();
2615
2616        ContentValues updatedValues;
2617        if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
2618            updatedValues = mValues;
2619            updatedValues.clear();
2620            updatedValues.putAll(values);
2621            updatedValues.put(Groups.DIRTY, 1);
2622        } else {
2623            updatedValues = values;
2624        }
2625
2626        int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs);
2627        if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
2628            mVisibleTouched = true;
2629        }
2630        if (updatedValues.containsKey(Groups.SHOULD_SYNC)
2631                && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
2632            Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
2633                    Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null,
2634                    null, null);
2635            String accountName;
2636            String accountType;
2637            try {
2638                while (c.moveToNext()) {
2639                    accountName = c.getString(0);
2640                    accountType = c.getString(1);
2641                    if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
2642                        Account account = new Account(accountName, accountType);
2643                        ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
2644                                new Bundle());
2645                        break;
2646                    }
2647                }
2648            } finally {
2649                c.close();
2650            }
2651        }
2652        return count;
2653    }
2654
2655    private int updateSettings(Uri uri, ContentValues values, String selection,
2656            String[] selectionArgs) {
2657        final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
2658        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
2659            mVisibleTouched = true;
2660        }
2661        return count;
2662    }
2663
2664    private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs,
2665            boolean callerIsSyncAdapter) {
2666        if (values.containsKey(RawContacts.CONTACT_ID)) {
2667            throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
2668                    "in content values. Contact IDs are assigned automatically");
2669        }
2670
2671        if (!callerIsSyncAdapter) {
2672            selection = DatabaseUtils.concatenateWhere(selection,
2673                    RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0");
2674        }
2675
2676        int count = 0;
2677        Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
2678                new String[] { RawContacts._ID }, selection,
2679                selectionArgs, null, null, null);
2680        try {
2681            while (cursor.moveToNext()) {
2682                long rawContactId = cursor.getLong(0);
2683                updateRawContact(rawContactId, values, callerIsSyncAdapter);
2684                count++;
2685            }
2686        } finally {
2687            cursor.close();
2688        }
2689
2690        return count;
2691    }
2692
2693    private int updateRawContact(long rawContactId, ContentValues values,
2694            boolean callerIsSyncAdapter) {
2695        final String selection = RawContacts._ID + " = ?";
2696        mSelectionArgs1[0] = Long.toString(rawContactId);
2697        final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
2698                && values.getAsInteger(RawContacts.DELETED) == 0);
2699        int previousDeleted = 0;
2700        String accountType = null;
2701        String accountName = null;
2702        if (requestUndoDelete) {
2703            Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
2704                    mSelectionArgs1, null, null, null);
2705            try {
2706                if (cursor.moveToFirst()) {
2707                    previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
2708                    accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
2709                    accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
2710                }
2711            } finally {
2712                cursor.close();
2713            }
2714            values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
2715                    ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
2716        }
2717
2718        int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1);
2719        if (count != 0) {
2720            if (values.containsKey(RawContacts.AGGREGATION_MODE)) {
2721                int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE);
2722
2723                // As per ContactsContract documentation, changing aggregation mode
2724                // to DEFAULT should not trigger aggregation
2725                if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
2726                    mContactAggregator.markForAggregation(rawContactId, aggregationMode, false);
2727                }
2728            }
2729            if (values.containsKey(RawContacts.STARRED)) {
2730                if (!callerIsSyncAdapter) {
2731                    updateFavoritesMembership(rawContactId,
2732                            values.getAsLong(RawContacts.STARRED) != 0);
2733                }
2734                mContactAggregator.updateStarred(rawContactId);
2735            } else {
2736                // if this raw contact is being associated with an account, then update the
2737                // favorites group membership based on whether or not this contact is starred.
2738                // If it is starred, add a group membership, if one doesn't already exist
2739                // otherwise delete any matching group memberships.
2740                if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
2741                    boolean starred = 0 != DatabaseUtils.longForQuery(mDb,
2742                            SELECTION_STARRED_FROM_RAW_CONTACTS,
2743                            new String[]{Long.toString(rawContactId)});
2744                    updateFavoritesMembership(rawContactId, starred);
2745                }
2746            }
2747
2748            // if this raw contact is being associated with an account, then add a
2749            // group membership to the group marked as AutoAdd, if any.
2750            if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
2751                addAutoAddMembership(rawContactId);
2752            }
2753
2754            if (values.containsKey(RawContacts.SOURCE_ID)) {
2755                mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId);
2756            }
2757            if (values.containsKey(RawContacts.NAME_VERIFIED)) {
2758
2759                // If setting NAME_VERIFIED for this raw contact, reset it for all
2760                // other raw contacts in the same aggregate
2761                if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) {
2762                    mDbHelper.resetNameVerifiedForOtherRawContacts(rawContactId);
2763                }
2764                mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId);
2765            }
2766            if (requestUndoDelete && previousDeleted == 1) {
2767                mTransactionContext.rawContactInserted(rawContactId,
2768                        new Account(accountName, accountType));
2769            }
2770        }
2771        return count;
2772    }
2773
2774    private int updateData(Uri uri, ContentValues values, String selection,
2775            String[] selectionArgs, boolean callerIsSyncAdapter) {
2776        mValues.clear();
2777        mValues.putAll(values);
2778        mValues.remove(Data._ID);
2779        mValues.remove(Data.RAW_CONTACT_ID);
2780        mValues.remove(Data.MIMETYPE);
2781
2782        String packageName = values.getAsString(Data.RES_PACKAGE);
2783        if (packageName != null) {
2784            mValues.remove(Data.RES_PACKAGE);
2785            mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
2786        }
2787
2788        if (!callerIsSyncAdapter) {
2789            selection = DatabaseUtils.concatenateWhere(selection,
2790                    Data.IS_READ_ONLY + "=0");
2791        }
2792
2793        int count = 0;
2794
2795        // Note that the query will return data according to the access restrictions,
2796        // so we don't need to worry about updating data we don't have permission to read.
2797        Cursor c = query(uri, DataRowHandler.DataUpdateQuery.COLUMNS,
2798                selection, selectionArgs, null);
2799        try {
2800            while(c.moveToNext()) {
2801                count += updateData(mValues, c, callerIsSyncAdapter);
2802            }
2803        } finally {
2804            c.close();
2805        }
2806
2807        return count;
2808    }
2809
2810    private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
2811        if (values.size() == 0) {
2812            return 0;
2813        }
2814
2815        final String mimeType = c.getString(DataRowHandler.DataUpdateQuery.MIMETYPE);
2816        DataRowHandler rowHandler = getDataRowHandler(mimeType);
2817        if (rowHandler.update(mDb, mTransactionContext, values, c, callerIsSyncAdapter)) {
2818            return 1;
2819        } else {
2820            return 0;
2821        }
2822    }
2823
2824    private int updateContactOptions(ContentValues values, String selection,
2825            String[] selectionArgs, boolean callerIsSyncAdapter) {
2826        int count = 0;
2827        Cursor cursor = mDb.query(mDbHelper.getContactView(),
2828                new String[] { Contacts._ID }, selection,
2829                selectionArgs, null, null, null);
2830        try {
2831            while (cursor.moveToNext()) {
2832                long contactId = cursor.getLong(0);
2833                updateContactOptions(contactId, values, callerIsSyncAdapter);
2834                count++;
2835            }
2836        } finally {
2837            cursor.close();
2838        }
2839
2840        return count;
2841    }
2842
2843    private int updateContactOptions(long contactId, ContentValues values,
2844            boolean callerIsSyncAdapter) {
2845
2846        mValues.clear();
2847        ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
2848                values, Contacts.CUSTOM_RINGTONE);
2849        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
2850                values, Contacts.SEND_TO_VOICEMAIL);
2851        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
2852                values, Contacts.LAST_TIME_CONTACTED);
2853        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
2854                values, Contacts.TIMES_CONTACTED);
2855        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
2856                values, Contacts.STARRED);
2857
2858        // Nothing to update - just return
2859        if (mValues.size() == 0) {
2860            return 0;
2861        }
2862
2863        if (mValues.containsKey(RawContacts.STARRED)) {
2864            // Mark dirty when changing starred to trigger sync
2865            mValues.put(RawContacts.DIRTY, 1);
2866        }
2867
2868        mSelectionArgs1[0] = String.valueOf(contactId);
2869        mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?"
2870                + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1);
2871
2872        if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) {
2873            Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
2874                    new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?",
2875                    mSelectionArgs1, null, null, null);
2876            try {
2877                while (cursor.moveToNext()) {
2878                    long rawContactId = cursor.getLong(0);
2879                    updateFavoritesMembership(rawContactId,
2880                            mValues.getAsLong(RawContacts.STARRED) != 0);
2881                }
2882            } finally {
2883                cursor.close();
2884            }
2885        }
2886
2887        // Copy changeable values to prevent automatically managed fields from
2888        // being explicitly updated by clients.
2889        mValues.clear();
2890        ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
2891                values, Contacts.CUSTOM_RINGTONE);
2892        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
2893                values, Contacts.SEND_TO_VOICEMAIL);
2894        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
2895                values, Contacts.LAST_TIME_CONTACTED);
2896        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
2897                values, Contacts.TIMES_CONTACTED);
2898        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
2899                values, Contacts.STARRED);
2900
2901        int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1);
2902
2903        if (values.containsKey(Contacts.LAST_TIME_CONTACTED) &&
2904                !values.containsKey(Contacts.TIMES_CONTACTED)) {
2905            mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
2906            mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
2907        }
2908        return rslt;
2909    }
2910
2911    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
2912        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
2913        long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
2914        long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
2915
2916        long rawContactId1;
2917        long rawContactId2;
2918        if (rcId1 < rcId2) {
2919            rawContactId1 = rcId1;
2920            rawContactId2 = rcId2;
2921        } else {
2922            rawContactId2 = rcId1;
2923            rawContactId1 = rcId2;
2924        }
2925
2926        if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
2927            mSelectionArgs2[0] = String.valueOf(rawContactId1);
2928            mSelectionArgs2[1] = String.valueOf(rawContactId2);
2929            db.delete(Tables.AGGREGATION_EXCEPTIONS,
2930                    AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
2931                    + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
2932        } else {
2933            ContentValues exceptionValues = new ContentValues(3);
2934            exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
2935            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
2936            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
2937            db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
2938                    exceptionValues);
2939        }
2940
2941        mContactAggregator.invalidateAggregationExceptionCache();
2942        mContactAggregator.markForAggregation(rawContactId1,
2943                RawContacts.AGGREGATION_MODE_DEFAULT, true);
2944        mContactAggregator.markForAggregation(rawContactId2,
2945                RawContacts.AGGREGATION_MODE_DEFAULT, true);
2946
2947        mContactAggregator.aggregateContact(mTransactionContext, db, rawContactId1);
2948        mContactAggregator.aggregateContact(mTransactionContext, db, rawContactId2);
2949
2950        // The return value is fake - we just confirm that we made a change, not count actual
2951        // rows changed.
2952        return 1;
2953    }
2954
2955    public void onAccountsUpdated(Account[] accounts) {
2956        scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
2957    }
2958
2959    protected boolean updateAccountsInBackground(Account[] accounts) {
2960        // TODO : Check the unit test.
2961        boolean accountsChanged = false;
2962        HashSet<Account> existingAccounts = new HashSet<Account>();
2963        mDb = mDbHelper.getWritableDatabase();
2964        mDb.beginTransaction();
2965        try {
2966            findValidAccounts(existingAccounts);
2967
2968            // Add a row to the ACCOUNTS table for each new account
2969            for (Account account : accounts) {
2970                if (!existingAccounts.contains(account)) {
2971                    accountsChanged = true;
2972                    mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
2973                            + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)",
2974                            new String[] {account.name, account.type});
2975                }
2976            }
2977
2978            // Remove all valid accounts from the existing account set. What is left
2979            // in the accountsToDelete set will be extra accounts whose data must be deleted.
2980            HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts);
2981            for (Account account : accounts) {
2982                accountsToDelete.remove(account);
2983            }
2984
2985            if (!accountsToDelete.isEmpty()) {
2986                accountsChanged = true;
2987                for (Account account : accountsToDelete) {
2988                    Log.d(TAG, "removing data for removed account " + account);
2989                    String[] params = new String[] {account.name, account.type};
2990                    mDb.execSQL(
2991                            "DELETE FROM " + Tables.GROUPS +
2992                            " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
2993                                    " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
2994                    mDb.execSQL(
2995                            "DELETE FROM " + Tables.PRESENCE +
2996                            " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
2997                                    "SELECT " + RawContacts._ID +
2998                                    " FROM " + Tables.RAW_CONTACTS +
2999                                    " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
3000                                    " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params);
3001                    mDb.execSQL(
3002                            "DELETE FROM " + Tables.RAW_CONTACTS +
3003                            " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
3004                            " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
3005                    mDb.execSQL(
3006                            "DELETE FROM " + Tables.SETTINGS +
3007                            " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
3008                            " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
3009                    mDb.execSQL(
3010                            "DELETE FROM " + Tables.ACCOUNTS +
3011                            " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
3012                            " AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
3013                    mDb.execSQL(
3014                            "DELETE FROM " + Tables.DIRECTORIES +
3015                            " WHERE " + Directory.ACCOUNT_NAME + "=?" +
3016                            " AND " + Directory.ACCOUNT_TYPE + "=?", params);
3017                    resetDirectoryCache();
3018                }
3019
3020                // Find all aggregated contacts that used to contain the raw contacts
3021                // we have just deleted and see if they are still referencing the deleted
3022                // names or photos.  If so, fix up those contacts.
3023                HashSet<Long> orphanContactIds = Sets.newHashSet();
3024                Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID +
3025                        " FROM " + Tables.CONTACTS +
3026                        " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
3027                                Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
3028                                        "(SELECT " + RawContacts._ID +
3029                                        " FROM " + Tables.RAW_CONTACTS + "))" +
3030                        " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
3031                                Contacts.PHOTO_ID + " NOT IN " +
3032                                        "(SELECT " + Data._ID +
3033                                        " FROM " + Tables.DATA + "))", null);
3034                try {
3035                    while (cursor.moveToNext()) {
3036                        orphanContactIds.add(cursor.getLong(0));
3037                    }
3038                } finally {
3039                    cursor.close();
3040                }
3041
3042                for (Long contactId : orphanContactIds) {
3043                    mContactAggregator.updateAggregateData(mTransactionContext, contactId);
3044                }
3045                mDbHelper.updateAllVisible();
3046                updateSearchIndexInTransaction();
3047            }
3048
3049            if (accountsChanged) {
3050                mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
3051            }
3052            mDb.setTransactionSuccessful();
3053        } finally {
3054            mDb.endTransaction();
3055        }
3056        mAccountWritability.clear();
3057
3058        if (accountsChanged) {
3059            updateContactsAccountCount(accounts);
3060            updateProviderStatus();
3061        }
3062
3063        return accountsChanged;
3064    }
3065
3066    private void updateContactsAccountCount(Account[] accounts) {
3067        int count = 0;
3068        for (Account account : accounts) {
3069            if (isContactsAccount(account)) {
3070                count++;
3071            }
3072        }
3073        mContactsAccountCount = count;
3074    }
3075
3076    protected boolean isContactsAccount(Account account) {
3077        final IContentService cs = ContentResolver.getContentService();
3078        try {
3079            return cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
3080        } catch (RemoteException e) {
3081            Log.e(TAG, "Cannot obtain sync flag for account: " + account, e);
3082            return false;
3083        }
3084    }
3085
3086    public void onPackageChanged(String packageName) {
3087        scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_DIRECTORIES, packageName);
3088    }
3089
3090    /**
3091     * Finds all distinct accounts present in the specified table.
3092     */
3093    private void findValidAccounts(Set<Account> validAccounts) {
3094        Cursor c = mDb.rawQuery(
3095                "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE +
3096                " FROM " + Tables.ACCOUNTS, null);
3097        try {
3098            while (c.moveToNext()) {
3099                if (!c.isNull(0) || !c.isNull(1)) {
3100                    validAccounts.add(new Account(c.getString(0), c.getString(1)));
3101                }
3102            }
3103        } finally {
3104            c.close();
3105        }
3106    }
3107
3108    private static class DirectoryCursorWrapper extends CursorWrapper
3109            implements CrossProcessCursor {
3110        private final CrossProcessCursor mCrossProcessCursor;
3111
3112        public DirectoryCursorWrapper(Cursor cursor, CrossProcessCursor crossProcessCursor) {
3113            super(cursor);
3114            mCrossProcessCursor = crossProcessCursor;
3115        }
3116
3117        @Override
3118        public void fillWindow(int pos, CursorWindow window) {
3119            mCrossProcessCursor.fillWindow(pos, window);
3120        }
3121
3122        @Override
3123        public CursorWindow getWindow() {
3124            return mCrossProcessCursor.getWindow();
3125        }
3126
3127        @Override
3128        public boolean onMove(int oldPosition, int newPosition) {
3129            return mCrossProcessCursor.onMove(oldPosition, newPosition);
3130        }
3131    }
3132
3133    @Override
3134    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
3135            String sortOrder) {
3136
3137        waitForAccess(mReadAccessLatch);
3138
3139        String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY);
3140        if (directory == null) {
3141            return queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1);
3142        } else if (directory.equals("0")) {
3143            return queryLocal(uri, projection, selection, selectionArgs, sortOrder,
3144                    Directory.DEFAULT);
3145        } else if (directory.equals("1")) {
3146            return queryLocal(uri, projection, selection, selectionArgs, sortOrder,
3147                    Directory.LOCAL_INVISIBLE);
3148        }
3149
3150        DirectoryInfo directoryInfo = getDirectoryAuthority(directory);
3151        if (directoryInfo == null) {
3152            Log.e(TAG, "Invalid directory ID: " + uri);
3153            return null;
3154        }
3155
3156        Builder builder = new Uri.Builder();
3157        builder.scheme(ContentResolver.SCHEME_CONTENT);
3158        builder.authority(directoryInfo.authority);
3159        builder.encodedPath(uri.getEncodedPath());
3160        if (directoryInfo.accountName != null) {
3161            builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName);
3162        }
3163        if (directoryInfo.accountType != null) {
3164            builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType);
3165        }
3166
3167        String limit = getLimit(uri);
3168        if (limit != null) {
3169            builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, limit);
3170        }
3171
3172        Uri directoryUri = builder.build();
3173
3174        if (projection == null) {
3175            projection = getDefaultProjection(uri);
3176        }
3177
3178        Cursor cursor = getContext().getContentResolver().query(directoryUri, projection, selection,
3179                selectionArgs, sortOrder);
3180
3181        if (cursor == null) {
3182            return null;
3183        }
3184
3185        CrossProcessCursor crossProcessCursor = getCrossProcessCursor(cursor);
3186        if (crossProcessCursor != null) {
3187            return new DirectoryCursorWrapper(cursor, crossProcessCursor);
3188        } else {
3189            return matrixCursorFromCursor(cursor);
3190        }
3191    }
3192
3193    private CrossProcessCursor getCrossProcessCursor(Cursor cursor) {
3194        Cursor c = cursor;
3195        if (c instanceof CrossProcessCursor) {
3196            return (CrossProcessCursor) c;
3197        } else if (c instanceof CursorWindow) {
3198            return getCrossProcessCursor(((CursorWrapper) c).getWrappedCursor());
3199        } else {
3200            return null;
3201        }
3202    }
3203
3204    public MatrixCursor matrixCursorFromCursor(Cursor cursor) {
3205        MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
3206        int numColumns = cursor.getColumnCount();
3207        String data[] = new String[numColumns];
3208        cursor.moveToPosition(-1);
3209        while (cursor.moveToNext()) {
3210            for (int i = 0; i < numColumns; i++) {
3211                data[i] = cursor.getString(i);
3212            }
3213            newCursor.addRow(data);
3214        }
3215        return newCursor;
3216    }
3217
3218    private static final class DirectoryQuery {
3219        public static final String[] COLUMNS = new String[] {
3220                Directory._ID,
3221                Directory.DIRECTORY_AUTHORITY,
3222                Directory.ACCOUNT_NAME,
3223                Directory.ACCOUNT_TYPE
3224        };
3225
3226        public static final int DIRECTORY_ID = 0;
3227        public static final int AUTHORITY = 1;
3228        public static final int ACCOUNT_NAME = 2;
3229        public static final int ACCOUNT_TYPE = 3;
3230    }
3231
3232    /**
3233     * Reads and caches directory information for the database.
3234     */
3235    private DirectoryInfo getDirectoryAuthority(String directoryId) {
3236        synchronized (mDirectoryCache) {
3237            if (!mDirectoryCacheValid) {
3238                mDirectoryCache.clear();
3239                SQLiteDatabase db = mDbHelper.getReadableDatabase();
3240                Cursor cursor = db.query(Tables.DIRECTORIES,
3241                        DirectoryQuery.COLUMNS,
3242                        null, null, null, null, null);
3243                try {
3244                    while (cursor.moveToNext()) {
3245                        DirectoryInfo info = new DirectoryInfo();
3246                        String id = cursor.getString(DirectoryQuery.DIRECTORY_ID);
3247                        info.authority = cursor.getString(DirectoryQuery.AUTHORITY);
3248                        info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
3249                        info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
3250                        mDirectoryCache.put(id, info);
3251                    }
3252                } finally {
3253                    cursor.close();
3254                }
3255                mDirectoryCacheValid = true;
3256            }
3257
3258            return mDirectoryCache.get(directoryId);
3259        }
3260    }
3261
3262    public void resetDirectoryCache() {
3263        synchronized(mDirectoryCache) {
3264            mDirectoryCacheValid = false;
3265        }
3266    }
3267
3268    public Cursor queryLocal(Uri uri, String[] projection, String selection, String[] selectionArgs,
3269                String sortOrder, long directoryId) {
3270        if (VERBOSE_LOGGING) {
3271            Log.v(TAG, "query: " + uri);
3272        }
3273
3274        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
3275
3276        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
3277        String groupBy = null;
3278        String limit = getLimit(uri);
3279
3280        // TODO: Consider writing a test case for RestrictionExceptions when you
3281        // write a new query() block to make sure it protects restricted data.
3282        final int match = sUriMatcher.match(uri);
3283        switch (match) {
3284            case SYNCSTATE:
3285                return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
3286                        sortOrder);
3287
3288            case CONTACTS: {
3289                setTablesAndProjectionMapForContacts(qb, uri, projection);
3290                appendLocalDirectorySelectionIfNeeded(qb, directoryId);
3291                break;
3292            }
3293
3294            case CONTACTS_ID: {
3295                long contactId = ContentUris.parseId(uri);
3296                setTablesAndProjectionMapForContacts(qb, uri, projection);
3297                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
3298                qb.appendWhere(Contacts._ID + "=?");
3299                break;
3300            }
3301
3302            case CONTACTS_LOOKUP:
3303            case CONTACTS_LOOKUP_ID: {
3304                List<String> pathSegments = uri.getPathSegments();
3305                int segmentCount = pathSegments.size();
3306                if (segmentCount < 3) {
3307                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
3308                            "Missing a lookup key", uri));
3309                }
3310
3311                String lookupKey = pathSegments.get(2);
3312                if (segmentCount == 4) {
3313                    long contactId = Long.parseLong(pathSegments.get(3));
3314                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
3315                    setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
3316
3317                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
3318                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
3319                            Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey);
3320                    if (c != null) {
3321                        return c;
3322                    }
3323                }
3324
3325                setTablesAndProjectionMapForContacts(qb, uri, projection);
3326                selectionArgs = insertSelectionArg(selectionArgs,
3327                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
3328                qb.appendWhere(Contacts._ID + "=?");
3329                break;
3330            }
3331
3332            case CONTACTS_LOOKUP_DATA:
3333            case CONTACTS_LOOKUP_ID_DATA: {
3334                List<String> pathSegments = uri.getPathSegments();
3335                int segmentCount = pathSegments.size();
3336                if (segmentCount < 4) {
3337                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
3338                            "Missing a lookup key", uri));
3339                }
3340                String lookupKey = pathSegments.get(2);
3341                if (segmentCount == 5) {
3342                    long contactId = Long.parseLong(pathSegments.get(3));
3343                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
3344                    setTablesAndProjectionMapForData(lookupQb, uri, projection, false);
3345                    lookupQb.appendWhere(" AND ");
3346                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
3347                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
3348                            Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey);
3349                    if (c != null) {
3350                        return c;
3351                    }
3352
3353                    // TODO see if the contact exists but has no data rows (rare)
3354                }
3355
3356                setTablesAndProjectionMapForData(qb, uri, projection, false);
3357                selectionArgs = insertSelectionArg(selectionArgs,
3358                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
3359                qb.appendWhere(" AND " + Data.CONTACT_ID + "=?");
3360                break;
3361            }
3362
3363            case CONTACTS_AS_VCARD: {
3364                // When reading as vCard always use restricted view
3365                final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
3366                qb.setTables(mDbHelper.getContactView(true /* require restricted */));
3367                qb.setProjectionMap(sContactsVCardProjectionMap);
3368                selectionArgs = insertSelectionArg(selectionArgs,
3369                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
3370                qb.appendWhere(Contacts._ID + "=?");
3371                break;
3372            }
3373
3374            case CONTACTS_AS_MULTI_VCARD: {
3375                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
3376                String currentDateString = dateFormat.format(new Date()).toString();
3377                return db.rawQuery(
3378                    "SELECT" +
3379                    " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," +
3380                    " NULL AS " + OpenableColumns.SIZE,
3381                    new String[] { currentDateString });
3382            }
3383
3384            case CONTACTS_FILTER: {
3385                String filterParam = "";
3386                if (uri.getPathSegments().size() > 2) {
3387                    filterParam = uri.getLastPathSegment();
3388                }
3389                setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam);
3390                appendLocalDirectorySelectionIfNeeded(qb, directoryId);
3391                break;
3392            }
3393
3394            case CONTACTS_STREQUENT_FILTER:
3395            case CONTACTS_STREQUENT: {
3396                String filterSql = null;
3397                if (match == CONTACTS_STREQUENT_FILTER
3398                        && uri.getPathSegments().size() > 3) {
3399                    String filterParam = uri.getLastPathSegment();
3400                    StringBuilder sb = new StringBuilder();
3401                    sb.append(Contacts._ID + " IN ");
3402                    appendContactFilterAsNestedQuery(sb, filterParam);
3403                    filterSql = sb.toString();
3404                }
3405
3406                setTablesAndProjectionMapForContacts(qb, uri, projection);
3407
3408                String[] starredProjection = null;
3409                String[] frequentProjection = null;
3410                if (projection != null) {
3411                    starredProjection =
3412                            appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN);
3413                    frequentProjection =
3414                            appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN);
3415                }
3416
3417                // Build the first query for starred
3418                if (filterSql != null) {
3419                    qb.appendWhere(filterSql);
3420                }
3421                qb.setProjectionMap(sStrequentStarredProjectionMap);
3422                final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1",
3423                        null, Contacts._ID, null, null, null);
3424
3425                // Build the second query for frequent
3426                qb = new SQLiteQueryBuilder();
3427                setTablesAndProjectionMapForContacts(qb, uri, projection);
3428                if (filterSql != null) {
3429                    qb.appendWhere(filterSql);
3430                }
3431                qb.setProjectionMap(sStrequentFrequentProjectionMap);
3432                final String frequentQuery = qb.buildQuery(frequentProjection,
3433                        Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
3434                        + " = 0 OR " + Contacts.STARRED + " IS NULL)",
3435                        null, Contacts._ID, null, null, null);
3436
3437                // Put them together
3438                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
3439                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
3440                Cursor c = db.rawQuery(query, null);
3441                if (c != null) {
3442                    c.setNotificationUri(getContext().getContentResolver(),
3443                            ContactsContract.AUTHORITY_URI);
3444                }
3445                return c;
3446            }
3447
3448            case CONTACTS_GROUP: {
3449                setTablesAndProjectionMapForContacts(qb, uri, projection);
3450                if (uri.getPathSegments().size() > 2) {
3451                    qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
3452                    selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3453                }
3454                break;
3455            }
3456
3457            case CONTACTS_ID_DATA: {
3458                long contactId = Long.parseLong(uri.getPathSegments().get(1));
3459                setTablesAndProjectionMapForData(qb, uri, projection, false);
3460                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
3461                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
3462                break;
3463            }
3464
3465            case CONTACTS_ID_PHOTO: {
3466                long contactId = Long.parseLong(uri.getPathSegments().get(1));
3467                setTablesAndProjectionMapForData(qb, uri, projection, false);
3468                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
3469                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
3470                qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
3471                break;
3472            }
3473
3474            case CONTACTS_ID_ENTITIES: {
3475                long contactId = Long.parseLong(uri.getPathSegments().get(1));
3476                setTablesAndProjectionMapForEntities(qb, uri, projection);
3477                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
3478                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
3479                break;
3480            }
3481
3482            case CONTACTS_LOOKUP_ENTITIES:
3483            case CONTACTS_LOOKUP_ID_ENTITIES: {
3484                List<String> pathSegments = uri.getPathSegments();
3485                int segmentCount = pathSegments.size();
3486                if (segmentCount < 4) {
3487                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
3488                            "Missing a lookup key", uri));
3489                }
3490                String lookupKey = pathSegments.get(2);
3491                if (segmentCount == 5) {
3492                    long contactId = Long.parseLong(pathSegments.get(3));
3493                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
3494                    setTablesAndProjectionMapForEntities(lookupQb, uri, projection);
3495                    lookupQb.appendWhere(" AND ");
3496
3497                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
3498                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
3499                            Contacts.Entity.CONTACT_ID, contactId,
3500                            Contacts.Entity.LOOKUP_KEY, lookupKey);
3501                    if (c != null) {
3502                        return c;
3503                    }
3504                }
3505
3506                setTablesAndProjectionMapForEntities(qb, uri, projection);
3507                selectionArgs = insertSelectionArg(selectionArgs,
3508                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
3509                qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?");
3510                break;
3511            }
3512
3513            case PHONES: {
3514                setTablesAndProjectionMapForData(qb, uri, projection, false);
3515                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
3516                break;
3517            }
3518
3519            case PHONES_ID: {
3520                setTablesAndProjectionMapForData(qb, uri, projection, false);
3521                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3522                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
3523                qb.appendWhere(" AND " + Data._ID + "=?");
3524                break;
3525            }
3526
3527            case PHONES_FILTER: {
3528                setTablesAndProjectionMapForData(qb, uri, projection, true);
3529                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
3530                if (uri.getPathSegments().size() > 2) {
3531                    String filterParam = uri.getLastPathSegment();
3532                    StringBuilder sb = new StringBuilder();
3533                    sb.append(" AND (");
3534
3535                    boolean hasCondition = false;
3536                    boolean orNeeded = false;
3537                    String normalizedName = NameNormalizer.normalize(filterParam);
3538                    if (normalizedName.length() > 0) {
3539                        sb.append(Data.RAW_CONTACT_ID + " IN ");
3540                        appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
3541                        orNeeded = true;
3542                        hasCondition = true;
3543                    }
3544
3545                    String number = PhoneNumberUtils.normalizeNumber(filterParam);
3546                    if (!TextUtils.isEmpty(number)) {
3547                        if (orNeeded) {
3548                            sb.append(" OR ");
3549                        }
3550                        sb.append(Data._ID +
3551                                " IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID
3552                                + " FROM " + Tables.PHONE_LOOKUP
3553                                + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
3554                        sb.append(number);
3555                        sb.append("%')");
3556                        hasCondition = true;
3557                    }
3558
3559                    if (!hasCondition) {
3560                        // If it is neither a phone number nor a name, the query should return
3561                        // an empty cursor.  Let's ensure that.
3562                        sb.append("0");
3563                    }
3564                    sb.append(")");
3565                    qb.appendWhere(sb);
3566                }
3567                groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
3568                if (sortOrder == null) {
3569                    sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
3570                }
3571                break;
3572            }
3573
3574            case EMAILS: {
3575                setTablesAndProjectionMapForData(qb, uri, projection, false);
3576                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
3577                break;
3578            }
3579
3580            case EMAILS_ID: {
3581                setTablesAndProjectionMapForData(qb, uri, projection, false);
3582                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3583                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"
3584                        + " AND " + Data._ID + "=?");
3585                break;
3586            }
3587
3588            case EMAILS_LOOKUP: {
3589                setTablesAndProjectionMapForData(qb, uri, projection, false);
3590                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
3591                if (uri.getPathSegments().size() > 2) {
3592                    String email = uri.getLastPathSegment();
3593                    String address = mDbHelper.extractAddressFromEmailAddress(email);
3594                    selectionArgs = insertSelectionArg(selectionArgs, address);
3595                    qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)");
3596                }
3597                break;
3598            }
3599
3600            case EMAILS_FILTER: {
3601                setTablesAndProjectionMapForData(qb, uri, projection, true);
3602                String filterParam = null;
3603                if (uri.getPathSegments().size() > 3) {
3604                    filterParam = uri.getLastPathSegment();
3605                    if (TextUtils.isEmpty(filterParam)) {
3606                        filterParam = null;
3607                    }
3608                }
3609
3610                if (filterParam == null) {
3611                    // If the filter is unspecified, return nothing
3612                    qb.appendWhere(" AND 0");
3613                } else {
3614                    StringBuilder sb = new StringBuilder();
3615                    sb.append(" AND " + Data._ID + " IN (");
3616                    sb.append(
3617                            "SELECT " + Data._ID +
3618                            " FROM " + Tables.DATA +
3619                            " WHERE " + DataColumns.MIMETYPE_ID + "=");
3620                    sb.append(mDbHelper.getMimeTypeIdForEmail());
3621                    sb.append(" AND " + Data.DATA1 + " LIKE ");
3622                    DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
3623                    if (!filterParam.contains("@")) {
3624                        String normalizedName = NameNormalizer.normalize(filterParam);
3625                        if (normalizedName.length() > 0) {
3626
3627                            /*
3628                             * Using a UNION instead of an "OR" to make SQLite use the right
3629                             * indexes. We need it to use the (mimetype,data1) index for the
3630                             * email lookup (see above), but not for the name lookup.
3631                             * SQLite is not smart enough to use the index on one side of an OR
3632                             * but not on the other. Using two separate nested queries
3633                             * and a UNION between them does the job.
3634                             */
3635                            sb.append(
3636                                    " UNION SELECT " + Data._ID +
3637                                    " FROM " + Tables.DATA +
3638                                    " WHERE +" + DataColumns.MIMETYPE_ID + "=");
3639                            sb.append(mDbHelper.getMimeTypeIdForEmail());
3640                            sb.append(" AND " + Data.RAW_CONTACT_ID + " IN ");
3641                            appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
3642                        }
3643                    }
3644                    sb.append(")");
3645                    qb.appendWhere(sb);
3646                }
3647                groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
3648                if (sortOrder == null) {
3649                    sortOrder = EMAIL_FILTER_SORT_ORDER;
3650                }
3651                break;
3652            }
3653
3654            case POSTALS: {
3655                setTablesAndProjectionMapForData(qb, uri, projection, false);
3656                qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
3657                        + StructuredPostal.CONTENT_ITEM_TYPE + "'");
3658                break;
3659            }
3660
3661            case POSTALS_ID: {
3662                setTablesAndProjectionMapForData(qb, uri, projection, false);
3663                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3664                qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
3665                        + StructuredPostal.CONTENT_ITEM_TYPE + "'");
3666                qb.appendWhere(" AND " + Data._ID + "=?");
3667                break;
3668            }
3669
3670            case RAW_CONTACTS: {
3671                setTablesAndProjectionMapForRawContacts(qb, uri);
3672                break;
3673            }
3674
3675            case RAW_CONTACTS_ID: {
3676                long rawContactId = ContentUris.parseId(uri);
3677                setTablesAndProjectionMapForRawContacts(qb, uri);
3678                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
3679                qb.appendWhere(" AND " + RawContacts._ID + "=?");
3680                break;
3681            }
3682
3683            case RAW_CONTACTS_DATA: {
3684                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
3685                setTablesAndProjectionMapForData(qb, uri, projection, false);
3686                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
3687                qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
3688                break;
3689            }
3690
3691            case DATA: {
3692                setTablesAndProjectionMapForData(qb, uri, projection, false);
3693                break;
3694            }
3695
3696            case DATA_ID: {
3697                setTablesAndProjectionMapForData(qb, uri, projection, false);
3698                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3699                qb.appendWhere(" AND " + Data._ID + "=?");
3700                break;
3701            }
3702
3703            case PHONE_LOOKUP: {
3704
3705                if (TextUtils.isEmpty(sortOrder)) {
3706                    // Default the sort order to something reasonable so we get consistent
3707                    // results when callers don't request an ordering
3708                    sortOrder = " length(lookup.normalized_number) DESC";
3709                }
3710
3711                String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
3712                String numberE164 = PhoneNumberUtils.formatNumberToE164(number,
3713                        mDbHelper.getCurrentCountryIso());
3714                String normalizedNumber =
3715                        PhoneNumberUtils.normalizeNumber(number);
3716                mDbHelper.buildPhoneLookupAndContactQuery(qb, normalizedNumber, numberE164);
3717                qb.setProjectionMap(sPhoneLookupProjectionMap);
3718                // Phone lookup cannot be combined with a selection
3719                selection = null;
3720                selectionArgs = null;
3721                break;
3722            }
3723
3724            case GROUPS: {
3725                qb.setTables(mDbHelper.getGroupView());
3726                qb.setProjectionMap(sGroupsProjectionMap);
3727                appendAccountFromParameter(qb, uri);
3728                break;
3729            }
3730
3731            case GROUPS_ID: {
3732                qb.setTables(mDbHelper.getGroupView());
3733                qb.setProjectionMap(sGroupsProjectionMap);
3734                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3735                qb.appendWhere(Groups._ID + "=?");
3736                break;
3737            }
3738
3739            case GROUPS_SUMMARY: {
3740                qb.setTables(mDbHelper.getGroupView() + " AS groups");
3741                qb.setProjectionMap(sGroupsSummaryProjectionMap);
3742                appendAccountFromParameter(qb, uri);
3743                groupBy = Groups._ID;
3744                break;
3745            }
3746
3747            case AGGREGATION_EXCEPTIONS: {
3748                qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
3749                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
3750                break;
3751            }
3752
3753            case AGGREGATION_SUGGESTIONS: {
3754                long contactId = Long.parseLong(uri.getPathSegments().get(1));
3755                String filter = null;
3756                if (uri.getPathSegments().size() > 3) {
3757                    filter = uri.getPathSegments().get(3);
3758                }
3759                final int maxSuggestions;
3760                if (limit != null) {
3761                    maxSuggestions = Integer.parseInt(limit);
3762                } else {
3763                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
3764                }
3765
3766                ArrayList<AggregationSuggestionParameter> parameters = null;
3767                List<String> query = uri.getQueryParameters("query");
3768                if (query != null && !query.isEmpty()) {
3769                    parameters = new ArrayList<AggregationSuggestionParameter>(query.size());
3770                    for (String parameter : query) {
3771                        int offset = parameter.indexOf(':');
3772                        parameters.add(offset == -1
3773                                ? new AggregationSuggestionParameter(
3774                                        AggregationSuggestions.PARAMETER_MATCH_NAME,
3775                                        parameter)
3776                                : new AggregationSuggestionParameter(
3777                                        parameter.substring(0, offset),
3778                                        parameter.substring(offset + 1)));
3779                    }
3780                }
3781
3782                setTablesAndProjectionMapForContacts(qb, uri, projection);
3783
3784                return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId,
3785                        maxSuggestions, filter, parameters);
3786            }
3787
3788            case SETTINGS: {
3789                qb.setTables(Tables.SETTINGS);
3790                qb.setProjectionMap(sSettingsProjectionMap);
3791                appendAccountFromParameter(qb, uri);
3792
3793                // When requesting specific columns, this query requires
3794                // late-binding of the GroupMembership MIME-type.
3795                final String groupMembershipMimetypeId = Long.toString(mDbHelper
3796                        .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
3797                if (projection != null && projection.length != 0 &&
3798                        mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) {
3799                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
3800                }
3801                if (projection != null && projection.length != 0 &&
3802                        mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) {
3803                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
3804                }
3805
3806                break;
3807            }
3808
3809            case STATUS_UPDATES: {
3810                setTableAndProjectionMapForStatusUpdates(qb, projection);
3811                break;
3812            }
3813
3814            case STATUS_UPDATES_ID: {
3815                setTableAndProjectionMapForStatusUpdates(qb, projection);
3816                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3817                qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
3818                break;
3819            }
3820
3821            case SEARCH_SUGGESTIONS: {
3822                return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
3823            }
3824
3825            case SEARCH_SHORTCUT: {
3826                String lookupKey = uri.getLastPathSegment();
3827                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection);
3828            }
3829
3830            case LIVE_FOLDERS_CONTACTS:
3831                qb.setTables(mDbHelper.getContactView());
3832                qb.setProjectionMap(sLiveFoldersProjectionMap);
3833                break;
3834
3835            case LIVE_FOLDERS_CONTACTS_WITH_PHONES:
3836                qb.setTables(mDbHelper.getContactView());
3837                qb.setProjectionMap(sLiveFoldersProjectionMap);
3838                qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1");
3839                break;
3840
3841            case LIVE_FOLDERS_CONTACTS_FAVORITES:
3842                qb.setTables(mDbHelper.getContactView());
3843                qb.setProjectionMap(sLiveFoldersProjectionMap);
3844                qb.appendWhere(Contacts.STARRED + "=1");
3845                break;
3846
3847            case LIVE_FOLDERS_CONTACTS_GROUP_NAME:
3848                qb.setTables(mDbHelper.getContactView());
3849                qb.setProjectionMap(sLiveFoldersProjectionMap);
3850                qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
3851                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3852                break;
3853
3854            case RAW_CONTACT_ENTITIES: {
3855                setTablesAndProjectionMapForRawEntities(qb, uri);
3856                break;
3857            }
3858
3859            case RAW_CONTACT_ENTITY_ID: {
3860                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
3861                setTablesAndProjectionMapForRawEntities(qb, uri);
3862                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
3863                qb.appendWhere(" AND " + RawContacts._ID + "=?");
3864                break;
3865            }
3866
3867            case PROVIDER_STATUS: {
3868                return queryProviderStatus(uri, projection);
3869            }
3870
3871            case DIRECTORIES : {
3872                qb.setTables(Tables.DIRECTORIES);
3873                qb.setProjectionMap(sDirectoryProjectionMap);
3874                break;
3875            }
3876
3877            case DIRECTORIES_ID : {
3878                long id = ContentUris.parseId(uri);
3879                qb.setTables(Tables.DIRECTORIES);
3880                qb.setProjectionMap(sDirectoryProjectionMap);
3881                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id));
3882                qb.appendWhere(Directory._ID + "=?");
3883                break;
3884            }
3885
3886            case COMPLETE_NAME: {
3887                return completeName(uri, projection);
3888            }
3889
3890            default:
3891                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
3892                        sortOrder, limit);
3893        }
3894
3895        qb.setStrictProjectionMap(true);
3896
3897        Cursor cursor =
3898                query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
3899        if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
3900            cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder);
3901        }
3902        return cursor;
3903    }
3904
3905    private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
3906            String selection, String[] selectionArgs, String sortOrder, String groupBy,
3907            String limit) {
3908        if (projection != null && projection.length == 1
3909                && BaseColumns._COUNT.equals(projection[0])) {
3910            qb.setProjectionMap(sCountProjectionMap);
3911        }
3912        final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
3913                sortOrder, limit);
3914        if (c != null) {
3915            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
3916        }
3917        return c;
3918    }
3919
3920    /**
3921     * Creates a single-row cursor containing the current status of the provider.
3922     */
3923    private Cursor queryProviderStatus(Uri uri, String[] projection) {
3924        MatrixCursor cursor = new MatrixCursor(projection);
3925        RowBuilder row = cursor.newRow();
3926        for (int i = 0; i < projection.length; i++) {
3927            if (ProviderStatus.STATUS.equals(projection[i])) {
3928                row.add(mProviderStatus);
3929            } else if (ProviderStatus.DATA1.equals(projection[i])) {
3930                row.add(mEstimatedStorageRequirement);
3931            }
3932        }
3933        return cursor;
3934    }
3935
3936    /**
3937     * Runs the query with the supplied contact ID and lookup ID.  If the query succeeds,
3938     * it returns the resulting cursor, otherwise it returns null and the calling
3939     * method needs to resolve the lookup key and rerun the query.
3940     */
3941    private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb,
3942            SQLiteDatabase db, Uri uri,
3943            String[] projection, String selection, String[] selectionArgs,
3944            String sortOrder, String groupBy, String limit,
3945            String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey) {
3946        String[] args;
3947        if (selectionArgs == null) {
3948            args = new String[2];
3949        } else {
3950            args = new String[selectionArgs.length + 2];
3951            System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
3952        }
3953        args[0] = String.valueOf(contactId);
3954        args[1] = Uri.encode(lookupKey);
3955        lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?");
3956        Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
3957                groupBy, limit);
3958        if (c.getCount() != 0) {
3959            return c;
3960        }
3961
3962        c.close();
3963        return null;
3964    }
3965
3966    private static final class AddressBookIndexQuery {
3967        public static final String LETTER = "letter";
3968        public static final String TITLE = "title";
3969        public static final String COUNT = "count";
3970
3971        public static final String[] COLUMNS = new String[] {
3972                LETTER, TITLE, COUNT
3973        };
3974
3975        public static final int COLUMN_LETTER = 0;
3976        public static final int COLUMN_TITLE = 1;
3977        public static final int COLUMN_COUNT = 2;
3978
3979        public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
3980    }
3981
3982    /**
3983     * Computes counts by the address book index titles and adds the resulting tally
3984     * to the returned cursor as a bundle of extras.
3985     */
3986    private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db,
3987            SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) {
3988        String sortKey;
3989
3990        // The sort order suffix could be something like "DESC".
3991        // We want to preserve it in the query even though we will change
3992        // the sort column itself.
3993        String sortOrderSuffix = "";
3994        if (sortOrder != null) {
3995            int spaceIndex = sortOrder.indexOf(' ');
3996            if (spaceIndex != -1) {
3997                sortKey = sortOrder.substring(0, spaceIndex);
3998                sortOrderSuffix = sortOrder.substring(spaceIndex);
3999            } else {
4000                sortKey = sortOrder;
4001            }
4002        } else {
4003            sortKey = Contacts.SORT_KEY_PRIMARY;
4004        }
4005
4006        String locale = getLocale().toString();
4007        HashMap<String, String> projectionMap = Maps.newHashMap();
4008        projectionMap.put(AddressBookIndexQuery.LETTER,
4009                "SUBSTR(" + sortKey + ",1,1) AS " + AddressBookIndexQuery.LETTER);
4010
4011        /**
4012         * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3,
4013         * to map the first letter of the sort key to a character that is traditionally
4014         * used in phonebooks to represent that letter.  For example, in Korean it will
4015         * be the first consonant in the letter; for Japanese it will be Hiragana rather
4016         * than Katakana.
4017         */
4018        projectionMap.put(AddressBookIndexQuery.TITLE,
4019                "GET_PHONEBOOK_INDEX(SUBSTR(" + sortKey + ",1,1),'" + locale + "')"
4020                        + " AS " + AddressBookIndexQuery.TITLE);
4021        projectionMap.put(AddressBookIndexQuery.COUNT,
4022                "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT);
4023        qb.setProjectionMap(projectionMap);
4024
4025        Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
4026                AddressBookIndexQuery.ORDER_BY, null /* having */,
4027                AddressBookIndexQuery.ORDER_BY + sortOrderSuffix);
4028
4029        try {
4030            int groupCount = indexCursor.getCount();
4031            String titles[] = new String[groupCount];
4032            int counts[] = new int[groupCount];
4033            int indexCount = 0;
4034            String currentTitle = null;
4035
4036            // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up
4037            // with multiple entries for the same title.  The following code
4038            // collapses those duplicates.
4039            for (int i = 0; i < groupCount; i++) {
4040                indexCursor.moveToNext();
4041                String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
4042                int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
4043                if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
4044                    titles[indexCount] = currentTitle = title;
4045                    counts[indexCount] = count;
4046                    indexCount++;
4047                } else {
4048                    counts[indexCount - 1] += count;
4049                }
4050            }
4051
4052            if (indexCount < groupCount) {
4053                String[] newTitles = new String[indexCount];
4054                System.arraycopy(titles, 0, newTitles, 0, indexCount);
4055                titles = newTitles;
4056
4057                int[] newCounts = new int[indexCount];
4058                System.arraycopy(counts, 0, newCounts, 0, indexCount);
4059                counts = newCounts;
4060            }
4061
4062            return new AddressBookCursor((CrossProcessCursor) cursor, titles, counts);
4063        } finally {
4064            indexCursor.close();
4065        }
4066    }
4067
4068    /**
4069     * Returns the contact Id for the contact identified by the lookupKey.
4070     * Robust against changes in the lookup key: if the key has changed, will
4071     * look up the contact by the raw contact IDs or name encoded in the lookup
4072     * key.
4073     */
4074    public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
4075        ContactLookupKey key = new ContactLookupKey();
4076        ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
4077
4078        long contactId = -1;
4079        if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) {
4080            contactId = lookupContactIdBySourceIds(db, segments);
4081            if (contactId != -1) {
4082                return contactId;
4083            }
4084        }
4085
4086        boolean hasRawContactIds =
4087                lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID);
4088        if (hasRawContactIds) {
4089            contactId = lookupContactIdByRawContactIds(db, segments);
4090            if (contactId != -1) {
4091                return contactId;
4092            }
4093        }
4094
4095        if (hasRawContactIds
4096                || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) {
4097            contactId = lookupContactIdByDisplayNames(db, segments);
4098        }
4099
4100        return contactId;
4101    }
4102
4103    private interface LookupBySourceIdQuery {
4104        String TABLE = Tables.RAW_CONTACTS;
4105
4106        String COLUMNS[] = {
4107                RawContacts.CONTACT_ID,
4108                RawContacts.ACCOUNT_TYPE,
4109                RawContacts.ACCOUNT_NAME,
4110                RawContacts.SOURCE_ID
4111        };
4112
4113        int CONTACT_ID = 0;
4114        int ACCOUNT_TYPE = 1;
4115        int ACCOUNT_NAME = 2;
4116        int SOURCE_ID = 3;
4117    }
4118
4119    private long lookupContactIdBySourceIds(SQLiteDatabase db,
4120                ArrayList<LookupKeySegment> segments) {
4121        StringBuilder sb = new StringBuilder();
4122        sb.append(RawContacts.SOURCE_ID + " IN (");
4123        for (int i = 0; i < segments.size(); i++) {
4124            LookupKeySegment segment = segments.get(i);
4125            if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) {
4126                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
4127                sb.append(",");
4128            }
4129        }
4130        sb.setLength(sb.length() - 1);      // Last comma
4131        sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
4132
4133        Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
4134                 sb.toString(), null, null, null, null);
4135        try {
4136            while (c.moveToNext()) {
4137                String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE);
4138                String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
4139                int accountHashCode =
4140                        ContactLookupKey.getAccountHashCode(accountType, accountName);
4141                String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
4142                for (int i = 0; i < segments.size(); i++) {
4143                    LookupKeySegment segment = segments.get(i);
4144                    if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID
4145                            && accountHashCode == segment.accountHashCode
4146                            && segment.key.equals(sourceId)) {
4147                        segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
4148                        break;
4149                    }
4150                }
4151            }
4152        } finally {
4153            c.close();
4154        }
4155
4156        return getMostReferencedContactId(segments);
4157    }
4158
4159    private interface LookupByRawContactIdQuery {
4160        String TABLE = Tables.RAW_CONTACTS;
4161
4162        String COLUMNS[] = {
4163                RawContacts.CONTACT_ID,
4164                RawContacts.ACCOUNT_TYPE,
4165                RawContacts.ACCOUNT_NAME,
4166                RawContacts._ID,
4167        };
4168
4169        int CONTACT_ID = 0;
4170        int ACCOUNT_TYPE = 1;
4171        int ACCOUNT_NAME = 2;
4172        int ID = 3;
4173    }
4174
4175    private long lookupContactIdByRawContactIds(SQLiteDatabase db,
4176            ArrayList<LookupKeySegment> segments) {
4177        StringBuilder sb = new StringBuilder();
4178        sb.append(RawContacts._ID + " IN (");
4179        for (int i = 0; i < segments.size(); i++) {
4180            LookupKeySegment segment = segments.get(i);
4181            if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
4182                sb.append(segment.rawContactId);
4183                sb.append(",");
4184            }
4185        }
4186        sb.setLength(sb.length() - 1);      // Last comma
4187        sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
4188
4189        Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS,
4190                 sb.toString(), null, null, null, null);
4191        try {
4192            while (c.moveToNext()) {
4193                String accountType = c.getString(LookupByRawContactIdQuery.ACCOUNT_TYPE);
4194                String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME);
4195                int accountHashCode =
4196                        ContactLookupKey.getAccountHashCode(accountType, accountName);
4197                String rawContactId = c.getString(LookupByRawContactIdQuery.ID);
4198                for (int i = 0; i < segments.size(); i++) {
4199                    LookupKeySegment segment = segments.get(i);
4200                    if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID
4201                            && accountHashCode == segment.accountHashCode
4202                            && segment.rawContactId.equals(rawContactId)) {
4203                        segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID);
4204                        break;
4205                    }
4206                }
4207            }
4208        } finally {
4209            c.close();
4210        }
4211
4212        return getMostReferencedContactId(segments);
4213    }
4214
4215    private interface LookupByDisplayNameQuery {
4216        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
4217
4218        String COLUMNS[] = {
4219                RawContacts.CONTACT_ID,
4220                RawContacts.ACCOUNT_TYPE,
4221                RawContacts.ACCOUNT_NAME,
4222                NameLookupColumns.NORMALIZED_NAME
4223        };
4224
4225        int CONTACT_ID = 0;
4226        int ACCOUNT_TYPE = 1;
4227        int ACCOUNT_NAME = 2;
4228        int NORMALIZED_NAME = 3;
4229    }
4230
4231    private long lookupContactIdByDisplayNames(SQLiteDatabase db,
4232                ArrayList<LookupKeySegment> segments) {
4233        StringBuilder sb = new StringBuilder();
4234        sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
4235        for (int i = 0; i < segments.size(); i++) {
4236            LookupKeySegment segment = segments.get(i);
4237            if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
4238                    || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
4239                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
4240                sb.append(",");
4241            }
4242        }
4243        sb.setLength(sb.length() - 1);      // Last comma
4244        sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
4245                + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
4246
4247        Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
4248                 sb.toString(), null, null, null, null);
4249        try {
4250            while (c.moveToNext()) {
4251                String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE);
4252                String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
4253                int accountHashCode =
4254                        ContactLookupKey.getAccountHashCode(accountType, accountName);
4255                String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
4256                for (int i = 0; i < segments.size(); i++) {
4257                    LookupKeySegment segment = segments.get(i);
4258                    if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
4259                            || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID)
4260                            && accountHashCode == segment.accountHashCode
4261                            && segment.key.equals(name)) {
4262                        segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
4263                        break;
4264                    }
4265                }
4266            }
4267        } finally {
4268            c.close();
4269        }
4270
4271        return getMostReferencedContactId(segments);
4272    }
4273
4274    private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) {
4275        for (int i = 0; i < segments.size(); i++) {
4276            LookupKeySegment segment = segments.get(i);
4277            if (segment.lookupType == lookupType) {
4278                return true;
4279            }
4280        }
4281
4282        return false;
4283    }
4284
4285    public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
4286        mContactAggregator.updateLookupKeyForRawContact(db, rawContactId);
4287    }
4288
4289    /**
4290     * Returns the contact ID that is mentioned the highest number of times.
4291     */
4292    private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
4293        Collections.sort(segments);
4294
4295        long bestContactId = -1;
4296        int bestRefCount = 0;
4297
4298        long contactId = -1;
4299        int count = 0;
4300
4301        int segmentCount = segments.size();
4302        for (int i = 0; i < segmentCount; i++) {
4303            LookupKeySegment segment = segments.get(i);
4304            if (segment.contactId != -1) {
4305                if (segment.contactId == contactId) {
4306                    count++;
4307                } else {
4308                    if (count > bestRefCount) {
4309                        bestContactId = contactId;
4310                        bestRefCount = count;
4311                    }
4312                    contactId = segment.contactId;
4313                    count = 1;
4314                }
4315            }
4316        }
4317        if (count > bestRefCount) {
4318            return contactId;
4319        } else {
4320            return bestContactId;
4321        }
4322    }
4323
4324    private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
4325            String[] projection) {
4326        StringBuilder sb = new StringBuilder();
4327        appendContactsTables(sb, uri, projection);
4328        qb.setTables(sb.toString());
4329        qb.setProjectionMap(sContactsProjectionMap);
4330    }
4331
4332    /**
4333     * Finds name lookup records matching the supplied filter, picks one arbitrary match per
4334     * contact and joins that with other contacts tables.
4335     */
4336    private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri,
4337            String[] projection, String filter) {
4338
4339        if (filter != null) {
4340            filter = filter.trim();
4341        }
4342
4343        StringBuilder sb = new StringBuilder();
4344        appendContactsTables(sb, uri, projection);
4345        if (TextUtils.isEmpty(filter)) {
4346            sb.append(" JOIN (SELECT NULL AS " + SearchSnippetColumns.SNIPPET + ") WHERE 0");
4347        } else {
4348            appendSearchIndexJoin(sb, uri, projection, filter);
4349        }
4350        qb.setTables(sb.toString());
4351        qb.setProjectionMap(sContactsProjectionWithSnippetMap);
4352    }
4353
4354    private void appendSearchIndexJoin(
4355            StringBuilder sb, Uri uri, String[] projection, String filter) {
4356        sb.append(" JOIN (SELECT " + SearchIndexColumns.CONTACT_ID + " AS snippet_contact_id");
4357
4358        boolean snippetNeeded = mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET);
4359        boolean isEmailAddress = false;
4360        String emailAddress = null;
4361        boolean isPhoneNumber = false;
4362        String phoneNumber = null;
4363        if (snippetNeeded) {
4364            if (filter.indexOf('@') != -1) {
4365                emailAddress = mDbHelper.extractAddressFromEmailAddress(filter);
4366                isEmailAddress = !TextUtils.isEmpty(emailAddress);
4367            } else {
4368                isPhoneNumber = isPhoneNumber(filter);
4369            }
4370
4371            String[] args = null;
4372            String snippetArgs =
4373                    getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY);
4374            if (snippetArgs != null) {
4375                args = snippetArgs.split(",");
4376            }
4377
4378            String startMatch = args != null && args.length > 0 ? args[0]
4379                    : DEFAULT_SNIPPET_ARG_START_MATCH;
4380            String endMatch = args != null && args.length > 1 ? args[1]
4381                    : DEFAULT_SNIPPET_ARG_END_MATCH;
4382            String ellipsis = args != null && args.length > 2 ? args[2]
4383                    : DEFAULT_SNIPPET_ARG_ELLIPSIS;
4384            int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3])
4385                    : DEFAULT_SNIPPET_ARG_MAX_TOKENS;
4386
4387            sb.append(", ");
4388            if (isEmailAddress) {
4389                sb.append("ifnull(");
4390                DatabaseUtils.appendEscapedSQLString(sb, startMatch);
4391                sb.append("||email_address||");
4392                DatabaseUtils.appendEscapedSQLString(sb, endMatch);
4393                sb.append(",");
4394                appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
4395                sb.append(")");
4396            } else if (isPhoneNumber) {
4397                sb.append("ifnull(");
4398                DatabaseUtils.appendEscapedSQLString(sb, startMatch);
4399                sb.append("||phone_number||");
4400                DatabaseUtils.appendEscapedSQLString(sb, endMatch);
4401                sb.append(",");
4402                appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
4403                sb.append(")");
4404            } else {
4405                sb.append("(CASE WHEN name_contact_id NOT NULL THEN NULL ELSE ");
4406                appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
4407                sb.append(" END)");
4408            }
4409            sb.append(" AS " + SearchSnippetColumns.SNIPPET);
4410        }
4411
4412        sb.append(" FROM " + Tables.SEARCH_INDEX);
4413
4414        if (isEmailAddress) {
4415            sb.append(" LEFT OUTER JOIN " +
4416                    "(SELECT "
4417                            + RawContacts.CONTACT_ID + " AS email_contact_id,"
4418                            + "MIN(" + Email.ADDRESS + ") AS email_address" +
4419                    " FROM " + Tables.DATA_JOIN_RAW_CONTACTS +
4420                    " WHERE " + Email.ADDRESS + " LIKE ");
4421            DatabaseUtils.appendEscapedSQLString(sb, filter + "%");
4422            sb.append(") ON (email_contact_id=snippet_contact_id)");
4423        } else if (isPhoneNumber) {
4424            phoneNumber = PhoneNumberUtils.normalizeNumber(filter);
4425            sb.append(" LEFT OUTER JOIN " +
4426                    "(SELECT "
4427                            + RawContacts.CONTACT_ID + " AS phone_contact_id,"
4428                            + "MIN(" + Phone.NUMBER + ") AS phone_number" +
4429                    " FROM " + Tables.DATA_JOIN_RAW_CONTACTS +
4430                    " JOIN " + Tables.PHONE_LOOKUP +
4431                    " ON(" + DataColumns.CONCRETE_ID + "="
4432                        + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.DATA_ID + ")" +
4433                    " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
4434            sb.append(phoneNumber);
4435            sb.append("%'");
4436
4437            String numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber,
4438                    mDbHelper.getCountryIso());
4439            if (!TextUtils.isEmpty(numberE164)) {
4440                sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
4441                sb.append(numberE164);
4442                sb.append("%'");
4443            }
4444            sb.append(" GROUP BY phone_contact_id");
4445            sb.append(") ON (phone_contact_id=snippet_contact_id)");
4446        } else {
4447            sb.append(" LEFT OUTER JOIN " +
4448                    "(SELECT DISTINCT "
4449                            + RawContacts.CONTACT_ID + " AS name_contact_id" +
4450                    " FROM " + Tables.RAW_CONTACTS +
4451                    " JOIN " + Tables.NAME_LOOKUP +
4452                    " ON(" + RawContactsColumns.CONCRETE_ID + "="
4453                            + NameLookupColumns.RAW_CONTACT_ID + ")");
4454
4455            String normalizedFilter = NameNormalizer.normalize(filter);
4456            if (!TextUtils.isEmpty(normalizedFilter)) {
4457                sb.append(" WHERE normalized_name GLOB '");
4458                sb.append(normalizedFilter);
4459                sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
4460                        "=" + NameLookupType.NAME_COLLATION_KEY);
4461            } else {
4462                sb.append(" WHERE 0");
4463            }
4464            sb.append(") ON (name_contact_id=snippet_contact_id)");
4465        }
4466
4467        sb.append(" WHERE ");
4468        sb.append(Tables.SEARCH_INDEX + " MATCH ");
4469        if (isEmailAddress) {
4470            DatabaseUtils.appendEscapedSQLString(sb, "\"" + filter + "*\"");
4471        } else if (isPhoneNumber) {
4472            DatabaseUtils.appendEscapedSQLString(sb, "\"" + filter + "*\" OR " + phoneNumber + "*");
4473        } else {
4474            DatabaseUtils.appendEscapedSQLString(sb, filter + "*");
4475        }
4476        sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)");
4477    }
4478
4479    private void appendSnippetFunction(
4480            StringBuilder sb, String startMatch, String endMatch, String ellipsis, int maxTokens) {
4481        sb.append("snippet(" + Tables.SEARCH_INDEX + ",");
4482        DatabaseUtils.appendEscapedSQLString(sb, startMatch);
4483        sb.append(",");
4484        DatabaseUtils.appendEscapedSQLString(sb, endMatch);
4485        sb.append(",");
4486        DatabaseUtils.appendEscapedSQLString(sb, ellipsis);
4487
4488        // The index of the column used for the snippet, "content"
4489        sb.append(",1,");
4490        sb.append(maxTokens);
4491        sb.append(")");
4492    }
4493
4494    private void appendContactsTables(StringBuilder sb, Uri uri, String[] projection) {
4495        boolean excludeRestrictedData = false;
4496        String requestingPackage = getQueryParameter(uri,
4497                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
4498        if (requestingPackage != null) {
4499            excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
4500        }
4501        sb.append(mDbHelper.getContactView(excludeRestrictedData));
4502        appendContactPresenceJoin(sb, projection, Contacts._ID);
4503        appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
4504    }
4505
4506    private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
4507        StringBuilder sb = new StringBuilder();
4508        boolean excludeRestrictedData = false;
4509        String requestingPackage = getQueryParameter(uri,
4510                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
4511        if (requestingPackage != null) {
4512            excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
4513        }
4514        sb.append(mDbHelper.getRawContactView(excludeRestrictedData));
4515        qb.setTables(sb.toString());
4516        qb.setProjectionMap(sRawContactsProjectionMap);
4517        appendAccountFromParameter(qb, uri);
4518    }
4519
4520    private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) {
4521        qb.setTables(mDbHelper.getRawEntitiesView(shouldExcludeRestrictedData(uri)));
4522        qb.setProjectionMap(sRawEntityProjectionMap);
4523        appendAccountFromParameter(qb, uri);
4524    }
4525
4526    private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
4527            String[] projection, boolean distinct) {
4528        StringBuilder sb = new StringBuilder();
4529        sb.append(mDbHelper.getDataView(shouldExcludeRestrictedData(uri)));
4530        sb.append(" data");
4531
4532        appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID);
4533        appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
4534        appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
4535        appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
4536
4537        qb.setTables(sb.toString());
4538
4539        boolean useDistinct = distinct
4540                || !mDbHelper.isInProjection(projection, DISTINCT_DATA_PROHIBITING_COLUMNS);
4541        qb.setDistinct(useDistinct);
4542        qb.setProjectionMap(useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap);
4543        appendAccountFromParameter(qb, uri);
4544    }
4545
4546    private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
4547            String[] projection) {
4548        StringBuilder sb = new StringBuilder();
4549        sb.append(mDbHelper.getDataView());
4550        sb.append(" data");
4551        appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
4552        appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
4553
4554        qb.setTables(sb.toString());
4555        qb.setProjectionMap(sStatusUpdatesProjectionMap);
4556    }
4557
4558    private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri,
4559            String[] projection) {
4560        StringBuilder sb = new StringBuilder();
4561        sb.append(mDbHelper.getEntitiesView(shouldExcludeRestrictedData(uri)));
4562        sb.append(" data");
4563
4564        appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID);
4565        appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
4566        appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID);
4567        appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID);
4568
4569        qb.setTables(sb.toString());
4570        qb.setProjectionMap(sEntityProjectionMap);
4571        appendAccountFromParameter(qb, uri);
4572    }
4573
4574    private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection,
4575            String lastStatusUpdateIdColumn) {
4576        if (mDbHelper.isInProjection(projection,
4577                Contacts.CONTACT_STATUS,
4578                Contacts.CONTACT_STATUS_RES_PACKAGE,
4579                Contacts.CONTACT_STATUS_ICON,
4580                Contacts.CONTACT_STATUS_LABEL,
4581                Contacts.CONTACT_STATUS_TIMESTAMP)) {
4582            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
4583                    + ContactsStatusUpdatesColumns.ALIAS +
4584                    " ON (" + lastStatusUpdateIdColumn + "="
4585                            + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
4586        }
4587    }
4588
4589    private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection,
4590            String dataIdColumn) {
4591        if (mDbHelper.isInProjection(projection,
4592                StatusUpdates.STATUS,
4593                StatusUpdates.STATUS_RES_PACKAGE,
4594                StatusUpdates.STATUS_ICON,
4595                StatusUpdates.STATUS_LABEL,
4596                StatusUpdates.STATUS_TIMESTAMP)) {
4597            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
4598                    " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
4599                            + dataIdColumn + ")");
4600        }
4601    }
4602
4603    private void appendContactPresenceJoin(StringBuilder sb, String[] projection,
4604            String contactIdColumn) {
4605        if (mDbHelper.isInProjection(projection,
4606                Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) {
4607            sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
4608                    " ON (" + contactIdColumn + " = "
4609                            + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")");
4610        }
4611    }
4612
4613    private void appendDataPresenceJoin(StringBuilder sb, String[] projection,
4614            String dataIdColumn) {
4615        if (mDbHelper.isInProjection(projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) {
4616            sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
4617                    " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")");
4618        }
4619    }
4620
4621    private void appendLocalDirectorySelectionIfNeeded(SQLiteQueryBuilder qb, long directoryId) {
4622        if (directoryId == Directory.DEFAULT) {
4623            qb.appendWhere(Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY);
4624        } else if (directoryId == Directory.LOCAL_INVISIBLE){
4625            qb.appendWhere(Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY);
4626        }
4627    }
4628
4629    private boolean shouldExcludeRestrictedData(Uri uri) {
4630        // Note: currently, "export only" equals to "restricted", but may not in the future.
4631        boolean excludeRestrictedData = readBooleanQueryParameter(uri,
4632                Data.FOR_EXPORT_ONLY, false);
4633        if (excludeRestrictedData) {
4634            return true;
4635        }
4636
4637        String requestingPackage = getQueryParameter(uri,
4638                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
4639        if (requestingPackage != null) {
4640            return !mDbHelper.hasAccessToRestrictedData(requestingPackage);
4641        }
4642
4643        return false;
4644    }
4645
4646    private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
4647        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
4648        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
4649
4650        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
4651        if (partialUri) {
4652            // Throw when either account is incomplete
4653            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
4654                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
4655        }
4656
4657        // Accounts are valid by only checking one parameter, since we've
4658        // already ruled out partial accounts.
4659        final boolean validAccount = !TextUtils.isEmpty(accountName);
4660        if (validAccount) {
4661            qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
4662                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
4663                    + RawContacts.ACCOUNT_TYPE + "="
4664                    + DatabaseUtils.sqlEscapeString(accountType));
4665        } else {
4666            qb.appendWhere("1");
4667        }
4668    }
4669
4670    private String appendAccountToSelection(Uri uri, String selection) {
4671        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
4672        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
4673
4674        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
4675        if (partialUri) {
4676            // Throw when either account is incomplete
4677            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
4678                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
4679        }
4680
4681        // Accounts are valid by only checking one parameter, since we've
4682        // already ruled out partial accounts.
4683        final boolean validAccount = !TextUtils.isEmpty(accountName);
4684        if (validAccount) {
4685            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
4686                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
4687                    + RawContacts.ACCOUNT_TYPE + "="
4688                    + DatabaseUtils.sqlEscapeString(accountType));
4689            if (!TextUtils.isEmpty(selection)) {
4690                selectionSb.append(" AND (");
4691                selectionSb.append(selection);
4692                selectionSb.append(')');
4693            }
4694            return selectionSb.toString();
4695        } else {
4696            return selection;
4697        }
4698    }
4699
4700    /**
4701     * Gets the value of the "limit" URI query parameter.
4702     *
4703     * @return A string containing a non-negative integer, or <code>null</code> if
4704     *         the parameter is not set, or is set to an invalid value.
4705     */
4706    private String getLimit(Uri uri) {
4707        String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY);
4708        if (limitParam == null) {
4709            return null;
4710        }
4711        // make sure that the limit is a non-negative integer
4712        try {
4713            int l = Integer.parseInt(limitParam);
4714            if (l < 0) {
4715                Log.w(TAG, "Invalid limit parameter: " + limitParam);
4716                return null;
4717            }
4718            return String.valueOf(l);
4719        } catch (NumberFormatException ex) {
4720            Log.w(TAG, "Invalid limit parameter: " + limitParam);
4721            return null;
4722        }
4723    }
4724
4725    String getContactsRestrictions() {
4726        if (mDbHelper.hasAccessToRestrictedData()) {
4727            return "1";
4728        } else {
4729            return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0";
4730        }
4731    }
4732
4733    public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
4734        if (mDbHelper.hasAccessToRestrictedData()) {
4735            return "1";
4736        } else {
4737            return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
4738                    + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
4739        }
4740    }
4741
4742    @Override
4743    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
4744
4745        waitForAccess(mReadAccessLatch);
4746
4747        int match = sUriMatcher.match(uri);
4748        switch (match) {
4749            case CONTACTS_ID_PHOTO: {
4750                return openPhotoAssetFile(uri, mode,
4751                        Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?",
4752                        new String[]{uri.getPathSegments().get(1)});
4753            }
4754
4755            case DATA_ID: {
4756                return openPhotoAssetFile(uri, mode,
4757                        Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'",
4758                        new String[]{uri.getPathSegments().get(1)});
4759            }
4760
4761            case CONTACTS_AS_VCARD: {
4762                SQLiteDatabase db = mDbHelper.getReadableDatabase();
4763                final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
4764                mSelectionArgs1[0] = String.valueOf(lookupContactIdByLookupKey(db, lookupKey));
4765                final String selection = Contacts._ID + "=?";
4766
4767                // When opening a contact as file, we pass back contents as a
4768                // vCard-encoded stream. We build into a local buffer first,
4769                // then pipe into MemoryFile once the exact size is known.
4770                final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
4771                outputRawContactsAsVCard(localStream, selection, mSelectionArgs1);
4772                return buildAssetFileDescriptor(localStream);
4773            }
4774
4775            case CONTACTS_AS_MULTI_VCARD: {
4776                SQLiteDatabase db = mDbHelper.getReadableDatabase();
4777                final String lookupKeys = uri.getPathSegments().get(2);
4778                final String[] loopupKeyList = lookupKeys.split(":");
4779                final StringBuilder inBuilder = new StringBuilder();
4780                int index = 0;
4781                // SQLite has limits on how many parameters can be used
4782                // so the IDs are concatenated to a query string here instead
4783                for (String lookupKey : loopupKeyList) {
4784                    if (index == 0) {
4785                        inBuilder.append("(");
4786                    } else {
4787                        inBuilder.append(",");
4788                    }
4789                    inBuilder.append(lookupContactIdByLookupKey(db, lookupKey));
4790                    index++;
4791                }
4792                inBuilder.append(')');
4793                final String selection = Contacts._ID + " IN " + inBuilder.toString();
4794
4795                // When opening a contact as file, we pass back contents as a
4796                // vCard-encoded stream. We build into a local buffer first,
4797                // then pipe into MemoryFile once the exact size is known.
4798                final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
4799                outputRawContactsAsVCard(localStream, selection, null);
4800                return buildAssetFileDescriptor(localStream);
4801            }
4802
4803            default:
4804                throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist",
4805                        uri));
4806        }
4807    }
4808
4809    private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection,
4810            String[] selectionArgs)
4811            throws FileNotFoundException {
4812        if (!"r".equals(mode)) {
4813            throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode
4814                    + " not supported.", uri));
4815        }
4816
4817        String sql =
4818                "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
4819                " WHERE " + selection;
4820        SQLiteDatabase db = mDbHelper.getReadableDatabase();
4821        try {
4822            return makeAssetFileDescriptor(
4823                    DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs));
4824        } catch (SQLiteDoneException e) {
4825            // this will happen if the DB query returns no rows (i.e. contact does not exist)
4826            throw new FileNotFoundException(uri.toString());
4827        }
4828    }
4829
4830    private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
4831
4832    /**
4833     * Returns an {@link AssetFileDescriptor} backed by the
4834     * contents of the given {@link ByteArrayOutputStream}.
4835     */
4836    private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
4837        try {
4838            stream.flush();
4839
4840            final byte[] byteData = stream.toByteArray();
4841
4842            return makeAssetFileDescriptor(
4843                    ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME),
4844                    byteData.length);
4845        } catch (IOException e) {
4846            Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString());
4847            return null;
4848        }
4849    }
4850
4851    private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd) {
4852        return makeAssetFileDescriptor(fd, AssetFileDescriptor.UNKNOWN_LENGTH);
4853    }
4854
4855    private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd, long length) {
4856        return fd != null ? new AssetFileDescriptor(fd, 0, length) : null;
4857    }
4858
4859    /**
4860     * Output {@link RawContacts} matching the requested selection in the vCard
4861     * format to the given {@link OutputStream}. This method returns silently if
4862     * any errors encountered.
4863     */
4864    private void outputRawContactsAsVCard(OutputStream stream, String selection,
4865            String[] selectionArgs) {
4866        final Context context = this.getContext();
4867        final VCardComposer composer =
4868                new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false);
4869        composer.addHandler(composer.new HandlerForOutputStream(stream));
4870
4871        // No extra checks since composer always uses restricted views
4872        if (!composer.init(selection, selectionArgs)) {
4873            Log.w(TAG, "Failed to init VCardComposer");
4874            return;
4875        }
4876
4877        while (!composer.isAfterLast()) {
4878            if (!composer.createOneEntry()) {
4879                Log.w(TAG, "Failed to output a contact.");
4880            }
4881        }
4882        composer.terminate();
4883    }
4884
4885    @Override
4886    public String getType(Uri uri) {
4887
4888        waitForAccess(mReadAccessLatch);
4889
4890        final int match = sUriMatcher.match(uri);
4891        switch (match) {
4892            case CONTACTS:
4893                return Contacts.CONTENT_TYPE;
4894            case CONTACTS_LOOKUP:
4895            case CONTACTS_ID:
4896            case CONTACTS_LOOKUP_ID:
4897                return Contacts.CONTENT_ITEM_TYPE;
4898            case CONTACTS_AS_VCARD:
4899            case CONTACTS_AS_MULTI_VCARD:
4900                return Contacts.CONTENT_VCARD_TYPE;
4901            case CONTACTS_ID_PHOTO:
4902                return "image/png";
4903            case RAW_CONTACTS:
4904                return RawContacts.CONTENT_TYPE;
4905            case RAW_CONTACTS_ID:
4906                return RawContacts.CONTENT_ITEM_TYPE;
4907            case DATA:
4908                return Data.CONTENT_TYPE;
4909            case DATA_ID:
4910                return mDbHelper.getDataMimeType(ContentUris.parseId(uri));
4911            case PHONES:
4912                return Phone.CONTENT_TYPE;
4913            case PHONES_ID:
4914                return Phone.CONTENT_ITEM_TYPE;
4915            case PHONE_LOOKUP:
4916                return PhoneLookup.CONTENT_TYPE;
4917            case EMAILS:
4918                return Email.CONTENT_TYPE;
4919            case EMAILS_ID:
4920                return Email.CONTENT_ITEM_TYPE;
4921            case POSTALS:
4922                return StructuredPostal.CONTENT_TYPE;
4923            case POSTALS_ID:
4924                return StructuredPostal.CONTENT_ITEM_TYPE;
4925            case AGGREGATION_EXCEPTIONS:
4926                return AggregationExceptions.CONTENT_TYPE;
4927            case AGGREGATION_EXCEPTION_ID:
4928                return AggregationExceptions.CONTENT_ITEM_TYPE;
4929            case SETTINGS:
4930                return Settings.CONTENT_TYPE;
4931            case AGGREGATION_SUGGESTIONS:
4932                return Contacts.CONTENT_TYPE;
4933            case SEARCH_SUGGESTIONS:
4934                return SearchManager.SUGGEST_MIME_TYPE;
4935            case SEARCH_SHORTCUT:
4936                return SearchManager.SHORTCUT_MIME_TYPE;
4937            case DIRECTORIES:
4938                return Directory.CONTENT_TYPE;
4939            case DIRECTORIES_ID:
4940                return Directory.CONTENT_ITEM_TYPE;
4941            default:
4942                return mLegacyApiSupport.getType(uri);
4943        }
4944    }
4945
4946    public String[] getDefaultProjection(Uri uri) {
4947        final int match = sUriMatcher.match(uri);
4948        switch (match) {
4949            case CONTACTS:
4950            case CONTACTS_LOOKUP:
4951            case CONTACTS_ID:
4952            case CONTACTS_LOOKUP_ID:
4953            case AGGREGATION_SUGGESTIONS:
4954                return sContactsProjectionMap.getColumnNames();
4955
4956            case CONTACTS_ID_ENTITIES:
4957                return sEntityProjectionMap.getColumnNames();
4958
4959            case CONTACTS_AS_VCARD:
4960            case CONTACTS_AS_MULTI_VCARD:
4961                return sContactsVCardProjectionMap.getColumnNames();
4962
4963            case RAW_CONTACTS:
4964            case RAW_CONTACTS_ID:
4965                return sRawContactsProjectionMap.getColumnNames();
4966
4967            case DATA_ID:
4968            case PHONES:
4969            case PHONES_ID:
4970            case EMAILS:
4971            case EMAILS_ID:
4972            case POSTALS:
4973            case POSTALS_ID:
4974                return sDataProjectionMap.getColumnNames();
4975
4976            case PHONE_LOOKUP:
4977                return sPhoneLookupProjectionMap.getColumnNames();
4978
4979            case AGGREGATION_EXCEPTIONS:
4980            case AGGREGATION_EXCEPTION_ID:
4981                return sAggregationExceptionsProjectionMap.getColumnNames();
4982
4983            case SETTINGS:
4984                return sSettingsProjectionMap.getColumnNames();
4985
4986            case DIRECTORIES:
4987            case DIRECTORIES_ID:
4988                return sDirectoryProjectionMap.getColumnNames();
4989
4990            default:
4991                return null;
4992        }
4993    }
4994
4995    private class StructuredNameLookupBuilder extends NameLookupBuilder {
4996
4997        public StructuredNameLookupBuilder(NameSplitter splitter) {
4998            super(splitter);
4999        }
5000
5001        @Override
5002        protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
5003                String name) {
5004            mDbHelper.insertNameLookup(rawContactId, dataId, lookupType, name);
5005        }
5006
5007        @Override
5008        protected String[] getCommonNicknameClusters(String normalizedName) {
5009            return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
5010        }
5011    }
5012
5013    public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
5014        sb.append("(" +
5015                "SELECT DISTINCT " + RawContacts.CONTACT_ID +
5016                " FROM " + Tables.RAW_CONTACTS +
5017                " JOIN " + Tables.NAME_LOOKUP +
5018                " ON(" + RawContactsColumns.CONCRETE_ID + "="
5019                        + NameLookupColumns.RAW_CONTACT_ID + ")" +
5020                " WHERE normalized_name GLOB '");
5021        sb.append(NameNormalizer.normalize(filterParam));
5022        sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
5023                    " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))");
5024    }
5025
5026    public String getRawContactsByFilterAsNestedQuery(String filterParam) {
5027        StringBuilder sb = new StringBuilder();
5028        appendRawContactsByFilterAsNestedQuery(sb, filterParam);
5029        return sb.toString();
5030    }
5031
5032    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) {
5033        appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true);
5034    }
5035
5036    private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
5037            boolean allowEmailMatch) {
5038        if (TextUtils.isEmpty(normalizedName)) {
5039            // Effectively an empty IN clause - SQL syntax does not allow an actual empty list here
5040            sb.append("(0)");
5041        } else {
5042            sb.append("(" +
5043                    "SELECT " + NameLookupColumns.RAW_CONTACT_ID +
5044                    " FROM " + Tables.NAME_LOOKUP +
5045                    " WHERE " + NameLookupColumns.NORMALIZED_NAME +
5046                    " GLOB '");
5047            // Should not use a "?" argument placeholder here, because
5048            // that would prevent the SQL optimizer from using the index on NORMALIZED_NAME.
5049            sb.append(normalizedName);
5050            sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
5051                    + NameLookupType.NAME_COLLATION_KEY + ","
5052                    + NameLookupType.NICKNAME + ","
5053                    + NameLookupType.NAME_SHORTHAND + ","
5054                    + NameLookupType.ORGANIZATION + ","
5055                    + NameLookupType.NAME_CONSONANTS);
5056            if (allowEmailMatch) {
5057                sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME);
5058            }
5059            sb.append("))");
5060        }
5061    }
5062
5063
5064    public boolean appendEmailBasedDataFilter(StringBuilder sb, String filter) {
5065        if (filter.indexOf('@') == -1) {
5066            return false;
5067        }
5068
5069        String address = mDbHelper.extractAddressFromEmailAddress(filter);
5070        if (TextUtils.isEmpty(address)) {
5071            return false;
5072        }
5073
5074        sb.append(DataColumns.MIMETYPE_ID + " IN (");
5075        sb.append(mDbHelper.getMimeTypeIdForEmail());
5076        sb.append(",");
5077        sb.append(mDbHelper.getMimeTypeIdForIm());
5078        sb.append(",");
5079        sb.append(mDbHelper.getMimeTypeIdForSip());
5080        sb.append(") AND " + Data.DATA1 + " LIKE(");
5081        DatabaseUtils.appendEscapedSQLString(sb, address + '%');
5082        sb.append(")");
5083        return true;
5084    }
5085
5086    public boolean appendPhoneNumberBasedDataFilter(StringBuilder sb, String filter) {
5087        if (!isPhoneNumber(filter)) {
5088            return false;
5089        }
5090
5091        String number = PhoneNumberUtils.normalizeNumber(filter);
5092        sb.append(DataColumns.CONCRETE_ID + " IN " +
5093                "(SELECT " + PhoneLookupColumns.DATA_ID
5094                + " FROM " + Tables.PHONE_LOOKUP
5095                + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
5096        sb.append(number);
5097        sb.append("%'");
5098
5099        String numberE164 = PhoneNumberUtils.formatNumberToE164(number, mDbHelper.getCountryIso());
5100        if (!TextUtils.isEmpty(numberE164)) {
5101            sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
5102            sb.append(numberE164);
5103            sb.append("%'");
5104        }
5105        sb.append(")");
5106
5107        String normalizedFilter = NameNormalizer.normalize(filter);
5108        if (TextUtils.isEmpty(normalizedFilter)) {
5109            return true;
5110        }
5111
5112        sb.append(" OR " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN " +
5113                "(SELECT " + NameLookupColumns.RAW_CONTACT_ID +
5114                " FROM " + Tables.NAME_LOOKUP +
5115                " WHERE " + NameLookupColumns.NORMALIZED_NAME +
5116                " GLOB '");
5117        sb.append(normalizedFilter);
5118        sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
5119                + CONTACT_LOOKUP_NAME_TYPES + "))");
5120        return true;
5121    }
5122
5123    public boolean appendNameBasedRawContactFilter(StringBuilder sb, String filter) {
5124        String normalizedFilter = NameNormalizer.normalize(filter);
5125        if (TextUtils.isEmpty(normalizedFilter)) {
5126            return false;
5127        }
5128
5129        sb.append(DataColumns.CONCRETE_RAW_CONTACT_ID + " IN " +
5130                "(SELECT " + NameLookupColumns.RAW_CONTACT_ID +
5131                " FROM " + Tables.NAME_LOOKUP +
5132                " WHERE " + NameLookupColumns.NORMALIZED_NAME +
5133                " GLOB '");
5134        // Should not use a "?" argument placeholder here, because
5135        // that would prevent the SQL optimizer from using the index on NORMALIZED_NAME.
5136        sb.append(normalizedFilter);
5137        sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
5138                + CONTACT_LOOKUP_NAME_TYPES + "))");
5139        return true;
5140    }
5141
5142    public boolean isPhoneNumber(String filter) {
5143        boolean atLeastOneDigit = false;
5144        int len = filter.length();
5145        for (int i = 0; i < len; i++) {
5146            char c = filter.charAt(i);
5147            if (c >= '0' && c <= '9') {
5148                atLeastOneDigit = true;
5149            } else if (c != '*' && c != '#' && c != '+' && c != 'N' && c != '.' && c != ';'
5150                    && c != '-' && c != '(' && c != ')' && c != ' ') {
5151                return false;
5152            }
5153        }
5154        return atLeastOneDigit;
5155    }
5156
5157    /**
5158     * Takes components of a name from the query parameters and returns a cursor with those
5159     * components as well as all missing components.  There is no database activity involved
5160     * in this so the call can be made on the UI thread.
5161     */
5162    private Cursor completeName(Uri uri, String[] projection) {
5163        if (projection == null) {
5164            projection = sDataProjectionMap.getColumnNames();
5165        }
5166
5167        ContentValues values = new ContentValues();
5168        DataRowHandlerForStructuredName handler = (DataRowHandlerForStructuredName)
5169                getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE);
5170
5171        copyQueryParamsToContentValues(values, uri,
5172                StructuredName.DISPLAY_NAME,
5173                StructuredName.PREFIX,
5174                StructuredName.GIVEN_NAME,
5175                StructuredName.MIDDLE_NAME,
5176                StructuredName.FAMILY_NAME,
5177                StructuredName.SUFFIX,
5178                StructuredName.PHONETIC_NAME,
5179                StructuredName.PHONETIC_FAMILY_NAME,
5180                StructuredName.PHONETIC_MIDDLE_NAME,
5181                StructuredName.PHONETIC_GIVEN_NAME
5182        );
5183
5184        handler.fixStructuredNameComponents(values, values);
5185
5186        MatrixCursor cursor = new MatrixCursor(projection);
5187        Object[] row = new Object[projection.length];
5188        for (int i = 0; i < projection.length; i++) {
5189            row[i] = values.get(projection[i]);
5190        }
5191        cursor.addRow(row);
5192        return cursor;
5193    }
5194
5195    private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) {
5196        for (String column : columns) {
5197            String param = uri.getQueryParameter(column);
5198            if (param != null) {
5199                values.put(column, param);
5200            }
5201        }
5202    }
5203
5204
5205    /**
5206     * Inserts an argument at the beginning of the selection arg list.
5207     */
5208    private String[] insertSelectionArg(String[] selectionArgs, String arg) {
5209        if (selectionArgs == null) {
5210            return new String[] {arg};
5211        } else {
5212            int newLength = selectionArgs.length + 1;
5213            String[] newSelectionArgs = new String[newLength];
5214            newSelectionArgs[0] = arg;
5215            System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
5216            return newSelectionArgs;
5217        }
5218    }
5219
5220    private String[] appendProjectionArg(String[] projection, String arg) {
5221        if (projection == null) {
5222            return null;
5223        }
5224        final int length = projection.length;
5225        String[] newProjection = new String[length + 1];
5226        System.arraycopy(projection, 0, newProjection, 0, length);
5227        newProjection[length] = arg;
5228        return newProjection;
5229    }
5230
5231    protected Account getDefaultAccount() {
5232        AccountManager accountManager = AccountManager.get(getContext());
5233        try {
5234            Account[] accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE);
5235            if (accounts != null && accounts.length > 0) {
5236                return accounts[0];
5237            }
5238        } catch (Throwable e) {
5239            Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
5240        }
5241        return null;
5242    }
5243
5244    /**
5245     * Returns true if the specified account type is writable.
5246     */
5247    protected boolean isWritableAccount(String accountType) {
5248        if (accountType == null) {
5249            return true;
5250        }
5251
5252        Boolean writable = mAccountWritability.get(accountType);
5253        if (writable != null) {
5254            return writable;
5255        }
5256
5257        IContentService contentService = ContentResolver.getContentService();
5258        try {
5259            for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
5260                if (ContactsContract.AUTHORITY.equals(sync.authority) &&
5261                        accountType.equals(sync.accountType)) {
5262                    writable = sync.supportsUploading();
5263                    break;
5264                }
5265            }
5266        } catch (RemoteException e) {
5267            Log.e(TAG, "Could not acquire sync adapter types");
5268        }
5269
5270        if (writable == null) {
5271            writable = false;
5272        }
5273
5274        mAccountWritability.put(accountType, writable);
5275        return writable;
5276    }
5277
5278
5279    /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
5280            boolean defaultValue) {
5281
5282        // Manually parse the query, which is much faster than calling uri.getQueryParameter
5283        String query = uri.getEncodedQuery();
5284        if (query == null) {
5285            return defaultValue;
5286        }
5287
5288        int index = query.indexOf(parameter);
5289        if (index == -1) {
5290            return defaultValue;
5291        }
5292
5293        index += parameter.length();
5294
5295        return !matchQueryParameter(query, index, "=0", false)
5296                && !matchQueryParameter(query, index, "=false", true);
5297    }
5298
5299    private static boolean matchQueryParameter(String query, int index, String value,
5300            boolean ignoreCase) {
5301        int length = value.length();
5302        return query.regionMatches(ignoreCase, index, value, 0, length)
5303                && (query.length() == index + length || query.charAt(index + length) == '&');
5304    }
5305
5306    /**
5307     * A fast re-implementation of {@link Uri#getQueryParameter}
5308     */
5309    /* package */ static String getQueryParameter(Uri uri, String parameter) {
5310        String query = uri.getEncodedQuery();
5311        if (query == null) {
5312            return null;
5313        }
5314
5315        int queryLength = query.length();
5316        int parameterLength = parameter.length();
5317
5318        String value;
5319        int index = 0;
5320        while (true) {
5321            index = query.indexOf(parameter, index);
5322            if (index == -1) {
5323                return null;
5324            }
5325
5326            index += parameterLength;
5327
5328            if (queryLength == index) {
5329                return null;
5330            }
5331
5332            if (query.charAt(index) == '=') {
5333                index++;
5334                break;
5335            }
5336        }
5337
5338        int ampIndex = query.indexOf('&', index);
5339        if (ampIndex == -1) {
5340            value = query.substring(index);
5341        } else {
5342            value = query.substring(index, ampIndex);
5343        }
5344
5345        return Uri.decode(value);
5346    }
5347
5348    protected boolean isAggregationUpgradeNeeded() {
5349        if (!mContactAggregator.isEnabled()) {
5350            return false;
5351        }
5352
5353        int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_AGGREGATION_ALGORITHM, "1"));
5354        return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION;
5355    }
5356
5357    protected void upgradeAggregationAlgorithmInBackground() {
5358        // This upgrade will affect very few contacts, so it can be performed on the
5359        // main thread during the initial boot after an OTA
5360
5361        Log.i(TAG, "Upgrading aggregation algorithm");
5362        int count = 0;
5363        long start = SystemClock.currentThreadTimeMillis();
5364        try {
5365            mDb = mDbHelper.getWritableDatabase();
5366            mDb.beginTransaction();
5367            Cursor cursor = mDb.query(true,
5368                    Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2",
5369                    new String[]{"r1." + RawContacts._ID},
5370                    "r1." + RawContacts._ID + "!=r2." + RawContacts._ID +
5371                    " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID +
5372                    " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME +
5373                    " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE,
5374                    null, null, null, null, null);
5375            try {
5376                while (cursor.moveToNext()) {
5377                    long rawContactId = cursor.getLong(0);
5378                    mContactAggregator.markForAggregation(rawContactId,
5379                            RawContacts.AGGREGATION_MODE_DEFAULT, true);
5380                    count++;
5381                }
5382            } finally {
5383                cursor.close();
5384            }
5385            mContactAggregator.aggregateInTransaction(mTransactionContext, mDb);
5386            updateSearchIndexInTransaction();
5387            mDb.setTransactionSuccessful();
5388            mDbHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM,
5389                    String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
5390        } finally {
5391            mDb.endTransaction();
5392            long end = SystemClock.currentThreadTimeMillis();
5393            Log.i(TAG, "Aggregation algorithm upgraded for " + count
5394                    + " contacts, in " + (end - start) + "ms");
5395        }
5396    }
5397
5398    /* Visible for testing */
5399    boolean isPhone() {
5400        if (!sIsPhoneInitialized) {
5401            sIsPhone = new TelephonyManager(getContext()).isVoiceCapable();
5402            sIsPhoneInitialized = true;
5403        }
5404        return sIsPhone;
5405    }
5406}
5407