/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.providers.contacts; import com.android.internal.content.SyncStateContentProviderHelper; import com.android.providers.contacts.ContactLookupKey.LookupKeySegment; import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Clauses; import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns; import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns; import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.android.vcard.VCardComposer; import com.android.vcard.VCardConfig; import com.google.android.collect.Lists; import com.google.android.collect.Maps; import com.google.android.collect.Sets; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.OnAccountsUpdateListener; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.SearchManager; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.IContentService; import android.content.Intent; import android.content.OperationApplicationException; import android.content.SharedPreferences; import android.content.SyncAdapterType; import android.content.UriMatcher; import android.content.res.AssetFileDescriptor; import android.content.res.Configuration; import android.database.CharArrayBuffer; import android.database.Cursor; import android.database.CursorWrapper; import android.database.DatabaseUtils; import android.database.MatrixCursor; import android.database.MatrixCursor.RowBuilder; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteContentHelper; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.database.sqlite.SQLiteStatement; import android.net.Uri; import android.net.Uri.Builder; import android.os.AsyncTask; import android.os.Bundle; import android.os.MemoryFile; import android.os.RemoteException; import android.os.SystemProperties; import android.preference.PreferenceManager; import android.provider.BaseColumns; import android.provider.ContactsContract; import android.provider.ContactsContract.AggregationExceptions; import android.provider.ContactsContract.CommonDataKinds.BaseTypes; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.GroupMembership; import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Nickname; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Photo; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.ContactCounts; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.Directory; import android.provider.ContactsContract.DisplayNameSources; import android.provider.ContactsContract.FullNameStyle; import android.provider.ContactsContract.Groups; import android.provider.ContactsContract.Intents; import android.provider.ContactsContract.PhoneLookup; import android.provider.ContactsContract.PhoneticNameStyle; import android.provider.ContactsContract.ProviderStatus; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.SearchSnippetColumns; import android.provider.ContactsContract.Settings; import android.provider.ContactsContract.StatusUpdates; import android.provider.LiveFolders; import android.provider.OpenableColumns; import android.provider.SyncStateContract; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; /** * Contacts content provider. The contract between this provider and applications * is defined in {@link ContactsContract}. */ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener { private static final String TAG = "ContactsProvider"; private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); // TODO: carefully prevent all incoming nested queries; they can be gaping security holes // TODO: check for restricted flag during insert(), update(), and delete() calls /** Default for the maximum number of returned aggregation suggestions. */ private static final int DEFAULT_MAX_SUGGESTIONS = 5; /** * Property key for the legacy contact import version. The need for a version * as opposed to a boolean flag is that if we discover bugs in the contact import process, * we can trigger re-import by incrementing the import version. */ private static final String PROPERTY_CONTACTS_IMPORTED = "contacts_imported_v1"; private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1; private static final String PREF_LOCALE = "locale"; private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate"; private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); private static final String TIMES_CONTACTED_SORT_COLUMN = "times_contacted_sort"; private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, " + TIMES_CONTACTED_SORT_COLUMN + " DESC, " + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; private static final String STREQUENT_LIMIT = "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE " + Contacts.STARRED + "=1) + 25"; /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE = "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" + " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " + " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?"; /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE = "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" + " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " + " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?"; /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK"; private static final int CONTACTS = 1000; private static final int CONTACTS_ID = 1001; private static final int CONTACTS_LOOKUP = 1002; private static final int CONTACTS_LOOKUP_ID = 1003; private static final int CONTACTS_ID_DATA = 1004; private static final int CONTACTS_FILTER = 1005; private static final int CONTACTS_STREQUENT = 1006; private static final int CONTACTS_STREQUENT_FILTER = 1007; private static final int CONTACTS_GROUP = 1008; private static final int CONTACTS_ID_PHOTO = 1009; private static final int CONTACTS_AS_VCARD = 1010; private static final int CONTACTS_AS_MULTI_VCARD = 1011; private static final int CONTACTS_LOOKUP_DATA = 1012; private static final int CONTACTS_LOOKUP_ID_DATA = 1013; private static final int CONTACTS_ID_ENTITIES = 1014; private static final int CONTACTS_LOOKUP_ENTITIES = 1015; private static final int CONTACTS_LOOKUP_ID_ENTITIES = 1016; private static final int RAW_CONTACTS = 2002; private static final int RAW_CONTACTS_ID = 2003; private static final int RAW_CONTACTS_DATA = 2004; private static final int RAW_CONTACT_ENTITY_ID = 2005; private static final int DATA = 3000; private static final int DATA_ID = 3001; private static final int PHONES = 3002; private static final int PHONES_ID = 3003; private static final int PHONES_FILTER = 3004; private static final int EMAILS = 3005; private static final int EMAILS_ID = 3006; private static final int EMAILS_LOOKUP = 3007; private static final int EMAILS_FILTER = 3008; private static final int POSTALS = 3009; private static final int POSTALS_ID = 3010; private static final int PHONE_LOOKUP = 4000; private static final int AGGREGATION_EXCEPTIONS = 6000; private static final int AGGREGATION_EXCEPTION_ID = 6001; private static final int STATUS_UPDATES = 7000; private static final int STATUS_UPDATES_ID = 7001; private static final int AGGREGATION_SUGGESTIONS = 8000; private static final int SETTINGS = 9000; private static final int GROUPS = 10000; private static final int GROUPS_ID = 10001; private static final int GROUPS_SUMMARY = 10003; private static final int SYNCSTATE = 11000; private static final int SYNCSTATE_ID = 11001; private static final int SEARCH_SUGGESTIONS = 12001; private static final int SEARCH_SHORTCUT = 12002; private static final int LIVE_FOLDERS_CONTACTS = 14000; private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001; private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002; private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003; private static final int RAW_CONTACT_ENTITIES = 15001; private static final int PROVIDER_STATUS = 16001; private static final int DIRECTORIES = 17001; private static final int DIRECTORIES_ID = 17002; private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID = RawContactsColumns.CONCRETE_ID + "=? AND " + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND " + Groups.FAVORITES + " != 0"; private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID = RawContactsColumns.CONCRETE_ID + "=? AND " + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND " + Groups.AUTO_ADD + " != 0"; private static final String[] PROJECTION_GROUP_ID = new String[]{Tables.GROUPS + "." + Groups._ID}; private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? " + "AND " + GroupMembership.GROUP_ROW_ID + "=? " + "AND " + GroupMembership.RAW_CONTACT_ID + "=?"; private static final String SELECTION_STARRED_FROM_RAW_CONTACTS = "SELECT " + RawContacts.STARRED + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?"; private interface DataContactsQuery { public static final String TABLE = "data " + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) " + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; public static final String[] PROJECTION = new String[] { RawContactsColumns.CONCRETE_ID, DataColumns.CONCRETE_ID, ContactsColumns.CONCRETE_ID }; public static final int RAW_CONTACT_ID = 0; public static final int DATA_ID = 1; public static final int CONTACT_ID = 2; } private interface DataDeleteQuery { public static final String TABLE = Tables.DATA_JOIN_MIMETYPES; public static final String[] CONCRETE_COLUMNS = new String[] { DataColumns.CONCRETE_ID, MimetypesColumns.MIMETYPE, Data.RAW_CONTACT_ID, Data.IS_PRIMARY, Data.DATA1, }; public static final String[] COLUMNS = new String[] { Data._ID, MimetypesColumns.MIMETYPE, Data.RAW_CONTACT_ID, Data.IS_PRIMARY, Data.DATA1, }; public static final int _ID = 0; public static final int MIMETYPE = 1; public static final int RAW_CONTACT_ID = 2; public static final int IS_PRIMARY = 3; public static final int DATA1 = 4; } private interface DataUpdateQuery { String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE }; int _ID = 0; int RAW_CONTACT_ID = 1; int MIMETYPE = 2; } private interface RawContactsQuery { String TABLE = Tables.RAW_CONTACTS; String[] COLUMNS = new String[] { RawContacts.DELETED, RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME, }; int DELETED = 0; int ACCOUNT_TYPE = 1; int ACCOUNT_NAME = 2; } public static final String DEFAULT_ACCOUNT_TYPE = "com.google"; public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google"; /** Sql where statement for filtering on groups. */ private static final String CONTACTS_IN_GROUP_SELECT = Contacts._ID + " IN " + "(SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN " + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID + " FROM " + Tables.DATA_JOIN_MIMETYPES + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND " + GroupMembership.GROUP_ROW_ID + "=" + "(SELECT " + Tables.GROUPS + "." + Groups._ID + " FROM " + Tables.GROUPS + " WHERE " + Groups.TITLE + "=?)))"; /** Sql for updating DIRTY flag on multiple raw contacts */ private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL = "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.DIRTY + "=1" + " WHERE " + RawContacts._ID + " IN ("; /** Sql for updating VERSION on multiple raw contacts */ private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL = "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" + " WHERE " + RawContacts._ID + " IN ("; /** Name lookup types used for contact filtering */ private static final String CONTACT_LOOKUP_NAME_TYPES = NameLookupType.NAME_COLLATION_KEY + "," + NameLookupType.EMAIL_BASED_NICKNAME + "," + NameLookupType.NICKNAME + "," + NameLookupType.NAME_SHORTHAND + "," + NameLookupType.ORGANIZATION + "," + NameLookupType.NAME_CONSONANTS; private static final ProjectionMap sContactsColumns = ProjectionMap.builder() .add(Contacts.CUSTOM_RINGTONE) .add(Contacts.DISPLAY_NAME) .add(Contacts.DISPLAY_NAME_ALTERNATIVE) .add(Contacts.DISPLAY_NAME_SOURCE) .add(Contacts.IN_VISIBLE_GROUP) .add(Contacts.LAST_TIME_CONTACTED) .add(Contacts.LOOKUP_KEY) .add(Contacts.PHONETIC_NAME) .add(Contacts.PHONETIC_NAME_STYLE) .add(Contacts.PHOTO_ID) .add(Contacts.SEND_TO_VOICEMAIL) .add(Contacts.SORT_KEY_ALTERNATIVE) .add(Contacts.SORT_KEY_PRIMARY) .add(Contacts.STARRED) .add(Contacts.TIMES_CONTACTED) .build(); private static final ProjectionMap sContactsPresenceColumns = ProjectionMap.builder() .add(Contacts.CONTACT_PRESENCE, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE) .add(Contacts.CONTACT_CHAT_CAPABILITY, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY) .add(Contacts.CONTACT_STATUS, ContactsStatusUpdatesColumns.CONCRETE_STATUS) .add(Contacts.CONTACT_STATUS_TIMESTAMP, ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP) .add(Contacts.CONTACT_STATUS_RES_PACKAGE, ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE) .add(Contacts.CONTACT_STATUS_LABEL, ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL) .add(Contacts.CONTACT_STATUS_ICON, ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON) .build(); private static final ProjectionMap sSnippetColumns = ProjectionMap.builder() .add(SearchSnippetColumns.SNIPPET_MIMETYPE) .add(SearchSnippetColumns.SNIPPET_DATA_ID) .add(SearchSnippetColumns.SNIPPET_DATA1) .add(SearchSnippetColumns.SNIPPET_DATA2) .add(SearchSnippetColumns.SNIPPET_DATA3) .add(SearchSnippetColumns.SNIPPET_DATA4) .build(); private static final ProjectionMap sRawContactColumns = ProjectionMap.builder() .add(RawContacts.ACCOUNT_NAME) .add(RawContacts.ACCOUNT_TYPE) .add(RawContacts.DIRTY) .add(RawContacts.NAME_VERIFIED) .add(RawContacts.SOURCE_ID) .add(RawContacts.VERSION) .build(); private static final ProjectionMap sRawContactSyncColumns = ProjectionMap.builder() .add(RawContacts.SYNC1) .add(RawContacts.SYNC2) .add(RawContacts.SYNC3) .add(RawContacts.SYNC4) .build(); private static final ProjectionMap sDataColumns = ProjectionMap.builder() .add(Data.DATA1) .add(Data.DATA2) .add(Data.DATA3) .add(Data.DATA4) .add(Data.DATA5) .add(Data.DATA6) .add(Data.DATA7) .add(Data.DATA8) .add(Data.DATA9) .add(Data.DATA10) .add(Data.DATA11) .add(Data.DATA12) .add(Data.DATA13) .add(Data.DATA14) .add(Data.DATA15) .add(Data.DATA_VERSION) .add(Data.IS_PRIMARY) .add(Data.IS_SUPER_PRIMARY) .add(Data.MIMETYPE) .add(Data.RES_PACKAGE) .add(Data.SYNC1) .add(Data.SYNC2) .add(Data.SYNC3) .add(Data.SYNC4) .add(GroupMembership.GROUP_SOURCE_ID) .build(); private static final ProjectionMap sContactPresenceColumns = ProjectionMap.builder() .add(Contacts.CONTACT_PRESENCE, Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.PRESENCE) .add(Contacts.CONTACT_CHAT_CAPABILITY, Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.CHAT_CAPABILITY) .add(Contacts.CONTACT_STATUS, ContactsStatusUpdatesColumns.CONCRETE_STATUS) .add(Contacts.CONTACT_STATUS_TIMESTAMP, ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP) .add(Contacts.CONTACT_STATUS_RES_PACKAGE, ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE) .add(Contacts.CONTACT_STATUS_LABEL, ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL) .add(Contacts.CONTACT_STATUS_ICON, ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON) .build(); private static final ProjectionMap sDataPresenceColumns = ProjectionMap.builder() .add(Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE) .add(Data.CHAT_CAPABILITY, Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY) .add(Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS) .add(Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP) .add(Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE) .add(Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL) .add(Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON) .build(); /** Contains just BaseColumns._COUNT */ private static final ProjectionMap sCountProjectionMap = ProjectionMap.builder() .add(BaseColumns._COUNT, "COUNT(*)") .build(); /** Contains just the contacts columns */ private static final ProjectionMap sContactsProjectionMap = ProjectionMap.builder() .add(Contacts._ID) .add(Contacts.HAS_PHONE_NUMBER) .add(Contacts.NAME_RAW_CONTACT_ID) .addAll(sContactsColumns) .addAll(sContactsPresenceColumns) .build(); /** Contains just the contacts columns */ private static final ProjectionMap sContactsProjectionWithSnippetMap = ProjectionMap.builder() .addAll(sContactsProjectionMap) .addAll(sSnippetColumns) .build(); /** Used for pushing starred contacts to the top of a times contacted list **/ private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder() .addAll(sContactsProjectionMap) .add(TIMES_CONTACTED_SORT_COLUMN, String.valueOf(Long.MAX_VALUE)) .build(); private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder() .addAll(sContactsProjectionMap) .add(TIMES_CONTACTED_SORT_COLUMN, Contacts.TIMES_CONTACTED) .build(); /** Contains just the contacts vCard columns */ private static final ProjectionMap sContactsVCardProjectionMap = ProjectionMap.builder() .add(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf'") .add(OpenableColumns.SIZE, "NULL") .build(); /** Contains just the raw contacts columns */ private static final ProjectionMap sRawContactsProjectionMap = ProjectionMap.builder() .add(RawContacts._ID) .add(RawContacts.CONTACT_ID) .add(RawContacts.DELETED) .add(RawContacts.DISPLAY_NAME_PRIMARY) .add(RawContacts.DISPLAY_NAME_ALTERNATIVE) .add(RawContacts.DISPLAY_NAME_SOURCE) .add(RawContacts.PHONETIC_NAME) .add(RawContacts.PHONETIC_NAME_STYLE) .add(RawContacts.SORT_KEY_PRIMARY) .add(RawContacts.SORT_KEY_ALTERNATIVE) .add(RawContacts.TIMES_CONTACTED) .add(RawContacts.LAST_TIME_CONTACTED) .add(RawContacts.CUSTOM_RINGTONE) .add(RawContacts.SEND_TO_VOICEMAIL) .add(RawContacts.STARRED) .add(RawContacts.AGGREGATION_MODE) .addAll(sRawContactColumns) .addAll(sRawContactSyncColumns) .build(); /** Contains the columns from the raw entity view*/ private static final ProjectionMap sRawEntityProjectionMap = ProjectionMap.builder() .add(RawContacts._ID) .add(RawContacts.CONTACT_ID) .add(RawContacts.Entity.DATA_ID) .add(RawContacts.IS_RESTRICTED) .add(RawContacts.DELETED) .add(RawContacts.STARRED) .addAll(sRawContactColumns) .addAll(sRawContactSyncColumns) .addAll(sDataColumns) .build(); /** Contains the columns from the contact entity view*/ private static final ProjectionMap sEntityProjectionMap = ProjectionMap.builder() .add(Contacts.Entity._ID) .add(Contacts.Entity.CONTACT_ID) .add(Contacts.Entity.RAW_CONTACT_ID) .add(Contacts.Entity.DATA_ID) .add(Contacts.Entity.NAME_RAW_CONTACT_ID) .add(Contacts.Entity.DELETED) .add(Contacts.Entity.IS_RESTRICTED) .addAll(sContactsColumns) .addAll(sContactPresenceColumns) .addAll(sRawContactColumns) .addAll(sRawContactSyncColumns) .addAll(sDataColumns) .addAll(sDataPresenceColumns) .build(); /** Contains columns from the data view */ private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder() .add(Data._ID) .add(Data.RAW_CONTACT_ID) .add(Data.CONTACT_ID) .add(Data.NAME_RAW_CONTACT_ID) .addAll(sDataColumns) .addAll(sDataPresenceColumns) .addAll(sRawContactColumns) .addAll(sContactsColumns) .addAll(sContactPresenceColumns) .build(); /** Contains columns from the data view */ private static final ProjectionMap sDistinctDataProjectionMap = ProjectionMap.builder() .add(Data._ID, "MIN(" + Data._ID + ")") .add(RawContacts.CONTACT_ID) .addAll(sDataColumns) .addAll(sDataPresenceColumns) .addAll(sContactsColumns) .addAll(sContactPresenceColumns) .build(); /** Contains the data and contacts columns, for joined tables */ private static final ProjectionMap sPhoneLookupProjectionMap = ProjectionMap.builder() .add(PhoneLookup._ID, "contacts_view." + Contacts._ID) .add(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY) .add(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME) .add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED) .add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED) .add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED) .add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP) .add(PhoneLookup.PHOTO_ID, "contacts_view." + Contacts.PHOTO_ID) .add(PhoneLookup.CUSTOM_RINGTONE, "contacts_view." + Contacts.CUSTOM_RINGTONE) .add(PhoneLookup.HAS_PHONE_NUMBER, "contacts_view." + Contacts.HAS_PHONE_NUMBER) .add(PhoneLookup.SEND_TO_VOICEMAIL, "contacts_view." + Contacts.SEND_TO_VOICEMAIL) .add(PhoneLookup.NUMBER, Phone.NUMBER) .add(PhoneLookup.TYPE, Phone.TYPE) .add(PhoneLookup.LABEL, Phone.LABEL) .build(); /** Contains the just the {@link Groups} columns */ private static final ProjectionMap sGroupsProjectionMap = ProjectionMap.builder() .add(Groups._ID) .add(Groups.ACCOUNT_NAME) .add(Groups.ACCOUNT_TYPE) .add(Groups.SOURCE_ID) .add(Groups.DIRTY) .add(Groups.VERSION) .add(Groups.RES_PACKAGE) .add(Groups.TITLE) .add(Groups.TITLE_RES) .add(Groups.GROUP_VISIBLE) .add(Groups.SYSTEM_ID) .add(Groups.DELETED) .add(Groups.NOTES) .add(Groups.SHOULD_SYNC) .add(Groups.FAVORITES) .add(Groups.AUTO_ADD) .add(Groups.SYNC1) .add(Groups.SYNC2) .add(Groups.SYNC3) .add(Groups.SYNC4) .build(); /** Contains {@link Groups} columns along with summary details */ private static final ProjectionMap sGroupsSummaryProjectionMap = ProjectionMap.builder() .addAll(sGroupsProjectionMap) .add(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP + ")") .add(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP + " AND " + Contacts.HAS_PHONE_NUMBER + ")") .build(); /** Contains the agg_exceptions columns */ private static final ProjectionMap sAggregationExceptionsProjectionMap = ProjectionMap.builder() .add(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id") .add(AggregationExceptions.TYPE) .add(AggregationExceptions.RAW_CONTACT_ID1) .add(AggregationExceptions.RAW_CONTACT_ID2) .build(); /** Contains the agg_exceptions columns */ private static final ProjectionMap sSettingsProjectionMap = ProjectionMap.builder() .add(Settings.ACCOUNT_NAME) .add(Settings.ACCOUNT_TYPE) .add(Settings.UNGROUPED_VISIBLE) .add(Settings.SHOULD_SYNC) .add(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC + ",(SELECT " + "(CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL" + " THEN 1" + " ELSE MIN(" + Groups.SHOULD_SYNC + ")" + " END)" + " FROM " + Tables.GROUPS + " WHERE " + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0" + " THEN 1" + " ELSE 0" + " END)") .add(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*)" + " FROM (SELECT 1" + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS + "))") .add(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*)" + " FROM (SELECT 1" + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE " + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS + "))") .build(); /** Contains StatusUpdates columns */ private static final ProjectionMap sStatusUpdatesProjectionMap = ProjectionMap.builder() .add(PresenceColumns.RAW_CONTACT_ID) .add(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID) .add(StatusUpdates.IM_ACCOUNT) .add(StatusUpdates.IM_HANDLE) .add(StatusUpdates.PROTOCOL) // We cannot allow a null in the custom protocol field, because SQLite3 does not // properly enforce uniqueness of null values .add(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL + "=''" + " THEN NULL" + " ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END)") .add(StatusUpdates.PRESENCE) .add(StatusUpdates.CHAT_CAPABILITY) .add(StatusUpdates.STATUS) .add(StatusUpdates.STATUS_TIMESTAMP) .add(StatusUpdates.STATUS_RES_PACKAGE) .add(StatusUpdates.STATUS_ICON) .add(StatusUpdates.STATUS_LABEL) .build(); /** Contains Live Folders columns */ private static final ProjectionMap sLiveFoldersProjectionMap = ProjectionMap.builder() .add(LiveFolders._ID, Contacts._ID) .add(LiveFolders.NAME, Contacts.DISPLAY_NAME) // TODO: Put contact photo back when we have a way to display a default icon // for contacts without a photo // .add(LiveFolders.ICON_BITMAP, Photos.DATA) .build(); /** Contains {@link Directory} columns */ private static final ProjectionMap sDirectoryProjectionMap = ProjectionMap.builder() .add(Directory._ID) .add(Directory.PACKAGE_NAME) .add(Directory.TYPE_RESOURCE_ID) .add(Directory.DISPLAY_NAME) .add(Directory.DIRECTORY_AUTHORITY) .add(Directory.ACCOUNT_TYPE) .add(Directory.ACCOUNT_NAME) .add(Directory.EXPORT_SUPPORT) .build(); // where clause to update the status_updates table private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE = StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID + " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE + " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE "; private static final String[] EMPTY_STRING_ARRAY = new String[0]; /** * Notification ID for failure to import contacts. */ private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1; /** Precompiled sql statement for setting a data record to the primary. */ private SQLiteStatement mSetPrimaryStatement; /** Precompiled sql statement for setting a data record to the super primary. */ private SQLiteStatement mSetSuperPrimaryStatement; /** Precompiled sql statement for updating a contact display name */ private SQLiteStatement mRawContactDisplayNameUpdate; /** Precompiled sql statement for updating an aggregated status update */ private SQLiteStatement mLastStatusUpdate; private SQLiteStatement mNameLookupInsert; private SQLiteStatement mNameLookupDelete; private SQLiteStatement mStatusUpdateAutoTimestamp; private SQLiteStatement mStatusUpdateInsert; private SQLiteStatement mStatusUpdateReplace; private SQLiteStatement mStatusAttributionUpdate; private SQLiteStatement mStatusUpdateDelete; private SQLiteStatement mResetNameVerifiedForOtherRawContacts; private long mMimeTypeIdEmail; private long mMimeTypeIdIm; private long mMimeTypeIdStructuredName; private long mMimeTypeIdOrganization; private long mMimeTypeIdNickname; private long mMimeTypeIdPhone; private StringBuilder mSb = new StringBuilder(); private String[] mSelectionArgs1 = new String[1]; private String[] mSelectionArgs2 = new String[2]; private ArrayList mSelectionArgs = Lists.newArrayList(); private Account mAccount; static { // Contacts URI matching table final UriMatcher matcher = sUriMatcher; matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS); matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_ID_DATA); matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/entities", CONTACTS_ID_ENTITIES); matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", AGGREGATION_SUGGESTIONS); matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*", AGGREGATION_SUGGESTIONS); matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO); matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER); matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER); matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP); matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA); matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID); matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data", CONTACTS_LOOKUP_ID_DATA); matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/entities", CONTACTS_LOOKUP_ENTITIES); matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/entities", CONTACTS_LOOKUP_ID_ENTITIES); matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD); matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*", CONTACTS_AS_MULTI_VCARD); matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT); matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", CONTACTS_STREQUENT_FILTER); matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP); matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS); matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID); matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA); matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID); matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES); matcher.addURI(ContactsContract.AUTHORITY, "data", DATA); matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID); matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES); matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID); matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER); matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER); matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS); matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID); matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP); matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER); matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER); matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS); matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID); matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS); matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID); matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY); matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE); matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#", SYNCSTATE_ID); matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP); matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions", AGGREGATION_EXCEPTIONS); matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*", AGGREGATION_EXCEPTION_ID); matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS); matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES); matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID); matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGESTIONS); matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGESTIONS); matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SEARCH_SHORTCUT); matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts", LIVE_FOLDERS_CONTACTS); matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*", LIVE_FOLDERS_CONTACTS_GROUP_NAME); matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones", LIVE_FOLDERS_CONTACTS_WITH_PHONES); matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites", LIVE_FOLDERS_CONTACTS_FAVORITES); matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS); matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES); matcher.addURI(ContactsContract.AUTHORITY, "directories/#", DIRECTORIES_ID); } private static class DirectoryInfo { String authority; String accountName; String accountType; } /** * Cached information about contact directories. */ private HashMap mDirectoryCache; /** * Handles inserts and update for a specific Data type. */ private abstract class DataRowHandler { protected final String mMimetype; protected long mMimetypeId; @SuppressWarnings("all") public DataRowHandler(String mimetype) { mMimetype = mimetype; // To ensure the data column position. This is dead code if properly configured. if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1 || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1 || Email.DATA != Data.DATA1) { throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary" + " data is not in DATA1 column"); } } protected long getMimeTypeId() { if (mMimetypeId == 0) { mMimetypeId = mDbHelper.getMimeTypeId(mMimetype); } return mMimetypeId; } /** * Inserts a row into the {@link Data} table. */ public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { final long dataId = db.insert(Tables.DATA, null, values); Integer primary = values.getAsInteger(Data.IS_PRIMARY); if (primary != null && primary != 0) { setIsPrimary(rawContactId, dataId, getMimeTypeId()); } return dataId; } /** * Validates data and updates a {@link Data} row using the cursor, which contains * the current data. * * @return true if update changed something */ public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { long dataId = c.getLong(DataUpdateQuery._ID); long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); if (values.containsKey(Data.IS_SUPER_PRIMARY)) { long mimeTypeId = getMimeTypeId(); setIsSuperPrimary(rawContactId, dataId, mimeTypeId); setIsPrimary(rawContactId, dataId, mimeTypeId); // Now that we've taken care of setting these, remove them from "values". values.remove(Data.IS_SUPER_PRIMARY); values.remove(Data.IS_PRIMARY); } else if (values.containsKey(Data.IS_PRIMARY)) { setIsPrimary(rawContactId, dataId, getMimeTypeId()); // Now that we've taken care of setting this, remove it from "values". values.remove(Data.IS_PRIMARY); } if (values.size() > 0) { mSelectionArgs1[0] = String.valueOf(dataId); mDb.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1); } if (!callerIsSyncAdapter) { setRawContactDirty(rawContactId); } return true; } public int delete(SQLiteDatabase db, Cursor c) { long dataId = c.getLong(DataDeleteQuery._ID); long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0; mSelectionArgs1[0] = String.valueOf(dataId); int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1); mSelectionArgs1[0] = String.valueOf(rawContactId); db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1); if (count != 0 && primary) { fixPrimary(db, rawContactId); } return count; } private void fixPrimary(SQLiteDatabase db, long rawContactId) { long mimeTypeId = getMimeTypeId(); long primaryId = -1; int primaryType = -1; mSelectionArgs1[0] = String.valueOf(rawContactId); Cursor c = db.query(DataDeleteQuery.TABLE, DataDeleteQuery.CONCRETE_COLUMNS, Data.RAW_CONTACT_ID + "=?" + " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId, mSelectionArgs1, null, null, null); try { while (c.moveToNext()) { long dataId = c.getLong(DataDeleteQuery._ID); int type = c.getInt(DataDeleteQuery.DATA1); if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) { primaryId = dataId; primaryType = type; } } } finally { c.close(); } if (primaryId != -1) { setIsPrimary(rawContactId, primaryId, mimeTypeId); } } /** * Returns the rank of a specific record type to be used in determining the primary * row. Lower number represents higher priority. */ protected int getTypeRank(int type) { return 0; } protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) { if (!isNewRawContact(rawContactId)) { updateRawContactDisplayName(db, rawContactId); mContactAggregator.updateDisplayNameForRawContact(db, rawContactId); } } /** * Return set of values, using current values at given {@link Data#_ID} * as baseline, but augmented with any updates. Returns null if there is * no change. */ public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId, ContentValues update) { boolean changing = false; final ContentValues values = new ContentValues(); mSelectionArgs1[0] = String.valueOf(dataId); final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?", mSelectionArgs1, null, null, null); try { if (cursor.moveToFirst()) { for (int i = 0; i < cursor.getColumnCount(); i++) { final String key = cursor.getColumnName(i); final String value = cursor.getString(i); if (!changing && update.containsKey(key)) { Object newValue = update.get(key); String newString = newValue == null ? null : newValue.toString(); changing |= !TextUtils.equals(newString, value); } values.put(key, value); } } } finally { cursor.close(); } if (!changing) { return null; } values.putAll(update); return values; } } public class CustomDataRowHandler extends DataRowHandler { public CustomDataRowHandler(String mimetype) { super(mimetype); } } public class StructuredNameRowHandler extends DataRowHandler { private final NameSplitter mSplitter; public StructuredNameRowHandler(NameSplitter splitter) { super(StructuredName.CONTENT_ITEM_TYPE); mSplitter = splitter; } @Override public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { fixStructuredNameComponents(values, values); long dataId = super.insert(db, rawContactId, values); String name = values.getAsString(StructuredName.DISPLAY_NAME); Integer fullNameStyle = values.getAsInteger(StructuredName.FULL_NAME_STYLE); insertNameLookupForStructuredName(rawContactId, dataId, name, fullNameStyle != null ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle) : FullNameStyle.UNDEFINED); insertNameLookupForPhoneticName(rawContactId, dataId, values); fixRawContactDisplayName(db, rawContactId); triggerAggregation(rawContactId); return dataId; } @Override public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { final long dataId = c.getLong(DataUpdateQuery._ID); final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); final ContentValues augmented = getAugmentedValues(db, dataId, values); if (augmented == null) { // No change return false; } fixStructuredNameComponents(augmented, values); super.update(db, values, c, callerIsSyncAdapter); if (values.containsKey(StructuredName.DISPLAY_NAME) || values.containsKey(StructuredName.PHONETIC_FAMILY_NAME) || values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME) || values.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) { augmented.putAll(values); String name = augmented.getAsString(StructuredName.DISPLAY_NAME); deleteNameLookup(dataId); Integer fullNameStyle = augmented.getAsInteger(StructuredName.FULL_NAME_STYLE); insertNameLookupForStructuredName(rawContactId, dataId, name, fullNameStyle != null ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle) : FullNameStyle.UNDEFINED); insertNameLookupForPhoneticName(rawContactId, dataId, augmented); } fixRawContactDisplayName(db, rawContactId); triggerAggregation(rawContactId); return true; } @Override public int delete(SQLiteDatabase db, Cursor c) { long dataId = c.getLong(DataDeleteQuery._ID); long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); int count = super.delete(db, c); deleteNameLookup(dataId); fixRawContactDisplayName(db, rawContactId); triggerAggregation(rawContactId); return count; } /** * Specific list of structured fields. */ private final String[] STRUCTURED_FIELDS = new String[] { StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME, StructuredName.SUFFIX }; /** * Parses the supplied display name, but only if the incoming values do * not already contain structured name parts. Also, if the display name * is not provided, generate one by concatenating first name and last * name. */ private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) { final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME); final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); if (touchedUnstruct && !touchedStruct) { NameSplitter.Name name = new NameSplitter.Name(); mSplitter.split(name, unstruct); name.toValues(update); } else if (!touchedUnstruct && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) { // We need to update the display name when any structured components // are specified, even when they are null, which is why we are checking // areAnySpecified. The touchedStruct in the condition is an optimization: // if there are non-null values, we know for a fact that some values are present. NameSplitter.Name name = new NameSplitter.Name(); name.fromValues(augmented); // As the name could be changed, let's guess the name style again. name.fullNameStyle = FullNameStyle.UNDEFINED; mSplitter.guessNameStyle(name); int unadjustedFullNameStyle = name.fullNameStyle; name.fullNameStyle = mSplitter.getAdjustedFullNameStyle(name.fullNameStyle); final String joined = mSplitter.join(name, true); update.put(StructuredName.DISPLAY_NAME, joined); update.put(StructuredName.FULL_NAME_STYLE, unadjustedFullNameStyle); update.put(StructuredName.PHONETIC_NAME_STYLE, name.phoneticNameStyle); } else if (touchedUnstruct && touchedStruct){ if (!update.containsKey(StructuredName.FULL_NAME_STYLE)) { update.put(StructuredName.FULL_NAME_STYLE, mSplitter.guessFullNameStyle(unstruct)); } if (!update.containsKey(StructuredName.PHONETIC_NAME_STYLE)) { update.put(StructuredName.PHONETIC_NAME_STYLE, mSplitter.guessPhoneticNameStyle(unstruct)); } } } } public class StructuredPostalRowHandler extends DataRowHandler { private PostalSplitter mSplitter; public StructuredPostalRowHandler(PostalSplitter splitter) { super(StructuredPostal.CONTENT_ITEM_TYPE); mSplitter = splitter; } @Override public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { fixStructuredPostalComponents(values, values); return super.insert(db, rawContactId, values); } @Override public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { final long dataId = c.getLong(DataUpdateQuery._ID); final ContentValues augmented = getAugmentedValues(db, dataId, values); if (augmented == null) { // No change return false; } fixStructuredPostalComponents(augmented, values); super.update(db, values, c, callerIsSyncAdapter); return true; } /** * Specific list of structured fields. */ private final String[] STRUCTURED_FIELDS = new String[] { StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD, StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE, StructuredPostal.COUNTRY, }; /** * Prepares the given {@link StructuredPostal} row, building * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured * values when missing. When structured components are missing, the * unstructured value is assigned to {@link StructuredPostal#STREET}. */ private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) { final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS); final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); final PostalSplitter.Postal postal = new PostalSplitter.Postal(); if (touchedUnstruct && !touchedStruct) { mSplitter.split(postal, unstruct); postal.toValues(update); } else if (!touchedUnstruct && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) { // See comment in postal.fromValues(augmented); final String joined = mSplitter.join(postal); update.put(StructuredPostal.FORMATTED_ADDRESS, joined); } } } public class CommonDataRowHandler extends DataRowHandler { private final String mTypeColumn; private final String mLabelColumn; public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) { super(mimetype); mTypeColumn = typeColumn; mLabelColumn = labelColumn; } @Override public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { enforceTypeAndLabel(values, values); return super.insert(db, rawContactId, values); } @Override public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { final long dataId = c.getLong(DataUpdateQuery._ID); final ContentValues augmented = getAugmentedValues(db, dataId, values); if (augmented == null) { // No change return false; } enforceTypeAndLabel(augmented, values); return super.update(db, values, c, callerIsSyncAdapter); } /** * If the given {@link ContentValues} defines {@link #mTypeColumn}, * enforce that {@link #mLabelColumn} only appears when type is * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise. */ private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) { final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn)); final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn)); if (hasLabel && !hasType) { // When label exists, assert that some type is defined throw new IllegalArgumentException(mTypeColumn + " must be specified when " + mLabelColumn + " is defined."); } } } public class OrganizationDataRowHandler extends CommonDataRowHandler { public OrganizationDataRowHandler() { super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL); } @Override public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { String company = values.getAsString(Organization.COMPANY); String title = values.getAsString(Organization.TITLE); long dataId = super.insert(db, rawContactId, values); fixRawContactDisplayName(db, rawContactId); insertNameLookupForOrganization(rawContactId, dataId, company, title); return dataId; } @Override public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { if (!super.update(db, values, c, callerIsSyncAdapter)) { return false; } boolean containsCompany = values.containsKey(Organization.COMPANY); boolean containsTitle = values.containsKey(Organization.TITLE); if (containsCompany || containsTitle) { long dataId = c.getLong(DataUpdateQuery._ID); long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); String company; if (containsCompany) { company = values.getAsString(Organization.COMPANY); } else { mSelectionArgs1[0] = String.valueOf(dataId); company = DatabaseUtils.stringForQuery(db, "SELECT " + Organization.COMPANY + " FROM " + Tables.DATA + " WHERE " + Data._ID + "=?", mSelectionArgs1); } String title; if (containsTitle) { title = values.getAsString(Organization.TITLE); } else { mSelectionArgs1[0] = String.valueOf(dataId); title = DatabaseUtils.stringForQuery(db, "SELECT " + Organization.TITLE + " FROM " + Tables.DATA + " WHERE " + Data._ID + "=?", mSelectionArgs1); } deleteNameLookup(dataId); insertNameLookupForOrganization(rawContactId, dataId, company, title); fixRawContactDisplayName(db, rawContactId); } return true; } @Override public int delete(SQLiteDatabase db, Cursor c) { long dataId = c.getLong(DataUpdateQuery._ID); long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); int count = super.delete(db, c); fixRawContactDisplayName(db, rawContactId); deleteNameLookup(dataId); return count; } @Override protected int getTypeRank(int type) { switch (type) { case Organization.TYPE_WORK: return 0; case Organization.TYPE_CUSTOM: return 1; case Organization.TYPE_OTHER: return 2; default: return 1000; } } } public class EmailDataRowHandler extends CommonDataRowHandler { public EmailDataRowHandler() { super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL); } @Override public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { String email = values.getAsString(Email.DATA); long dataId = super.insert(db, rawContactId, values); fixRawContactDisplayName(db, rawContactId); String address = insertNameLookupForEmail(rawContactId, dataId, email); if (address != null) { triggerAggregation(rawContactId); } return dataId; } @Override public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { if (!super.update(db, values, c, callerIsSyncAdapter)) { return false; } if (values.containsKey(Email.DATA)) { long dataId = c.getLong(DataUpdateQuery._ID); long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); String address = values.getAsString(Email.DATA); deleteNameLookup(dataId); insertNameLookupForEmail(rawContactId, dataId, address); fixRawContactDisplayName(db, rawContactId); triggerAggregation(rawContactId); } return true; } @Override public int delete(SQLiteDatabase db, Cursor c) { long dataId = c.getLong(DataDeleteQuery._ID); long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); int count = super.delete(db, c); deleteNameLookup(dataId); fixRawContactDisplayName(db, rawContactId); triggerAggregation(rawContactId); return count; } @Override protected int getTypeRank(int type) { switch (type) { case Email.TYPE_HOME: return 0; case Email.TYPE_WORK: return 1; case Email.TYPE_CUSTOM: return 2; case Email.TYPE_OTHER: return 3; default: return 1000; } } } public class NicknameDataRowHandler extends CommonDataRowHandler { public NicknameDataRowHandler() { super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL); } @Override public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { String nickname = values.getAsString(Nickname.NAME); long dataId = super.insert(db, rawContactId, values); if (!TextUtils.isEmpty(nickname)) { fixRawContactDisplayName(db, rawContactId); insertNameLookupForNickname(rawContactId, dataId, nickname); triggerAggregation(rawContactId); } return dataId; } @Override public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { long dataId = c.getLong(DataUpdateQuery._ID); long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); if (!super.update(db, values, c, callerIsSyncAdapter)) { return false; } if (values.containsKey(Nickname.NAME)) { String nickname = values.getAsString(Nickname.NAME); deleteNameLookup(dataId); insertNameLookupForNickname(rawContactId, dataId, nickname); fixRawContactDisplayName(db, rawContactId); triggerAggregation(rawContactId); } return true; } @Override public int delete(SQLiteDatabase db, Cursor c) { long dataId = c.getLong(DataDeleteQuery._ID); long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); int count = super.delete(db, c); deleteNameLookup(dataId); fixRawContactDisplayName(db, rawContactId); triggerAggregation(rawContactId); return count; } } public class PhoneDataRowHandler extends CommonDataRowHandler { public PhoneDataRowHandler() { super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL); } @Override public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { long dataId; if (values.containsKey(Phone.NUMBER)) { String number = values.getAsString(Phone.NUMBER); String normalizedNumber = computeNormalizedNumber(number); values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber); dataId = super.insert(db, rawContactId, values); updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); mContactAggregator.updateHasPhoneNumber(db, rawContactId); fixRawContactDisplayName(db, rawContactId); if (normalizedNumber != null) { triggerAggregation(rawContactId); } } else { dataId = super.insert(db, rawContactId, values); } return dataId; } @Override public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { String number = null; String normalizedNumber = null; if (values.containsKey(Phone.NUMBER)) { number = values.getAsString(Phone.NUMBER); normalizedNumber = computeNormalizedNumber(number); values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber); } if (!super.update(db, values, c, callerIsSyncAdapter)) { return false; } if (values.containsKey(Phone.NUMBER)) { long dataId = c.getLong(DataUpdateQuery._ID); long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); mContactAggregator.updateHasPhoneNumber(db, rawContactId); fixRawContactDisplayName(db, rawContactId); triggerAggregation(rawContactId); } return true; } @Override public int delete(SQLiteDatabase db, Cursor c) { long dataId = c.getLong(DataDeleteQuery._ID); long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); int count = super.delete(db, c); updatePhoneLookup(db, rawContactId, dataId, null, null); mContactAggregator.updateHasPhoneNumber(db, rawContactId); fixRawContactDisplayName(db, rawContactId); triggerAggregation(rawContactId); return count; } private String computeNormalizedNumber(String number) { String normalizedNumber = null; if (number != null) { normalizedNumber = PhoneNumberUtils.getStrippedReversed(number); } return normalizedNumber; } private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId, String number, String normalizedNumber) { if (number != null) { ContentValues phoneValues = new ContentValues(); phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId); phoneValues.put(PhoneLookupColumns.DATA_ID, dataId); phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber); phoneValues.put(PhoneLookupColumns.MIN_MATCH, PhoneNumberUtils.toCallerIDMinMatch(number)); db.replace(Tables.PHONE_LOOKUP, null, phoneValues); } else { mSelectionArgs1[0] = String.valueOf(dataId); db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1); } } @Override protected int getTypeRank(int type) { switch (type) { case Phone.TYPE_MOBILE: return 0; case Phone.TYPE_WORK: return 1; case Phone.TYPE_HOME: return 2; case Phone.TYPE_PAGER: return 3; case Phone.TYPE_CUSTOM: return 4; case Phone.TYPE_OTHER: return 5; case Phone.TYPE_FAX_WORK: return 6; case Phone.TYPE_FAX_HOME: return 7; default: return 1000; } } } public class GroupMembershipRowHandler extends DataRowHandler { private static final String SELECTION_RAW_CONTACT_ID = RawContacts._ID + "=?"; private static final String QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID = "SELECT COUNT(*) FROM " + Tables.DATA + " LEFT OUTER JOIN " + Tables .GROUPS + " ON " + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + "=" + GroupsColumns.CONCRETE_ID + " WHERE " + DataColumns.MIMETYPE_ID + "=?" + " AND " + Tables.DATA + "." + GroupMembership.RAW_CONTACT_ID + "=?" + " AND " + Groups.FAVORITES + "!=0"; public GroupMembershipRowHandler() { super(GroupMembership.CONTENT_ITEM_TYPE); } @Override public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { resolveGroupSourceIdInValues(rawContactId, db, values, true); long dataId = super.insert(db, rawContactId, values); if (hasFavoritesGroupMembership(db, rawContactId)) { updateRawContactsStar(db, rawContactId, true /* starred */); } updateVisibility(rawContactId); return dataId; } @Override public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId); resolveGroupSourceIdInValues(rawContactId, db, values, false); if (!super.update(db, values, c, callerIsSyncAdapter)) { return false; } boolean isStarred = hasFavoritesGroupMembership(db, rawContactId); if (wasStarred != isStarred) { updateRawContactsStar(db, rawContactId, isStarred); } updateVisibility(rawContactId); return true; } private void updateRawContactsStar(SQLiteDatabase db, long rawContactId, boolean starred) { ContentValues rawContactValues = new ContentValues(); rawContactValues.put(RawContacts.STARRED, starred ? 1 : 0); if (db.update(Tables.RAW_CONTACTS, rawContactValues, SELECTION_RAW_CONTACT_ID, new String[]{Long.toString(rawContactId)}) > 0) { mContactAggregator.updateStarred(rawContactId); } } private boolean hasFavoritesGroupMembership(SQLiteDatabase db, long rawContactId) { final long groupMembershipMimetypeId = mDbHelper .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); boolean isStarred = 0 < DatabaseUtils .longForQuery(db, QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID, new String[]{Long.toString(groupMembershipMimetypeId), Long.toString(rawContactId)}); return isStarred; } @Override public int delete(SQLiteDatabase db, Cursor c) { long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId); int count = super.delete(db, c); boolean isStarred = hasFavoritesGroupMembership(db, rawContactId); if (wasStarred && !isStarred) { updateRawContactsStar(db, rawContactId, false /* starred */); } updateVisibility(rawContactId); return count; } private void updateVisibility(long rawContactId) { long contactId = mDbHelper.getContactId(rawContactId); if (contactId != 0) { mDbHelper.updateContactVisible(contactId); } } private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db, ContentValues values, boolean isInsert) { boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID); boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID); if (containsGroupSourceId && containsGroupId) { throw new IllegalArgumentException( "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID " + "and GroupMembership.GROUP_ROW_ID"); } if (!containsGroupSourceId && !containsGroupId) { if (isInsert) { throw new IllegalArgumentException( "you must set exactly one of GroupMembership.GROUP_SOURCE_ID " + "and GroupMembership.GROUP_ROW_ID"); } else { return; } } if (containsGroupSourceId) { final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID); final long groupId = getOrMakeGroup(db, rawContactId, sourceId, mInsertedRawContacts.get(rawContactId)); values.remove(GroupMembership.GROUP_SOURCE_ID); values.put(GroupMembership.GROUP_ROW_ID, groupId); } } } public class PhotoDataRowHandler extends DataRowHandler { public PhotoDataRowHandler() { super(Photo.CONTENT_ITEM_TYPE); } @Override public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { long dataId = super.insert(db, rawContactId, values); if (!isNewRawContact(rawContactId)) { mContactAggregator.updatePhotoId(db, rawContactId); } return dataId; } @Override public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); if (!super.update(db, values, c, callerIsSyncAdapter)) { return false; } mContactAggregator.updatePhotoId(db, rawContactId); return true; } @Override public int delete(SQLiteDatabase db, Cursor c) { long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); int count = super.delete(db, c); mContactAggregator.updatePhotoId(db, rawContactId); return count; } } /** * An entry in group id cache. It maps the combination of (account type, account name * and source id) to group row id. */ public class GroupIdCacheEntry { String accountType; String accountName; String sourceId; long groupId; } private HashMap mDataRowHandlers; private ContactsDatabaseHelper mDbHelper; private NameSplitter mNameSplitter; private NameLookupBuilder mNameLookupBuilder; private PostalSplitter mPostalSplitter; // We don't need a soft cache for groups - the assumption is that there will only // be a small number of contact groups. The cache is keyed off source id. The value // is a list of groups with this group id. private HashMap> mGroupIdCache = Maps.newHashMap(); private ContactDirectoryManager mContactDirectoryManager; private ContactAggregator mContactAggregator; private LegacyApiSupport mLegacyApiSupport; private GlobalSearchSupport mGlobalSearchSupport; private CommonNicknameCache mCommonNicknameCache; private ContentValues mValues = new ContentValues(); private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128); private NameSplitter.Name mName = new NameSplitter.Name(); private HashMap mAccountWritability = Maps.newHashMap(); private int mProviderStatus = ProviderStatus.STATUS_NORMAL; private long mEstimatedStorageRequirement = 0; private volatile CountDownLatch mAccessLatch; private HashMap mInsertedRawContacts = Maps.newHashMap(); private HashSet mUpdatedRawContacts = Sets.newHashSet(); private HashSet mDirtyRawContacts = Sets.newHashSet(); private HashMap mUpdatedSyncStates = Maps.newHashMap(); private boolean mVisibleTouched = false; private boolean mSyncToNetwork; private Locale mCurrentLocale; @Override public boolean onCreate() { super.onCreate(); try { return initialize(); } catch (RuntimeException e) { Log.e(TAG, "Cannot start provider", e); return false; } } private boolean initialize() { final Context context = getContext(); mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper(); mContactDirectoryManager = new ContactDirectoryManager(this); mGlobalSearchSupport = new GlobalSearchSupport(this); mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport); mContactAggregator = new ContactAggregator(this, mDbHelper, createPhotoPriorityResolver(context)); mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); mDb = mDbHelper.getWritableDatabase(); initForDefaultLocale(); mSetPrimaryStatement = mDb.compileStatement( "UPDATE " + Tables.DATA + " SET " + Data.IS_PRIMARY + "=(_id=?)" + " WHERE " + DataColumns.MIMETYPE_ID + "=?" + " AND " + Data.RAW_CONTACT_ID + "=?"); mSetSuperPrimaryStatement = mDb.compileStatement( "UPDATE " + Tables.DATA + " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" + " WHERE " + DataColumns.MIMETYPE_ID + "=?" + " AND " + Data.RAW_CONTACT_ID + " IN (" + "SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + " =(" + "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?))"); mRawContactDisplayNameUpdate = mDb.compileStatement( "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.DISPLAY_NAME_SOURCE + "=?," + RawContacts.DISPLAY_NAME_PRIMARY + "=?," + RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," + RawContacts.PHONETIC_NAME + "=?," + RawContacts.PHONETIC_NAME_STYLE + "=?," + RawContacts.SORT_KEY_PRIMARY + "=?," + RawContacts.SORT_KEY_ALTERNATIVE + "=?" + " WHERE " + RawContacts._ID + "=?"); mLastStatusUpdate = mDb.compileStatement( "UPDATE " + Tables.CONTACTS + " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" + "(SELECT " + DataColumns.CONCRETE_ID + " FROM " + Tables.STATUS_UPDATES + " JOIN " + Tables.DATA + " ON (" + StatusUpdatesColumns.DATA_ID + "=" + DataColumns.CONCRETE_ID + ")" + " JOIN " + Tables.RAW_CONTACTS + " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")" + " WHERE " + RawContacts.CONTACT_ID + "=?" + " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC," + StatusUpdates.STATUS + " LIMIT 1)" + " WHERE " + ContactsColumns.CONCRETE_ID + "=?"); mNameLookupInsert = mDb.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "(" + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + "," + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME + ") VALUES (?,?,?,?)"); mNameLookupDelete = mDb.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE " + NameLookupColumns.DATA_ID + "=?"); mStatusUpdateInsert = mDb.compileStatement( "INSERT INTO " + Tables.STATUS_UPDATES + "(" + StatusUpdatesColumns.DATA_ID + ", " + StatusUpdates.STATUS + "," + StatusUpdates.STATUS_RES_PACKAGE + "," + StatusUpdates.STATUS_ICON + "," + StatusUpdates.STATUS_LABEL + ")" + " VALUES (?,?,?,?,?)"); mStatusUpdateReplace = mDb.compileStatement( "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "(" + StatusUpdatesColumns.DATA_ID + ", " + StatusUpdates.STATUS_TIMESTAMP + "," + StatusUpdates.STATUS + "," + StatusUpdates.STATUS_RES_PACKAGE + "," + StatusUpdates.STATUS_ICON + "," + StatusUpdates.STATUS_LABEL + ")" + " VALUES (?,?,?,?,?,?)"); mStatusUpdateAutoTimestamp = mDb.compileStatement( "UPDATE " + Tables.STATUS_UPDATES + " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?," + StatusUpdates.STATUS + "=?" + " WHERE " + StatusUpdatesColumns.DATA_ID + "=?" + " AND " + StatusUpdates.STATUS + "!=?"); mStatusAttributionUpdate = mDb.compileStatement( "UPDATE " + Tables.STATUS_UPDATES + " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?," + StatusUpdates.STATUS_ICON + "=?," + StatusUpdates.STATUS_LABEL + "=?" + " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); mStatusUpdateDelete = mDb.compileStatement( "DELETE FROM " + Tables.STATUS_UPDATES + " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); // When setting NAME_VERIFIED to 1 on a raw contact, reset it to 0 // on all other raw contacts in the same aggregate mResetNameVerifiedForOtherRawContacts = mDb.compileStatement( "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.NAME_VERIFIED + "=0" + " WHERE " + RawContacts.CONTACT_ID + "=(" + "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?)" + " AND " + RawContacts._ID + "!=?"); mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE); mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE); mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE); mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE); mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); verifyAccounts(); if (isLegacyContactImportNeeded()) { importLegacyContactsAsync(); } else { verifyLocale(); } startContactDirectoryManager(); return (mDb != null); } private void initDataRowHandlers() { mDataRowHandlers = new HashMap(); mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler()); mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE, new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL)); mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler( StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL)); mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler()); mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler()); mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler()); mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE, new StructuredNameRowHandler(mNameSplitter)); mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, new StructuredPostalRowHandler(mPostalSplitter)); mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler()); mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler()); } /** * Visible for testing. */ /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) { return new PhotoPriorityResolver(context); } /** * (Re)allocates all locale-sensitive structures. */ private void initForDefaultLocale() { mCurrentLocale = getLocale(); mNameSplitter = mDbHelper.createNameSplitter(); mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter); mPostalSplitter = new PostalSplitter(mCurrentLocale); mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase()); ContactLocaleUtils.getIntance().setLocale(mCurrentLocale); initDataRowHandlers(); } @Override public void onConfigurationChanged(Configuration newConfig) { if (mProviderStatus != ProviderStatus.STATUS_NORMAL) { return; } initForDefaultLocale(); verifyLocale(); } protected void verifyAccounts() { AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false); onAccountsUpdated(AccountManager.get(getContext()).getAccounts()); } /** * Verifies that the contacts database is properly configured for the current locale. * If not, changes the database locale to the current locale using an asynchronous task. * This needs to be done asynchronously because the process involves rebuilding * large data structures (name lookup, sort keys), which can take minutes on * a large set of contacts. */ protected void verifyLocale() { // The process is already running - postpone the change if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) { return; } final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); final String providerLocale = prefs.getString(PREF_LOCALE, null); final Locale currentLocale = mCurrentLocale; if (currentLocale.toString().equals(providerLocale)) { return; } int providerStatus = mProviderStatus; setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE); AsyncTask task = new AsyncTask() { int savedProviderStatus; @Override protected Void doInBackground(Integer... params) { savedProviderStatus = params[0]; mDbHelper.setLocale(ContactsProvider2.this, currentLocale); return null; } @Override protected void onPostExecute(Void result) { prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).commit(); setProviderStatus(savedProviderStatus); // Recursive invocation, needed to cover the case where locale // changes once and then changes again before the db upgrade is completed. verifyLocale(); } }; task.execute(providerStatus); } /* Visible for testing */ @Override protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { return ContactsDatabaseHelper.getInstance(context); } /* package */ NameSplitter getNameSplitter() { return mNameSplitter; } /* Visible for testing */ public ContactDirectoryManager getContactDirectoryManager() { return mContactDirectoryManager; } /* Visible for testing */ protected Locale getLocale() { return Locale.getDefault(); } /* Visible for testing */ protected void startContactDirectoryManager() { getContactDirectoryManager().start(); } protected boolean isLegacyContactImportNeeded() { int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0")); return version < PROPERTY_CONTACTS_IMPORT_VERSION; } protected LegacyContactImporter getLegacyContactImporter() { return new LegacyContactImporter(getContext(), this); } /** * Imports legacy contacts in a separate thread. As long as the import process is running * all other access to the contacts is blocked. */ private void importLegacyContactsAsync() { Log.v(TAG, "Importing legacy contacts"); setProviderStatus(ProviderStatus.STATUS_UPGRADING); if (mAccessLatch == null) { mAccessLatch = new CountDownLatch(1); } Thread importThread = new Thread("LegacyContactImport") { @Override public void run() { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); mDbHelper.setLocale(ContactsProvider2.this, mCurrentLocale); prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit(); LegacyContactImporter importer = getLegacyContactImporter(); if (importLegacyContacts(importer)) { onLegacyContactImportSuccess(); } else { onLegacyContactImportFailure(); } } }; importThread.start(); } /** * Unlocks the provider and declares that the import process is complete. */ private void onLegacyContactImportSuccess() { NotificationManager nm = (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION); // Store a property in the database indicating that the conversion process succeeded mDbHelper.setProperty(PROPERTY_CONTACTS_IMPORTED, String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION)); setProviderStatus(ProviderStatus.STATUS_NORMAL); mAccessLatch.countDown(); mAccessLatch = null; Log.v(TAG, "Completed import of legacy contacts"); } /** * Announces the provider status and keeps the provider locked. */ private void onLegacyContactImportFailure() { Context context = getContext(); NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); // Show a notification Notification n = new Notification(android.R.drawable.stat_notify_error, context.getString(R.string.upgrade_out_of_memory_notification_ticker), System.currentTimeMillis()); n.setLatestEventInfo(context, context.getString(R.string.upgrade_out_of_memory_notification_title), context.getString(R.string.upgrade_out_of_memory_notification_text), PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0)); n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n); setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY); Log.v(TAG, "Failed to import legacy contacts"); } /* Visible for testing */ /* package */ boolean importLegacyContacts(LegacyContactImporter importer) { boolean aggregatorEnabled = mContactAggregator.isEnabled(); mContactAggregator.setEnabled(false); try { if (importer.importContacts()) { // TODO aggregate all newly added raw contacts mContactAggregator.setEnabled(aggregatorEnabled); return true; } } catch (Throwable e) { Log.e(TAG, "Legacy contact import failed", e); } mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement(); return false; } /** * Wipes all data from the contacts database. */ /* package */ void wipeData() { mDbHelper.wipeData(); } /** * While importing and aggregating contacts, this content provider will * block all attempts to change contacts data. In particular, it will hold * up all contact syncs. As soon as the import process is complete, all * processes waiting to write to the provider are unblocked and can proceed * to compete for the database transaction monitor. */ private void waitForAccess() { CountDownLatch latch = mAccessLatch; if (latch != null) { while (true) { try { latch.await(); mAccessLatch = null; return; } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } @Override public Uri insert(Uri uri, ContentValues values) { waitForAccess(); return super.insert(uri, values); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { if (mAccessLatch != null) { // We are stuck trying to upgrade contacts db. The only update request // allowed in this case is an update of provider status, which will trigger // an attempt to upgrade contacts again. int match = sUriMatcher.match(uri); if (match == PROVIDER_STATUS && isLegacyContactImportNeeded()) { Integer newStatus = values.getAsInteger(ProviderStatus.STATUS); if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) { importLegacyContactsAsync(); return 1; } else { return 0; } } } waitForAccess(); return super.update(uri, values, selection, selectionArgs); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { waitForAccess(); return super.delete(uri, selection, selectionArgs); } @Override public ContentProviderResult[] applyBatch(ArrayList operations) throws OperationApplicationException { waitForAccess(); return super.applyBatch(operations); } @Override protected void onBeginTransaction() { if (VERBOSE_LOGGING) { Log.v(TAG, "onBeginTransaction"); } super.onBeginTransaction(); mContactAggregator.clearPendingAggregations(); clearTransactionalChanges(); } private void clearTransactionalChanges() { mInsertedRawContacts.clear(); mUpdatedRawContacts.clear(); mUpdatedSyncStates.clear(); mDirtyRawContacts.clear(); } @Override protected void beforeTransactionCommit() { if (VERBOSE_LOGGING) { Log.v(TAG, "beforeTransactionCommit"); } super.beforeTransactionCommit(); flushTransactionalChanges(); mContactAggregator.aggregateInTransaction(mDb); if (mVisibleTouched) { mVisibleTouched = false; mDbHelper.updateAllVisible(); } } private void flushTransactionalChanges() { if (VERBOSE_LOGGING) { Log.v(TAG, "flushTransactionChanges"); } for (long rawContactId : mInsertedRawContacts.keySet()) { updateRawContactDisplayName(mDb, rawContactId); mContactAggregator.onRawContactInsert(mDb, rawContactId); } if (!mDirtyRawContacts.isEmpty()) { mSb.setLength(0); mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL); appendIds(mSb, mDirtyRawContacts); mSb.append(")"); mDb.execSQL(mSb.toString()); } if (!mUpdatedRawContacts.isEmpty()) { mSb.setLength(0); mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL); appendIds(mSb, mUpdatedRawContacts); mSb.append(")"); mDb.execSQL(mSb.toString()); } for (Map.Entry entry : mUpdatedSyncStates.entrySet()) { long id = entry.getKey(); if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) { throw new IllegalStateException( "unable to update sync state, does it still exist?"); } } clearTransactionalChanges(); } /** * Appends comma separated ids. * @param ids Should not be empty */ private void appendIds(StringBuilder sb, HashSet ids) { for (long id : ids) { sb.append(id).append(','); } sb.setLength(sb.length() - 1); // Yank the last comma } @Override protected void notifyChange() { notifyChange(mSyncToNetwork); mSyncToNetwork = false; } protected void notifyChange(boolean syncToNetwork) { getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, syncToNetwork); } protected void setProviderStatus(int status) { mProviderStatus = status; getContext().getContentResolver().notifyChange(ContactsContract.ProviderStatus.CONTENT_URI, null, false); } private boolean isNewRawContact(long rawContactId) { return mInsertedRawContacts.containsKey(rawContactId); } private DataRowHandler getDataRowHandler(final String mimeType) { DataRowHandler handler = mDataRowHandlers.get(mimeType); if (handler == null) { handler = new CustomDataRowHandler(mimeType); mDataRowHandlers.put(mimeType, handler); } return handler; } @Override protected Uri insertInTransaction(Uri uri, ContentValues values) { if (VERBOSE_LOGGING) { Log.v(TAG, "insertInTransaction: " + uri + " " + values); } final boolean callerIsSyncAdapter = readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); final int match = sUriMatcher.match(uri); long id = 0; switch (match) { case SYNCSTATE: id = mDbHelper.getSyncState().insert(mDb, values); break; case CONTACTS: { insertContact(values); break; } case RAW_CONTACTS: { id = insertRawContact(uri, values, callerIsSyncAdapter); mSyncToNetwork |= !callerIsSyncAdapter; break; } case RAW_CONTACTS_DATA: { values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1)); id = insertData(values, callerIsSyncAdapter); mSyncToNetwork |= !callerIsSyncAdapter; break; } case DATA: { id = insertData(values, callerIsSyncAdapter); mSyncToNetwork |= !callerIsSyncAdapter; break; } case GROUPS: { id = insertGroup(uri, values, callerIsSyncAdapter); mSyncToNetwork |= !callerIsSyncAdapter; break; } case SETTINGS: { id = insertSettings(uri, values); mSyncToNetwork |= !callerIsSyncAdapter; break; } case STATUS_UPDATES: { id = insertStatusUpdate(values); break; } default: mSyncToNetwork = true; return mLegacyApiSupport.insert(uri, values); } if (id < 0) { return null; } return ContentUris.withAppendedId(uri, id); } /** * If account is non-null then store it in the values. If the account is * already specified in the values then it must be consistent with the * account, if it is non-null. * * @param uri Current {@link Uri} being operated on. * @param values {@link ContentValues} to read and possibly update. * @throws IllegalArgumentException when only one of * {@link RawContacts#ACCOUNT_NAME} or * {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the * other undefined. * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME} * and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between * the given {@link Uri} and {@link ContentValues}. */ private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException { String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME); String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE); final boolean partialValues = TextUtils.isEmpty(valueAccountName) ^ TextUtils.isEmpty(valueAccountType); if (partialUri || partialValues) { // Throw when either account is incomplete throw new IllegalArgumentException(mDbHelper.exceptionMessage( "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); } // Accounts are valid by only checking one parameter, since we've // already ruled out partial accounts. final boolean validUri = !TextUtils.isEmpty(accountName); final boolean validValues = !TextUtils.isEmpty(valueAccountName); if (validValues && validUri) { // Check that accounts match when both present final boolean accountMatch = TextUtils.equals(accountName, valueAccountName) && TextUtils.equals(accountType, valueAccountType); if (!accountMatch) { throw new IllegalArgumentException(mDbHelper.exceptionMessage( "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri)); } } else if (validUri) { // Fill values from Uri when not present values.put(RawContacts.ACCOUNT_NAME, accountName); values.put(RawContacts.ACCOUNT_TYPE, accountType); } else if (validValues) { accountName = valueAccountName; accountType = valueAccountType; } else { return null; } // Use cached Account object when matches, otherwise create if (mAccount == null || !mAccount.name.equals(accountName) || !mAccount.type.equals(accountType)) { mAccount = new Account(accountName, accountType); } return mAccount; } /** * Inserts an item in the contacts table * * @param values the values for the new row * @return the row ID of the newly created row */ private long insertContact(ContentValues values) { throw new UnsupportedOperationException("Aggregate contacts are created automatically"); } /** * Inserts an item in the contacts table * * @param uri the values for the new row * @param values the account this contact should be associated with. may be null. * @param callerIsSyncAdapter * @return the row ID of the newly created row */ private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { mValues.clear(); mValues.putAll(values); mValues.putNull(RawContacts.CONTACT_ID); final Account account = resolveAccount(uri, mValues); if (values.containsKey(RawContacts.DELETED) && values.getAsInteger(RawContacts.DELETED) != 0) { mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); } long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues); int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) { aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE); } mContactAggregator.markNewForAggregation(rawContactId, aggregationMode); // Trigger creation of a Contact based on this RawContact at the end of transaction mInsertedRawContacts.put(rawContactId, account); if (!callerIsSyncAdapter) { addAutoAddMembership(rawContactId); final Long starred = values.getAsLong(RawContacts.STARRED); if (starred != null && starred != 0) { updateFavoritesMembership(rawContactId, starred != 0); } } return rawContactId; } private void addAutoAddMembership(long rawContactId) { final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID, rawContactId); if (groupId != null) { insertDataGroupMembership(rawContactId, groupId); } } private Long findGroupByRawContactId(String selection, long rawContactId) { Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID, selection, new String[]{Long.toString(rawContactId)}, null /* groupBy */, null /* having */, null /* orderBy */); try { while (c.moveToNext()) { return c.getLong(0); } return null; } finally { c.close(); } } private void updateFavoritesMembership(long rawContactId, boolean isStarred) { final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID, rawContactId); if (groupId != null) { if (isStarred) { insertDataGroupMembership(rawContactId, groupId); } else { deleteDataGroupMembership(rawContactId, groupId); } } } private void insertDataGroupMembership(long rawContactId, long groupId) { ContentValues groupMembershipValues = new ContentValues(); groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId); groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId); groupMembershipValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); mDb.insert(Tables.DATA, null, groupMembershipValues); } private void deleteDataGroupMembership(long rawContactId, long groupId) { final String[] selectionArgs = { Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)), Long.toString(groupId), Long.toString(rawContactId)}; mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs); } /** * Inserts an item in the data table * * @param values the values for the new row * @return the row ID of the newly created row */ private long insertData(ContentValues values, boolean callerIsSyncAdapter) { long id = 0; mValues.clear(); mValues.putAll(values); long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); // Replace package with internal mapping final String packageName = mValues.getAsString(Data.RES_PACKAGE); if (packageName != null) { mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); } mValues.remove(Data.RES_PACKAGE); // Replace mimetype with internal mapping final String mimeType = mValues.getAsString(Data.MIMETYPE); if (TextUtils.isEmpty(mimeType)) { throw new IllegalArgumentException(Data.MIMETYPE + " is required"); } mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType)); mValues.remove(Data.MIMETYPE); DataRowHandler rowHandler = getDataRowHandler(mimeType); id = rowHandler.insert(mDb, rawContactId, mValues); if (!callerIsSyncAdapter) { setRawContactDirty(rawContactId); } mUpdatedRawContacts.add(rawContactId); return id; } private void triggerAggregation(long rawContactId) { if (!mContactAggregator.isEnabled()) { return; } int aggregationMode = mDbHelper.getAggregationMode(rawContactId); switch (aggregationMode) { case RawContacts.AGGREGATION_MODE_DISABLED: break; case RawContacts.AGGREGATION_MODE_DEFAULT: { mContactAggregator.markForAggregation(rawContactId, aggregationMode, false); break; } case RawContacts.AGGREGATION_MODE_SUSPENDED: { long contactId = mDbHelper.getContactId(rawContactId); if (contactId != 0) { mContactAggregator.updateAggregateData(contactId); } break; } case RawContacts.AGGREGATION_MODE_IMMEDIATE: { long contactId = mDbHelper.getContactId(rawContactId); mContactAggregator.aggregateContact(mDb, rawContactId, contactId); break; } } } /** * Returns the group id of the group with sourceId and the same account as rawContactId. * If the group doesn't already exist then it is first created, * @param db SQLiteDatabase to use for this operation * @param rawContactId the contact this group is associated with * @param sourceId the sourceIf of the group to query or create * @return the group id of the existing or created group * @throws IllegalArgumentException if the contact is not associated with an account * @throws IllegalStateException if a group needs to be created but the creation failed */ private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId, Account account) { if (account == null) { mSelectionArgs1[0] = String.valueOf(rawContactId); Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, RawContacts._ID + "=?", mSelectionArgs1, null, null, null); try { if (c.moveToFirst()) { String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME); String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { account = new Account(accountName, accountType); } } } finally { c.close(); } } if (account == null) { throw new IllegalArgumentException("if the groupmembership only " + "has a sourceid the the contact must be associated with " + "an account"); } ArrayList entries = mGroupIdCache.get(sourceId); if (entries == null) { entries = new ArrayList(1); mGroupIdCache.put(sourceId, entries); } int count = entries.size(); for (int i = 0; i < count; i++) { GroupIdCacheEntry entry = entries.get(i); if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) { return entry.groupId; } } GroupIdCacheEntry entry = new GroupIdCacheEntry(); entry.accountName = account.name; entry.accountType = account.type; entry.sourceId = sourceId; entries.add(0, entry); // look up the group that contains this sourceId and has the same account name and type // as the contact refered to by rawContactId Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID}, Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID, new String[]{sourceId, account.name, account.type}, null, null, null); try { if (c.moveToFirst()) { entry.groupId = c.getLong(0); } else { ContentValues groupValues = new ContentValues(); groupValues.put(Groups.ACCOUNT_NAME, account.name); groupValues.put(Groups.ACCOUNT_TYPE, account.type); groupValues.put(Groups.SOURCE_ID, sourceId); long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues); if (groupId < 0) { throw new IllegalStateException("unable to create a new group with " + "this sourceid: " + groupValues); } entry.groupId = groupId; } } finally { c.close(); } return entry.groupId; } private interface DisplayNameQuery { public static final String RAW_SQL = "SELECT " + DataColumns.MIMETYPE_ID + "," + Data.IS_PRIMARY + "," + Data.DATA1 + "," + Data.DATA2 + "," + Data.DATA3 + "," + Data.DATA4 + "," + Data.DATA5 + "," + Data.DATA6 + "," + Data.DATA7 + "," + Data.DATA8 + "," + Data.DATA9 + "," + Data.DATA10 + "," + Data.DATA11 + " FROM " + Tables.DATA + " WHERE " + Data.RAW_CONTACT_ID + "=?" + " AND (" + Data.DATA1 + " NOT NULL OR " + Organization.TITLE + " NOT NULL)"; public static final int MIMETYPE = 0; public static final int IS_PRIMARY = 1; public static final int DATA1 = 2; public static final int GIVEN_NAME = 3; // data2 public static final int FAMILY_NAME = 4; // data3 public static final int PREFIX = 5; // data4 public static final int TITLE = 5; // data4 public static final int MIDDLE_NAME = 6; // data5 public static final int SUFFIX = 7; // data6 public static final int PHONETIC_GIVEN_NAME = 8; // data7 public static final int PHONETIC_MIDDLE_NAME = 9; // data8 public static final int ORGANIZATION_PHONETIC_NAME = 9; // data8 public static final int PHONETIC_FAMILY_NAME = 10; // data9 public static final int FULL_NAME_STYLE = 11; // data10 public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11; // data10 public static final int PHONETIC_NAME_STYLE = 12; // data11 } /** * Updates a raw contact display name based on data rows, e.g. structured name, * organization, email etc. */ public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) { int bestDisplayNameSource = DisplayNameSources.UNDEFINED; NameSplitter.Name bestName = null; String bestDisplayName = null; String bestPhoneticName = null; int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; mSelectionArgs1[0] = String.valueOf(rawContactId); Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1); try { while (c.moveToNext()) { int mimeType = c.getInt(DisplayNameQuery.MIMETYPE); int source = getDisplayNameSource(mimeType); if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) { continue; } if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) { continue; } if (mimeType == mMimeTypeIdStructuredName) { NameSplitter.Name name; if (bestName != null) { name = new NameSplitter.Name(); } else { name = mName; name.clear(); } name.prefix = c.getString(DisplayNameQuery.PREFIX); name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME); name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME); name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME); name.suffix = c.getString(DisplayNameQuery.SUFFIX); name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE) ? FullNameStyle.UNDEFINED : c.getInt(DisplayNameQuery.FULL_NAME_STYLE); name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME); name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME); name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME); name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE) ? PhoneticNameStyle.UNDEFINED : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE); if (!name.isEmpty()) { bestDisplayNameSource = source; bestName = name; } } else if (mimeType == mMimeTypeIdOrganization) { mCharArrayBuffer.sizeCopied = 0; c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer); if (mCharArrayBuffer.sizeCopied != 0) { bestDisplayNameSource = source; bestDisplayName = new String(mCharArrayBuffer.data, 0, mCharArrayBuffer.sizeCopied); bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME); bestPhoneticNameStyle = c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE) ? PhoneticNameStyle.UNDEFINED : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE); } else { c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer); if (mCharArrayBuffer.sizeCopied != 0) { bestDisplayNameSource = source; bestDisplayName = new String(mCharArrayBuffer.data, 0, mCharArrayBuffer.sizeCopied); bestPhoneticName = null; bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; } } } else { // Display name is at DATA1 in all other types. // This is ensured in the constructor. mCharArrayBuffer.sizeCopied = 0; c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer); if (mCharArrayBuffer.sizeCopied != 0) { bestDisplayNameSource = source; bestDisplayName = new String(mCharArrayBuffer.data, 0, mCharArrayBuffer.sizeCopied); bestPhoneticName = null; bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; } } } } finally { c.close(); } String displayNamePrimary; String displayNameAlternative; String sortKeyPrimary = null; String sortKeyAlternative = null; int displayNameStyle = FullNameStyle.UNDEFINED; if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) { displayNameStyle = bestName.fullNameStyle; if (displayNameStyle == FullNameStyle.CJK || displayNameStyle == FullNameStyle.UNDEFINED) { displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle); bestName.fullNameStyle = displayNameStyle; } displayNamePrimary = mNameSplitter.join(bestName, true); displayNameAlternative = mNameSplitter.join(bestName, false); bestPhoneticName = mNameSplitter.joinPhoneticName(bestName); bestPhoneticNameStyle = bestName.phoneticNameStyle; } else { displayNamePrimary = displayNameAlternative = bestDisplayName; } if (bestPhoneticName != null) { sortKeyPrimary = sortKeyAlternative = bestPhoneticName; if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) { bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName); } } else { if (displayNameStyle == FullNameStyle.UNDEFINED) { displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName); if (displayNameStyle == FullNameStyle.UNDEFINED || displayNameStyle == FullNameStyle.CJK) { displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle( displayNameStyle, bestPhoneticNameStyle); } displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle); } if (displayNameStyle == FullNameStyle.CHINESE || displayNameStyle == FullNameStyle.CJK) { sortKeyPrimary = sortKeyAlternative = ContactLocaleUtils.getIntance().getSortKey( displayNamePrimary, displayNameStyle); } } if (sortKeyPrimary == null) { sortKeyPrimary = displayNamePrimary; sortKeyAlternative = displayNameAlternative; } setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary, displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle, sortKeyPrimary, sortKeyAlternative); } private int getDisplayNameSource(int mimeTypeId) { if (mimeTypeId == mMimeTypeIdStructuredName) { return DisplayNameSources.STRUCTURED_NAME; } else if (mimeTypeId == mMimeTypeIdEmail) { return DisplayNameSources.EMAIL; } else if (mimeTypeId == mMimeTypeIdPhone) { return DisplayNameSources.PHONE; } else if (mimeTypeId == mMimeTypeIdOrganization) { return DisplayNameSources.ORGANIZATION; } else if (mimeTypeId == mMimeTypeIdNickname) { return DisplayNameSources.NICKNAME; } else { return DisplayNameSources.UNDEFINED; } } /** * Delete data row by row so that fixing of primaries etc work correctly. */ private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { int count = 0; // Note that the query will return data according to the access restrictions, // so we don't need to worry about deleting data we don't have permission to read. Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null); try { while(c.moveToNext()) { long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); String mimeType = c.getString(DataDeleteQuery.MIMETYPE); DataRowHandler rowHandler = getDataRowHandler(mimeType); count += rowHandler.delete(mDb, c); if (!callerIsSyncAdapter) { setRawContactDirty(rawContactId); } } } finally { c.close(); } return count; } /** * Delete a data row provided that it is one of the allowed mime types. */ public int deleteData(long dataId, String[] allowedMimeTypes) { // Note that the query will return data according to the access restrictions, // so we don't need to worry about deleting data we don't have permission to read. mSelectionArgs1[0] = String.valueOf(dataId); Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?", mSelectionArgs1, null); try { if (!c.moveToFirst()) { return 0; } String mimeType = c.getString(DataDeleteQuery.MIMETYPE); boolean valid = false; for (int i = 0; i < allowedMimeTypes.length; i++) { if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { valid = true; break; } } if (!valid) { throw new IllegalArgumentException("Data type mismatch: expected " + Lists.newArrayList(allowedMimeTypes)); } DataRowHandler rowHandler = getDataRowHandler(mimeType); return rowHandler.delete(mDb, c); } finally { c.close(); } } /** * Inserts an item in the groups table */ private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { mValues.clear(); mValues.putAll(values); final Account account = resolveAccount(uri, mValues); // Replace package with internal mapping final String packageName = mValues.getAsString(Groups.RES_PACKAGE); if (packageName != null) { mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); } mValues.remove(Groups.RES_PACKAGE); final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null ? mValues.getAsLong(Groups.FAVORITES) != 0 : false; if (!callerIsSyncAdapter) { mValues.put(Groups.DIRTY, 1); } long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues); if (!callerIsSyncAdapter && isFavoritesGroup) { // add all starred raw contacts to this group String selection; String[] selectionArgs; if (account == null) { selection = RawContacts.ACCOUNT_NAME + " IS NULL AND " + RawContacts.ACCOUNT_TYPE + " IS NULL"; selectionArgs = null; } else { selection = RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?"; selectionArgs = new String[]{account.name, account.type}; } Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID, RawContacts.STARRED}, selection, selectionArgs, null, null, null); while (c.moveToNext()) { if (c.getLong(1) != 0) { final long rawContactId = c.getLong(0); insertDataGroupMembership(rawContactId, result); setRawContactDirty(rawContactId); } } } if (mValues.containsKey(Groups.GROUP_VISIBLE)) { mVisibleTouched = true; } return result; } private long insertSettings(Uri uri, ContentValues values) { final long id = mDb.insert(Tables.SETTINGS, null, values); if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { mVisibleTouched = true; } return id; } /** * Inserts a status update. */ public long insertStatusUpdate(ContentValues values) { final String handle = values.getAsString(StatusUpdates.IM_HANDLE); final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL); String customProtocol = null; if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) { customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL); if (TextUtils.isEmpty(customProtocol)) { throw new IllegalArgumentException( "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); } } long rawContactId = -1; long contactId = -1; Long dataId = values.getAsLong(StatusUpdates.DATA_ID); mSb.setLength(0); mSelectionArgs.clear(); if (dataId != null) { // Lookup the contact info for the given data row. mSb.append(Tables.DATA + "." + Data._ID + "=?"); mSelectionArgs.add(String.valueOf(dataId)); } else { // Lookup the data row to attach this presence update to if (TextUtils.isEmpty(handle) || protocol == null) { throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); } // TODO: generalize to allow other providers to match against email boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; String mimeTypeIdIm = String.valueOf(mMimeTypeIdIm); if (matchEmail) { String mimeTypeIdEmail = String.valueOf(mMimeTypeIdEmail); // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise // the "OR" conjunction confuses it and it switches to a full scan of // the raw_contacts table. // This code relies on the fact that Im.DATA and Email.DATA are in fact the same // column - Data.DATA1 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" + " AND " + Data.DATA1 + "=?" + " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?"); mSelectionArgs.add(mimeTypeIdEmail); mSelectionArgs.add(mimeTypeIdIm); mSelectionArgs.add(handle); mSelectionArgs.add(mimeTypeIdIm); mSelectionArgs.add(String.valueOf(protocol)); if (customProtocol != null) { mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); mSelectionArgs.add(customProtocol); } mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))"); mSelectionArgs.add(mimeTypeIdEmail); } else { mSb.append(DataColumns.MIMETYPE_ID + "=?" + " AND " + Im.PROTOCOL + "=?" + " AND " + Im.DATA + "=?"); mSelectionArgs.add(mimeTypeIdIm); mSelectionArgs.add(String.valueOf(protocol)); mSelectionArgs.add(handle); if (customProtocol != null) { mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); mSelectionArgs.add(customProtocol); } } if (values.containsKey(StatusUpdates.DATA_ID)) { mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?"); mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID)); } } mSb.append(" AND ").append(getContactsRestrictions()); Cursor cursor = null; try { cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null, Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID); if (cursor.moveToFirst()) { dataId = cursor.getLong(DataContactsQuery.DATA_ID); rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); } else { // No contact found, return a null URI return -1; } } finally { if (cursor != null) { cursor.close(); } } if (values.containsKey(StatusUpdates.PRESENCE)) { if (customProtocol == null) { // We cannot allow a null in the custom protocol field, because SQLite3 does not // properly enforce uniqueness of null values customProtocol = ""; } mValues.clear(); mValues.put(StatusUpdates.DATA_ID, dataId); mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); mValues.put(PresenceColumns.CONTACT_ID, contactId); mValues.put(StatusUpdates.PROTOCOL, protocol); mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol); mValues.put(StatusUpdates.IM_HANDLE, handle); if (values.containsKey(StatusUpdates.IM_ACCOUNT)) { mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT)); } mValues.put(StatusUpdates.PRESENCE, values.getAsString(StatusUpdates.PRESENCE)); mValues.put(StatusUpdates.CHAT_CAPABILITY, values.getAsString(StatusUpdates.CHAT_CAPABILITY)); // Insert the presence update mDb.replace(Tables.PRESENCE, null, mValues); } if (values.containsKey(StatusUpdates.STATUS)) { String status = values.getAsString(StatusUpdates.STATUS); String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE); Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL); if (TextUtils.isEmpty(resPackage) && (labelResource == null || labelResource == 0) && protocol != null) { labelResource = Im.getProtocolLabelResource(protocol); } Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON); // TODO compute the default icon based on the protocol if (TextUtils.isEmpty(status)) { mStatusUpdateDelete.bindLong(1, dataId); mStatusUpdateDelete.execute(); } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) { long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP); mStatusUpdateReplace.bindLong(1, dataId); mStatusUpdateReplace.bindLong(2, timestamp); bindString(mStatusUpdateReplace, 3, status); bindString(mStatusUpdateReplace, 4, resPackage); bindLong(mStatusUpdateReplace, 5, iconResource); bindLong(mStatusUpdateReplace, 6, labelResource); mStatusUpdateReplace.execute(); } else { try { mStatusUpdateInsert.bindLong(1, dataId); bindString(mStatusUpdateInsert, 2, status); bindString(mStatusUpdateInsert, 3, resPackage); bindLong(mStatusUpdateInsert, 4, iconResource); bindLong(mStatusUpdateInsert, 5, labelResource); mStatusUpdateInsert.executeInsert(); } catch (SQLiteConstraintException e) { // The row already exists - update it long timestamp = System.currentTimeMillis(); mStatusUpdateAutoTimestamp.bindLong(1, timestamp); bindString(mStatusUpdateAutoTimestamp, 2, status); mStatusUpdateAutoTimestamp.bindLong(3, dataId); bindString(mStatusUpdateAutoTimestamp, 4, status); mStatusUpdateAutoTimestamp.execute(); bindString(mStatusAttributionUpdate, 1, resPackage); bindLong(mStatusAttributionUpdate, 2, iconResource); bindLong(mStatusAttributionUpdate, 3, labelResource); mStatusAttributionUpdate.bindLong(4, dataId); mStatusAttributionUpdate.execute(); } } } if (contactId != -1) { mLastStatusUpdate.bindLong(1, contactId); mLastStatusUpdate.bindLong(2, contactId); mLastStatusUpdate.execute(); } return dataId; } @Override protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { if (VERBOSE_LOGGING) { Log.v(TAG, "deleteInTransaction: " + uri); } flushTransactionalChanges(); final boolean callerIsSyncAdapter = readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); final int match = sUriMatcher.match(uri); switch (match) { case SYNCSTATE: return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); case SYNCSTATE_ID: String selectionWithId = (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") + (selection == null ? "" : " AND (" + selection + ")"); return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs); case CONTACTS: { // TODO return 0; } case CONTACTS_ID: { long contactId = ContentUris.parseId(uri); return deleteContact(contactId, callerIsSyncAdapter); } case CONTACTS_LOOKUP: { final List pathSegments = uri.getPathSegments(); final int segmentCount = pathSegments.size(); if (segmentCount < 3) { throw new IllegalArgumentException(mDbHelper.exceptionMessage( "Missing a lookup key", uri)); } final String lookupKey = pathSegments.get(2); final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); return deleteContact(contactId, callerIsSyncAdapter); } case CONTACTS_LOOKUP_ID: { // lookup contact by id and lookup key to see if they still match the actual record final List pathSegments = uri.getPathSegments(); final String lookupKey = pathSegments.get(2); SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); setTablesAndProjectionMapForContacts(lookupQb, uri, null); long contactId = ContentUris.parseId(uri); String[] args; if (selectionArgs == null) { args = new String[2]; } else { args = new String[selectionArgs.length + 2]; System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); } args[0] = String.valueOf(contactId); args[1] = Uri.encode(lookupKey); lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?"); final SQLiteDatabase db = mDbHelper.getReadableDatabase(); Cursor c = query(db, lookupQb, null, selection, args, null, null, null); try { if (c.getCount() == 1) { // contact was unmodified so go ahead and delete it return deleteContact(contactId, callerIsSyncAdapter); } else { // row was changed (e.g. the merging might have changed), we got multiple // rows or the supplied selection filtered the record out return 0; } } finally { c.close(); } } case RAW_CONTACTS: { int numDeletes = 0; Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID, RawContacts.CONTACT_ID}, appendAccountToSelection(uri, selection), selectionArgs, null, null, null); try { while (c.moveToNext()) { final long rawContactId = c.getLong(0); long contactId = c.getLong(1); numDeletes += deleteRawContact(rawContactId, contactId, callerIsSyncAdapter); } } finally { c.close(); } return numDeletes; } case RAW_CONTACTS_ID: { final long rawContactId = ContentUris.parseId(uri); return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId), callerIsSyncAdapter); } case DATA: { mSyncToNetwork |= !callerIsSyncAdapter; return deleteData(appendAccountToSelection(uri, selection), selectionArgs, callerIsSyncAdapter); } case DATA_ID: case PHONES_ID: case EMAILS_ID: case POSTALS_ID: { long dataId = ContentUris.parseId(uri); mSyncToNetwork |= !callerIsSyncAdapter; mSelectionArgs1[0] = String.valueOf(dataId); return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter); } case GROUPS_ID: { mSyncToNetwork |= !callerIsSyncAdapter; return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter); } case GROUPS: { int numDeletes = 0; Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, appendAccountToSelection(uri, selection), selectionArgs, null, null, null); try { while (c.moveToNext()) { numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter); } } finally { c.close(); } if (numDeletes > 0) { mSyncToNetwork |= !callerIsSyncAdapter; } return numDeletes; } case SETTINGS: { mSyncToNetwork |= !callerIsSyncAdapter; return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs); } case STATUS_UPDATES: { return deleteStatusUpdates(selection, selectionArgs); } default: { mSyncToNetwork = true; return mLegacyApiSupport.delete(uri, selection, selectionArgs); } } } public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) { mGroupIdCache.clear(); final long groupMembershipMimetypeId = mDbHelper .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" + groupId, null); try { if (callerIsSyncAdapter) { return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); } else { mValues.clear(); mValues.put(Groups.DELETED, 1); mValues.put(Groups.DIRTY, 1); return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); } } finally { mVisibleTouched = true; } } private int deleteSettings(Uri uri, String selection, String[] selectionArgs) { final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); mVisibleTouched = true; return count; } private int deleteContact(long contactId, boolean callerIsSyncAdapter) { mSelectionArgs1[0] = Long.toString(contactId); Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null); try { while (c.moveToNext()) { long rawContactId = c.getLong(0); markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); } } finally { c.close(); } return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null); } public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) { mContactAggregator.invalidateAggregationExceptionCache(); if (callerIsSyncAdapter) { mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); mContactAggregator.updateDisplayNameForContact(mDb, contactId); return count; } else { mDbHelper.removeContactIfSingleton(rawContactId); return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); } } private int deleteStatusUpdates(String selection, String[] selectionArgs) { // delete from both tables: presence and status_updates // TODO should account type/name be appended to the where clause? if (VERBOSE_LOGGING) { Log.v(TAG, "deleting data from status_updates for " + selection); } mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection), selectionArgs); return mDb.delete(Tables.PRESENCE, selection, selectionArgs); } private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) { mSyncToNetwork = true; mValues.clear(); mValues.put(RawContacts.DELETED, 1); mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); mValues.putNull(RawContacts.CONTACT_ID); mValues.put(RawContacts.DIRTY, 1); return updateRawContact(rawContactId, mValues, callerIsSyncAdapter); } @Override protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) { if (VERBOSE_LOGGING) { Log.v(TAG, "updateInTransaction: " + uri); } int count = 0; final int match = sUriMatcher.match(uri); if (match == SYNCSTATE_ID && selection == null) { long rowId = ContentUris.parseId(uri); Object data = values.get(ContactsContract.SyncState.DATA); mUpdatedSyncStates.put(rowId, data); return 1; } flushTransactionalChanges(); final boolean callerIsSyncAdapter = readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); switch(match) { case SYNCSTATE: return mDbHelper.getSyncState().update(mDb, values, appendAccountToSelection(uri, selection), selectionArgs); case SYNCSTATE_ID: { selection = appendAccountToSelection(uri, selection); String selectionWithId = (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") + (selection == null ? "" : " AND (" + selection + ")"); return mDbHelper.getSyncState().update(mDb, values, selectionWithId, selectionArgs); } case CONTACTS: { count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter); break; } case CONTACTS_ID: { count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter); break; } case CONTACTS_LOOKUP: case CONTACTS_LOOKUP_ID: { final List pathSegments = uri.getPathSegments(); final int segmentCount = pathSegments.size(); if (segmentCount < 3) { throw new IllegalArgumentException(mDbHelper.exceptionMessage( "Missing a lookup key", uri)); } final String lookupKey = pathSegments.get(2); final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); count = updateContactOptions(contactId, values, callerIsSyncAdapter); break; } case RAW_CONTACTS_DATA: { final String rawContactId = uri.getPathSegments().get(1); String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ") + (selection == null ? "" : " AND " + selection); count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); break; } case DATA: { count = updateData(uri, values, appendAccountToSelection(uri, selection), selectionArgs, callerIsSyncAdapter); if (count > 0) { mSyncToNetwork |= !callerIsSyncAdapter; } break; } case DATA_ID: case PHONES_ID: case EMAILS_ID: case POSTALS_ID: { count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); if (count > 0) { mSyncToNetwork |= !callerIsSyncAdapter; } break; } case RAW_CONTACTS: { selection = appendAccountToSelection(uri, selection); count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter); break; } case RAW_CONTACTS_ID: { long rawContactId = ContentUris.parseId(uri); if (selection != null) { selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); count = updateRawContacts(values, RawContacts._ID + "=?" + " AND(" + selection + ")", selectionArgs, callerIsSyncAdapter); } else { mSelectionArgs1[0] = String.valueOf(rawContactId); count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1, callerIsSyncAdapter); } break; } case GROUPS: { count = updateGroups(uri, values, appendAccountToSelection(uri, selection), selectionArgs, callerIsSyncAdapter); if (count > 0) { mSyncToNetwork |= !callerIsSyncAdapter; } break; } case GROUPS_ID: { long groupId = ContentUris.parseId(uri); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId)); String selectionWithId = Groups._ID + "=? " + (selection == null ? "" : " AND " + selection); count = updateGroups(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); if (count > 0) { mSyncToNetwork |= !callerIsSyncAdapter; } break; } case AGGREGATION_EXCEPTIONS: { count = updateAggregationException(mDb, values); break; } case SETTINGS: { count = updateSettings(uri, values, appendAccountToSelection(uri, selection), selectionArgs); mSyncToNetwork |= !callerIsSyncAdapter; break; } case STATUS_UPDATES: { count = updateStatusUpdate(uri, values, selection, selectionArgs); break; } case DIRECTORIES: { mContactDirectoryManager.scheduleDirectoryUpdateForCaller(); count = 1; break; } default: { mSyncToNetwork = true; return mLegacyApiSupport.update(uri, values, selection, selectionArgs); } } return count; } private int updateStatusUpdate(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // update status_updates table, if status is provided // TODO should account type/name be appended to the where clause? int updateCount = 0; ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values); if (settableValues.size() > 0) { updateCount = mDb.update(Tables.STATUS_UPDATES, settableValues, getWhereClauseForStatusUpdatesTable(selection), selectionArgs); } // now update the Presence table settableValues = getSettableColumnsForPresenceTable(values); if (settableValues.size() > 0) { updateCount = mDb.update(Tables.PRESENCE, settableValues, selection, selectionArgs); } // TODO updateCount is not entirely a valid count of updated rows because 2 tables could // potentially get updated in this method. return updateCount; } /** * Build a where clause to select the rows to be updated in status_updates table. */ private String getWhereClauseForStatusUpdatesTable(String selection) { mSb.setLength(0); mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE); mSb.append(selection); mSb.append(")"); return mSb.toString(); } private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) { mValues.clear(); ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values, StatusUpdates.STATUS); ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values, StatusUpdates.STATUS_TIMESTAMP); ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values, StatusUpdates.STATUS_RES_PACKAGE); ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values, StatusUpdates.STATUS_LABEL); ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values, StatusUpdates.STATUS_ICON); return mValues; } private ContentValues getSettableColumnsForPresenceTable(ContentValues values) { mValues.clear(); ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values, StatusUpdates.PRESENCE); ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values, StatusUpdates.CHAT_CAPABILITY); return mValues; } private int updateGroups(Uri uri, ContentValues values, String selectionWithId, String[] selectionArgs, boolean callerIsSyncAdapter) { mGroupIdCache.clear(); ContentValues updatedValues; if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) { updatedValues = mValues; updatedValues.clear(); updatedValues.putAll(values); updatedValues.put(Groups.DIRTY, 1); } else { updatedValues = values; } int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { mVisibleTouched = true; } if (updatedValues.containsKey(Groups.SHOULD_SYNC) && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) { Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME, Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null, null, null); String accountName; String accountType; try { while (c.moveToNext()) { accountName = c.getString(0); accountType = c.getString(1); if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { Account account = new Account(accountName, accountType); ContentResolver.requestSync(account, ContactsContract.AUTHORITY, new Bundle()); break; } } } finally { c.close(); } } return count; } private int updateSettings(Uri uri, ContentValues values, String selection, String[] selectionArgs) { final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { mVisibleTouched = true; } return count; } private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { if (values.containsKey(RawContacts.CONTACT_ID)) { throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + "in content values. Contact IDs are assigned automatically"); } if (!callerIsSyncAdapter) { selection = DatabaseUtils.concatenateWhere(selection, RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0"); } int count = 0; Cursor cursor = mDb.query(mDbHelper.getRawContactView(), new String[] { RawContacts._ID }, selection, selectionArgs, null, null, null); try { while (cursor.moveToNext()) { long rawContactId = cursor.getLong(0); updateRawContact(rawContactId, values, callerIsSyncAdapter); count++; } } finally { cursor.close(); } return count; } private int updateRawContact(long rawContactId, ContentValues values, boolean callerIsSyncAdapter) { final String selection = RawContacts._ID + " = ?"; mSelectionArgs1[0] = Long.toString(rawContactId); final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED) && values.getAsInteger(RawContacts.DELETED) == 0); int previousDeleted = 0; String accountType = null; String accountName = null; if (requestUndoDelete) { Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection, mSelectionArgs1, null, null, null); try { if (cursor.moveToFirst()) { previousDeleted = cursor.getInt(RawContactsQuery.DELETED); accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE); accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME); } } finally { cursor.close(); } values.put(ContactsContract.RawContacts.AGGREGATION_MODE, ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT); } int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1); if (count != 0) { if (values.containsKey(RawContacts.AGGREGATION_MODE)) { int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE); // As per ContactsContract documentation, changing aggregation mode // to DEFAULT should not trigger aggregation if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) { mContactAggregator.markForAggregation(rawContactId, aggregationMode, false); } } if (values.containsKey(RawContacts.STARRED)) { if (!callerIsSyncAdapter) { updateFavoritesMembership(rawContactId, values.getAsLong(RawContacts.STARRED) != 0); } mContactAggregator.updateStarred(rawContactId); } else { // if this raw contact is being associated with an account, then update the // favorites group membership based on whether or not this contact is starred. // If it is starred, add a group membership, if one doesn't already exist // otherwise delete any matching group memberships. if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { boolean starred = 0 != DatabaseUtils.longForQuery(mDb, SELECTION_STARRED_FROM_RAW_CONTACTS, new String[]{Long.toString(rawContactId)}); updateFavoritesMembership(rawContactId, starred); } } // if this raw contact is being associated with an account, then add a // group membership to the group marked as AutoAdd, if any. if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { addAutoAddMembership(rawContactId); } if (values.containsKey(RawContacts.SOURCE_ID)) { mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId); } if (values.containsKey(RawContacts.NAME_VERIFIED)) { // If setting NAME_VERIFIED for this raw contact, reset it for all // other raw contacts in the same aggregate if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) { mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId); mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId); mResetNameVerifiedForOtherRawContacts.execute(); } mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId); } if (requestUndoDelete && previousDeleted == 1) { // undo delete, needs aggregation again. mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType)); } } return count; } private int updateData(Uri uri, ContentValues values, String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { mValues.clear(); mValues.putAll(values); mValues.remove(Data._ID); mValues.remove(Data.RAW_CONTACT_ID); mValues.remove(Data.MIMETYPE); String packageName = values.getAsString(Data.RES_PACKAGE); if (packageName != null) { mValues.remove(Data.RES_PACKAGE); mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); } boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY); boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY); // Remove primary or super primary values being set to 0. This is disallowed by the // content provider. if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) { containsIsSuperPrimary = false; mValues.remove(Data.IS_SUPER_PRIMARY); } if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) { containsIsPrimary = false; mValues.remove(Data.IS_PRIMARY); } if (!callerIsSyncAdapter) { selection = DatabaseUtils.concatenateWhere(selection, Data.IS_READ_ONLY + "=0"); } int count = 0; // Note that the query will return data according to the access restrictions, // so we don't need to worry about updating data we don't have permission to read. Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null); try { while(c.moveToNext()) { count += updateData(mValues, c, callerIsSyncAdapter); } } finally { c.close(); } return count; } private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { if (values.size() == 0) { return 0; } final String mimeType = c.getString(DataUpdateQuery.MIMETYPE); DataRowHandler rowHandler = getDataRowHandler(mimeType); if (rowHandler.update(mDb, values, c, callerIsSyncAdapter)) { return 1; } else { return 0; } } private int updateContactOptions(ContentValues values, String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { int count = 0; Cursor cursor = mDb.query(mDbHelper.getContactView(), new String[] { Contacts._ID }, selection, selectionArgs, null, null, null); try { while (cursor.moveToNext()) { long contactId = cursor.getLong(0); updateContactOptions(contactId, values, callerIsSyncAdapter); count++; } } finally { cursor.close(); } return count; } private int updateContactOptions(long contactId, ContentValues values, boolean callerIsSyncAdapter) { mValues.clear(); ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, values, Contacts.CUSTOM_RINGTONE); ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, values, Contacts.SEND_TO_VOICEMAIL); ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, values, Contacts.LAST_TIME_CONTACTED); ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, values, Contacts.TIMES_CONTACTED); ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, values, Contacts.STARRED); // Nothing to update - just return if (mValues.size() == 0) { return 0; } if (mValues.containsKey(RawContacts.STARRED)) { // Mark dirty when changing starred to trigger sync mValues.put(RawContacts.DIRTY, 1); } mSelectionArgs1[0] = String.valueOf(contactId); mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?" + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1); if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) { Cursor cursor = mDb.query(mDbHelper.getRawContactView(), new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null); try { while (cursor.moveToNext()) { long rawContactId = cursor.getLong(0); updateFavoritesMembership(rawContactId, mValues.getAsLong(RawContacts.STARRED) != 0); } } finally { cursor.close(); } } // Copy changeable values to prevent automatically managed fields from // being explicitly updated by clients. mValues.clear(); ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, values, Contacts.CUSTOM_RINGTONE); ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, values, Contacts.SEND_TO_VOICEMAIL); ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, values, Contacts.LAST_TIME_CONTACTED); ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, values, Contacts.TIMES_CONTACTED); ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, values, Contacts.STARRED); int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1); if (values.containsKey(Contacts.LAST_TIME_CONTACTED) && !values.containsKey(Contacts.TIMES_CONTACTED)) { mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1); mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1); } return rslt; } private int updateAggregationException(SQLiteDatabase db, ContentValues values) { int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); long rawContactId1, rawContactId2; if (rcId1 < rcId2) { rawContactId1 = rcId1; rawContactId2 = rcId2; } else { rawContactId2 = rcId1; rawContactId1 = rcId2; } if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { mSelectionArgs2[0] = String.valueOf(rawContactId1); mSelectionArgs2[1] = String.valueOf(rawContactId2); db.delete(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions.RAW_CONTACT_ID1 + "=? AND " + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2); } else { ContentValues exceptionValues = new ContentValues(3); exceptionValues.put(AggregationExceptions.TYPE, exceptionType); exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, exceptionValues); } mContactAggregator.invalidateAggregationExceptionCache(); mContactAggregator.markForAggregation(rawContactId1, RawContacts.AGGREGATION_MODE_DEFAULT, true); mContactAggregator.markForAggregation(rawContactId2, RawContacts.AGGREGATION_MODE_DEFAULT, true); long contactId1 = mDbHelper.getContactId(rawContactId1); mContactAggregator.aggregateContact(db, rawContactId1, contactId1); long contactId2 = mDbHelper.getContactId(rawContactId2); mContactAggregator.aggregateContact(db, rawContactId2, contactId2); // The return value is fake - we just confirm that we made a change, not count actual // rows changed. return 1; } public void onAccountsUpdated(Account[] accounts) { // TODO : Check the unit test. boolean accountsChanged = false; HashSet existingAccounts = new HashSet(); mDb.beginTransaction(); try { findValidAccounts(existingAccounts); // Add a row to the ACCOUNTS table for each new account for (Account account : accounts) { if (!existingAccounts.contains(account)) { accountsChanged = true; mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)", new String[] {account.name, account.type}); } } // Remove all valid accounts from the existing account set. What is left // in the accountsToDelete set will be extra accounts whose data must be deleted. HashSet accountsToDelete = new HashSet(existingAccounts); for (Account account : accounts) { accountsToDelete.remove(account); } if (!accountsToDelete.isEmpty()) { accountsChanged = true; for (Account account : accountsToDelete) { Log.d(TAG, "removing data for removed account " + account); String[] params = new String[] {account.name, account.type}; mDb.execSQL( "DELETE FROM " + Tables.GROUPS + " WHERE " + Groups.ACCOUNT_NAME + " = ?" + " AND " + Groups.ACCOUNT_TYPE + " = ?", params); mDb.execSQL( "DELETE FROM " + Tables.PRESENCE + " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" + "SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params); mDb.execSQL( "DELETE FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params); mDb.execSQL( "DELETE FROM " + Tables.SETTINGS + " WHERE " + Settings.ACCOUNT_NAME + " = ?" + " AND " + Settings.ACCOUNT_TYPE + " = ?", params); mDb.execSQL( "DELETE FROM " + Tables.ACCOUNTS + " WHERE " + RawContacts.ACCOUNT_NAME + "=?" + " AND " + RawContacts.ACCOUNT_TYPE + "=?", params); mDb.execSQL( "DELETE FROM " + Tables.DIRECTORIES + " WHERE " + Directory.ACCOUNT_NAME + "=?" + " AND " + Directory.ACCOUNT_TYPE + "=?", params); mDirectoryCache = null; } // Find all aggregated contacts that used to contain the raw contacts // we have just deleted and see if they are still referencing the deleted // names or photos. If so, fix up those contacts. HashSet orphanContactIds = Sets.newHashSet(); Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID + " FROM " + Tables.CONTACTS + " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " + Contacts.NAME_RAW_CONTACT_ID + " NOT IN " + "(SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS + "))" + " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + " NOT IN " + "(SELECT " + Data._ID + " FROM " + Tables.DATA + "))", null); try { while (cursor.moveToNext()) { orphanContactIds.add(cursor.getLong(0)); } } finally { cursor.close(); } for (Long contactId : orphanContactIds) { mContactAggregator.updateAggregateData(contactId); } mDbHelper.updateAllVisible(); } if (accountsChanged) { mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); } mDb.setTransactionSuccessful(); } finally { mDb.endTransaction(); } mAccountWritability.clear(); } public void onPackageChanged(String packageName) { mContactDirectoryManager.onPackageChanged(packageName); } /** * Finds all distinct accounts present in the specified table. */ private void findValidAccounts(Set validAccounts) { Cursor c = mDb.rawQuery( "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE + " FROM " + Tables.ACCOUNTS, null); try { while (c.moveToNext()) { if (!c.isNull(0) || !c.isNull(1)) { validAccounts.add(new Account(c.getString(0), c.getString(1))); } } } finally { c.close(); } } /** * Test all against {@link TextUtils#isEmpty(CharSequence)}. */ private static boolean areAllEmpty(ContentValues values, String[] keys) { for (String key : keys) { if (!TextUtils.isEmpty(values.getAsString(key))) { return false; } } return true; } /** * Returns true if a value (possibly null) is specified for at least one of the supplied keys. */ private static boolean areAnySpecified(ContentValues values, String[] keys) { for (String key : keys) { if (values.containsKey(key)) { return true; } } return false; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY); if (directory == null || directory.equals("0")) { return queryLocal(uri, projection, selection, selectionArgs, sortOrder, false); } else if (directory.equals("1")) { return queryLocal(uri, projection, selection, selectionArgs, sortOrder, true); } DirectoryInfo directoryInfo = getDirectoryAuthority(directory); if (directoryInfo == null) { throw new IllegalArgumentException( mDbHelper.exceptionMessage("Invalid directory ID", uri)); } Builder builder = new Uri.Builder(); builder.scheme(ContentResolver.SCHEME_CONTENT); builder.authority(directoryInfo.authority); builder.encodedPath(uri.getEncodedPath()); if (directoryInfo.accountName != null) { builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName); } if (directoryInfo.accountType != null) { builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType); } Uri directoryUri = builder.build(); if (projection == null) { projection = getDefaultProjection(uri); } return getContext().getContentResolver().query(directoryUri, projection, selection, selectionArgs, sortOrder); } private static final class DirectoryQuery { public static final String[] COLUMNS = new String[] { Directory._ID, Directory.DIRECTORY_AUTHORITY, Directory.ACCOUNT_NAME, Directory.ACCOUNT_TYPE }; public static final int DIRECTORY_ID = 0; public static final int AUTHORITY = 1; public static final int ACCOUNT_NAME = 2; public static final int ACCOUNT_TYPE = 3; } /** * Reads and caches directory information for the database. */ private DirectoryInfo getDirectoryAuthority(String directoryId) { if (mDirectoryCache == null) { mDirectoryCache = new HashMap(); Cursor cursor = mDb.query(Tables.DIRECTORIES, DirectoryQuery.COLUMNS, null, null, null, null, null); try { while (cursor.moveToNext()) { DirectoryInfo info = new DirectoryInfo(); String id = cursor.getString(DirectoryQuery.DIRECTORY_ID); info.authority = cursor.getString(DirectoryQuery.AUTHORITY); info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); mDirectoryCache.put(id, info); } } finally { cursor.close(); } } return mDirectoryCache.get(directoryId); } public void resetDirectoryCache() { mDirectoryCache = null; } public Cursor queryLocal(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, boolean hiddenOnly) { if (VERBOSE_LOGGING) { Log.v(TAG, "query: " + uri); } final SQLiteDatabase db = mDbHelper.getReadableDatabase(); SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); String groupBy = null; String limit = getLimit(uri); // TODO: Consider writing a test case for RestrictionExceptions when you // write a new query() block to make sure it protects restricted data. final int match = sUriMatcher.match(uri); switch (match) { case SYNCSTATE: return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs, sortOrder); case CONTACTS: { setTablesAndProjectionMapForContacts(qb, uri, projection); if (hiddenOnly) { qb.appendWhere(Contacts.IN_VISIBLE_GROUP + "=0"); } break; } case CONTACTS_ID: { long contactId = ContentUris.parseId(uri); setTablesAndProjectionMapForContacts(qb, uri, projection); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); qb.appendWhere(Contacts._ID + "=?"); break; } case CONTACTS_LOOKUP: case CONTACTS_LOOKUP_ID: { List pathSegments = uri.getPathSegments(); int segmentCount = pathSegments.size(); if (segmentCount < 3) { throw new IllegalArgumentException(mDbHelper.exceptionMessage( "Missing a lookup key", uri)); } String lookupKey = pathSegments.get(2); if (segmentCount == 4) { long contactId = Long.parseLong(pathSegments.get(3)); SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); setTablesAndProjectionMapForContacts(lookupQb, uri, projection); Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, projection, selection, selectionArgs, sortOrder, groupBy, limit, Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey); if (c != null) { return c; } } setTablesAndProjectionMapForContacts(qb, uri, projection); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); qb.appendWhere(Contacts._ID + "=?"); break; } case CONTACTS_LOOKUP_DATA: case CONTACTS_LOOKUP_ID_DATA: { List pathSegments = uri.getPathSegments(); int segmentCount = pathSegments.size(); if (segmentCount < 4) { throw new IllegalArgumentException(mDbHelper.exceptionMessage( "Missing a lookup key", uri)); } String lookupKey = pathSegments.get(2); if (segmentCount == 5) { long contactId = Long.parseLong(pathSegments.get(3)); SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); setTablesAndProjectionMapForData(lookupQb, uri, projection, false); lookupQb.appendWhere(" AND "); Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, projection, selection, selectionArgs, sortOrder, groupBy, limit, Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey); if (c != null) { return c; } // TODO see if the contact exists but has no data rows (rare) } setTablesAndProjectionMapForData(qb, uri, projection, false); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); qb.appendWhere(" AND " + Data.CONTACT_ID + "=?"); break; } case CONTACTS_AS_VCARD: { // When reading as vCard always use restricted view final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); qb.setTables(mDbHelper.getContactView(true /* require restricted */)); qb.setProjectionMap(sContactsVCardProjectionMap); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); qb.appendWhere(Contacts._ID + "=?"); break; } case CONTACTS_AS_MULTI_VCARD: { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); String currentDateString = dateFormat.format(new Date()).toString(); return db.rawQuery( "SELECT" + " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," + " NULL AS " + OpenableColumns.SIZE, new String[] { currentDateString }); } case CONTACTS_FILTER: { String filterParam = ""; if (uri.getPathSegments().size() > 2) { filterParam = uri.getLastPathSegment(); } setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam); if (hiddenOnly) { qb.appendWhere(Contacts.IN_VISIBLE_GROUP + "=0"); } break; } case CONTACTS_STREQUENT_FILTER: case CONTACTS_STREQUENT: { String filterSql = null; if (match == CONTACTS_STREQUENT_FILTER && uri.getPathSegments().size() > 3) { String filterParam = uri.getLastPathSegment(); StringBuilder sb = new StringBuilder(); sb.append(Contacts._ID + " IN "); appendContactFilterAsNestedQuery(sb, filterParam); filterSql = sb.toString(); } setTablesAndProjectionMapForContacts(qb, uri, projection); String[] starredProjection = null; String[] frequentProjection = null; if (projection != null) { starredProjection = appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN); frequentProjection = appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN); } // Build the first query for starred if (filterSql != null) { qb.appendWhere(filterSql); } qb.setProjectionMap(sStrequentStarredProjectionMap); final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1", null, Contacts._ID, null, null, null); // Build the second query for frequent qb = new SQLiteQueryBuilder(); setTablesAndProjectionMapForContacts(qb, uri, projection); if (filterSql != null) { qb.appendWhere(filterSql); } qb.setProjectionMap(sStrequentFrequentProjectionMap); final String frequentQuery = qb.buildQuery(frequentProjection, Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED + " = 0 OR " + Contacts.STARRED + " IS NULL)", null, Contacts._ID, null, null, null); // Put them together final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, STREQUENT_ORDER_BY, STREQUENT_LIMIT); Cursor c = db.rawQuery(query, null); if (c != null) { c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); } return c; } case CONTACTS_GROUP: { setTablesAndProjectionMapForContacts(qb, uri, projection); if (uri.getPathSegments().size() > 2) { qb.appendWhere(CONTACTS_IN_GROUP_SELECT); selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); } break; } case CONTACTS_ID_DATA: { long contactId = Long.parseLong(uri.getPathSegments().get(1)); setTablesAndProjectionMapForData(qb, uri, projection, false); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); break; } case CONTACTS_ID_PHOTO: { long contactId = Long.parseLong(uri.getPathSegments().get(1)); setTablesAndProjectionMapForData(qb, uri, projection, false); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); break; } case CONTACTS_ID_ENTITIES: { long contactId = Long.parseLong(uri.getPathSegments().get(1)); setTablesAndProjectionMapForEntities(qb, uri, projection); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); break; } case CONTACTS_LOOKUP_ENTITIES: case CONTACTS_LOOKUP_ID_ENTITIES: { List pathSegments = uri.getPathSegments(); int segmentCount = pathSegments.size(); if (segmentCount < 4) { throw new IllegalArgumentException(mDbHelper.exceptionMessage( "Missing a lookup key", uri)); } String lookupKey = pathSegments.get(2); if (segmentCount == 5) { long contactId = Long.parseLong(pathSegments.get(3)); SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); setTablesAndProjectionMapForEntities(lookupQb, uri, projection); lookupQb.appendWhere(" AND "); Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, projection, selection, selectionArgs, sortOrder, groupBy, limit, Contacts.Entity.CONTACT_ID, contactId, Contacts.Entity.LOOKUP_KEY, lookupKey); if (c != null) { return c; } } setTablesAndProjectionMapForEntities(qb, uri, projection); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?"); break; } case PHONES: { setTablesAndProjectionMapForData(qb, uri, projection, false); qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); break; } case PHONES_ID: { setTablesAndProjectionMapForData(qb, uri, projection, false); selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); qb.appendWhere(" AND " + Data._ID + "=?"); break; } case PHONES_FILTER: { setTablesAndProjectionMapForData(qb, uri, projection, true); qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); if (uri.getPathSegments().size() > 2) { String filterParam = uri.getLastPathSegment(); StringBuilder sb = new StringBuilder(); sb.append(" AND ("); boolean hasCondition = false; boolean orNeeded = false; String normalizedName = NameNormalizer.normalize(filterParam); if (normalizedName.length() > 0) { sb.append(Data.RAW_CONTACT_ID + " IN "); appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); orNeeded = true; hasCondition = true; } if (isPhoneNumber(filterParam)) { if (orNeeded) { sb.append(" OR "); } String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam); String reversed = PhoneNumberUtils.getStrippedReversed(number); sb.append(Data._ID + " IN (SELECT " + PhoneLookupColumns.DATA_ID + " FROM " + Tables.PHONE_LOOKUP + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%"); sb.append(reversed); sb.append("')"); hasCondition = true; } if (!hasCondition) { // If it is neither a phone number nor a name, the query should return // an empty cursor. Let's ensure that. sb.append("0"); } sb.append(")"); qb.appendWhere(sb); } groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; if (sortOrder == null) { sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; } break; } case EMAILS: { setTablesAndProjectionMapForData(qb, uri, projection, false); qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); break; } case EMAILS_ID: { setTablesAndProjectionMapForData(qb, uri, projection, false); selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'" + " AND " + Data._ID + "=?"); break; } case EMAILS_LOOKUP: { setTablesAndProjectionMapForData(qb, uri, projection, false); qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); if (uri.getPathSegments().size() > 2) { String email = uri.getLastPathSegment(); String address = mDbHelper.extractAddressFromEmailAddress(email); selectionArgs = insertSelectionArg(selectionArgs, address); qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)"); } break; } case EMAILS_FILTER: { setTablesAndProjectionMapForData(qb, uri, projection, true); String filterParam = null; if (uri.getPathSegments().size() > 3) { filterParam = uri.getLastPathSegment(); if (TextUtils.isEmpty(filterParam)) { filterParam = null; } } if (filterParam == null) { // If the filter is unspecified, return nothing qb.appendWhere(" AND 0"); } else { StringBuilder sb = new StringBuilder(); sb.append(" AND " + Data._ID + " IN ("); sb.append( "SELECT " + Data._ID + " FROM " + Tables.DATA + " WHERE " + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail + " AND " + Data.DATA1 + " LIKE "); DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%'); if (!filterParam.contains("@")) { String normalizedName = NameNormalizer.normalize(filterParam); if (normalizedName.length() > 0) { /* * Using a UNION instead of an "OR" to make SQLite use the right * indexes. We need it to use the (mimetype,data1) index for the * email lookup (see above), but not for the name lookup. * SQLite is not smart enough to use the index on one side of an OR * but not on the other. Using two separate nested queries * and a UNION between them does the job. */ sb.append( " UNION SELECT " + Data._ID + " FROM " + Tables.DATA + " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail + " AND " + Data.RAW_CONTACT_ID + " IN "); appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); } } sb.append(")"); qb.appendWhere(sb); } groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; if (sortOrder == null) { sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; } break; } case POSTALS: { setTablesAndProjectionMapForData(qb, uri, projection, false); qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + StructuredPostal.CONTENT_ITEM_TYPE + "'"); break; } case POSTALS_ID: { setTablesAndProjectionMapForData(qb, uri, projection, false); selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + StructuredPostal.CONTENT_ITEM_TYPE + "'"); qb.appendWhere(" AND " + Data._ID + "=?"); break; } case RAW_CONTACTS: { setTablesAndProjectionMapForRawContacts(qb, uri); break; } case RAW_CONTACTS_ID: { long rawContactId = ContentUris.parseId(uri); setTablesAndProjectionMapForRawContacts(qb, uri); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); qb.appendWhere(" AND " + RawContacts._ID + "=?"); break; } case RAW_CONTACTS_DATA: { long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); setTablesAndProjectionMapForData(qb, uri, projection, false); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?"); break; } case DATA: { setTablesAndProjectionMapForData(qb, uri, projection, false); break; } case DATA_ID: { setTablesAndProjectionMapForData(qb, uri, projection, false); selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); qb.appendWhere(" AND " + Data._ID + "=?"); break; } case PHONE_LOOKUP: { if (TextUtils.isEmpty(sortOrder)) { // Default the sort order to something reasonable so we get consistent // results when callers don't request an ordering sortOrder = RawContactsColumns.CONCRETE_ID; } String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; mDbHelper.buildPhoneLookupAndContactQuery(qb, number); qb.setProjectionMap(sPhoneLookupProjectionMap); // Phone lookup cannot be combined with a selection selection = null; selectionArgs = null; break; } case GROUPS: { qb.setTables(mDbHelper.getGroupView()); qb.setProjectionMap(sGroupsProjectionMap); appendAccountFromParameter(qb, uri); break; } case GROUPS_ID: { qb.setTables(mDbHelper.getGroupView()); qb.setProjectionMap(sGroupsProjectionMap); selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); qb.appendWhere(Groups._ID + "=?"); break; } case GROUPS_SUMMARY: { qb.setTables(mDbHelper.getGroupView() + " AS groups"); qb.setProjectionMap(sGroupsSummaryProjectionMap); appendAccountFromParameter(qb, uri); groupBy = Groups._ID; break; } case AGGREGATION_EXCEPTIONS: { qb.setTables(Tables.AGGREGATION_EXCEPTIONS); qb.setProjectionMap(sAggregationExceptionsProjectionMap); break; } case AGGREGATION_SUGGESTIONS: { long contactId = Long.parseLong(uri.getPathSegments().get(1)); String filter = null; if (uri.getPathSegments().size() > 3) { filter = uri.getPathSegments().get(3); } final int maxSuggestions; if (limit != null) { maxSuggestions = Integer.parseInt(limit); } else { maxSuggestions = DEFAULT_MAX_SUGGESTIONS; } setTablesAndProjectionMapForContacts(qb, uri, projection); return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId, maxSuggestions, filter); } case SETTINGS: { qb.setTables(Tables.SETTINGS); qb.setProjectionMap(sSettingsProjectionMap); appendAccountFromParameter(qb, uri); // When requesting specific columns, this query requires // late-binding of the GroupMembership MIME-type. final String groupMembershipMimetypeId = Long.toString(mDbHelper .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); if (projection != null && projection.length != 0 && mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); } if (projection != null && projection.length != 0 && mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); } break; } case STATUS_UPDATES: { setTableAndProjectionMapForStatusUpdates(qb, projection); break; } case STATUS_UPDATES_ID: { setTableAndProjectionMapForStatusUpdates(qb, projection); selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); qb.appendWhere(DataColumns.CONCRETE_ID + "=?"); break; } case SEARCH_SUGGESTIONS: { return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit); } case SEARCH_SHORTCUT: { String lookupKey = uri.getLastPathSegment(); return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection); } case LIVE_FOLDERS_CONTACTS: qb.setTables(mDbHelper.getContactView()); qb.setProjectionMap(sLiveFoldersProjectionMap); break; case LIVE_FOLDERS_CONTACTS_WITH_PHONES: qb.setTables(mDbHelper.getContactView()); qb.setProjectionMap(sLiveFoldersProjectionMap); qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); break; case LIVE_FOLDERS_CONTACTS_FAVORITES: qb.setTables(mDbHelper.getContactView()); qb.setProjectionMap(sLiveFoldersProjectionMap); qb.appendWhere(Contacts.STARRED + "=1"); break; case LIVE_FOLDERS_CONTACTS_GROUP_NAME: qb.setTables(mDbHelper.getContactView()); qb.setProjectionMap(sLiveFoldersProjectionMap); qb.appendWhere(CONTACTS_IN_GROUP_SELECT); selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); break; case RAW_CONTACT_ENTITIES: { setTablesAndProjectionMapForRawEntities(qb, uri); break; } case RAW_CONTACT_ENTITY_ID: { long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); setTablesAndProjectionMapForRawEntities(qb, uri); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); qb.appendWhere(" AND " + RawContacts._ID + "=?"); break; } case PROVIDER_STATUS: { return queryProviderStatus(uri, projection); } case DIRECTORIES : { qb.setTables(Tables.DIRECTORIES); qb.setProjectionMap(sDirectoryProjectionMap); break; } case DIRECTORIES_ID : { long directoryId = ContentUris.parseId(uri); qb.setTables(Tables.DIRECTORIES); qb.setProjectionMap(sDirectoryProjectionMap); selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(directoryId)); qb.appendWhere(Directory._ID + "=?"); break; } default: return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, sortOrder, limit); } qb.setStrictProjectionMap(true); Cursor cursor = query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) { cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder); } return cursor; } private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit) { if (projection != null && projection.length == 1 && BaseColumns._COUNT.equals(projection[0])) { qb.setProjectionMap(sCountProjectionMap); } final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit); if (c != null) { c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); } return c; } /** * Creates a single-row cursor containing the current status of the provider. */ private Cursor queryProviderStatus(Uri uri, String[] projection) { MatrixCursor cursor = new MatrixCursor(projection); RowBuilder row = cursor.newRow(); for (int i = 0; i < projection.length; i++) { if (ProviderStatus.STATUS.equals(projection[i])) { row.add(mProviderStatus); } else if (ProviderStatus.DATA1.equals(projection[i])) { row.add(mEstimatedStorageRequirement); } } return cursor; } /** * Runs the query with the supplied contact ID and lookup ID. If the query succeeds, * it returns the resulting cursor, otherwise it returns null and the calling * method needs to resolve the lookup key and rerun the query. */ private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb, SQLiteDatabase db, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit, String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey) { String[] args; if (selectionArgs == null) { args = new String[2]; } else { args = new String[selectionArgs.length + 2]; System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); } args[0] = String.valueOf(contactId); args[1] = Uri.encode(lookupKey); lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?"); Cursor c = query(db, lookupQb, projection, selection, args, sortOrder, groupBy, limit); if (c.getCount() != 0) { return c; } c.close(); return null; } private static final class AddressBookIndexQuery { public static final String LETTER = "letter"; public static final String TITLE = "title"; public static final String COUNT = "count"; public static final String[] COLUMNS = new String[] { LETTER, TITLE, COUNT }; public static final int COLUMN_LETTER = 0; public static final int COLUMN_TITLE = 1; public static final int COLUMN_COUNT = 2; public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME; } /** * Computes counts by the address book index titles and adds the resulting tally * to the returned cursor as a bundle of extras. */ private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db, SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) { String sortKey; // The sort order suffix could be something like "DESC". // We want to preserve it in the query even though we will change // the sort column itself. String sortOrderSuffix = ""; if (sortOrder != null) { int spaceIndex = sortOrder.indexOf(' '); if (spaceIndex != -1) { sortKey = sortOrder.substring(0, spaceIndex); sortOrderSuffix = sortOrder.substring(spaceIndex); } else { sortKey = sortOrder; } } else { sortKey = Contacts.SORT_KEY_PRIMARY; } String locale = getLocale().toString(); HashMap projectionMap = Maps.newHashMap(); projectionMap.put(AddressBookIndexQuery.LETTER, "SUBSTR(" + sortKey + ",1,1) AS " + AddressBookIndexQuery.LETTER); /** * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3, * to map the first letter of the sort key to a character that is traditionally * used in phonebooks to represent that letter. For example, in Korean it will * be the first consonant in the letter; for Japanese it will be Hiragana rather * than Katakana. */ projectionMap.put(AddressBookIndexQuery.TITLE, "GET_PHONEBOOK_INDEX(SUBSTR(" + sortKey + ",1,1),'" + locale + "')" + " AS " + AddressBookIndexQuery.TITLE); projectionMap.put(AddressBookIndexQuery.COUNT, "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT); qb.setProjectionMap(projectionMap); Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs, AddressBookIndexQuery.ORDER_BY, null /* having */, AddressBookIndexQuery.ORDER_BY + sortOrderSuffix); try { int groupCount = indexCursor.getCount(); String titles[] = new String[groupCount]; int counts[] = new int[groupCount]; int indexCount = 0; String currentTitle = null; // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up // with multiple entries for the same title. The following code // collapses those duplicates. for (int i = 0; i < groupCount; i++) { indexCursor.moveToNext(); String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE); int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT); if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) { titles[indexCount] = currentTitle = title; counts[indexCount] = count; indexCount++; } else { counts[indexCount - 1] += count; } } if (indexCount < groupCount) { String[] newTitles = new String[indexCount]; System.arraycopy(titles, 0, newTitles, 0, indexCount); titles = newTitles; int[] newCounts = new int[indexCount]; System.arraycopy(counts, 0, newCounts, 0, indexCount); counts = newCounts; } final Bundle bundle = new Bundle(); bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles); bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts); return new CursorWrapper(cursor) { @Override public Bundle getExtras() { return bundle; } }; } finally { indexCursor.close(); } } /** * Returns the contact Id for the contact identified by the lookupKey. * Robust against changes in the lookup key: if the key has changed, will * look up the contact by the raw contact IDs or name encoded in the lookup * key. */ public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { ContactLookupKey key = new ContactLookupKey(); ArrayList segments = key.parse(lookupKey); long contactId = -1; if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) { contactId = lookupContactIdBySourceIds(db, segments); if (contactId != -1) { return contactId; } } boolean hasRawContactIds = lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID); if (hasRawContactIds) { contactId = lookupContactIdByRawContactIds(db, segments); if (contactId != -1) { return contactId; } } if (hasRawContactIds || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) { contactId = lookupContactIdByDisplayNames(db, segments); } return contactId; } private interface LookupBySourceIdQuery { String TABLE = Tables.RAW_CONTACTS; String COLUMNS[] = { RawContacts.CONTACT_ID, RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME, RawContacts.SOURCE_ID }; int CONTACT_ID = 0; int ACCOUNT_TYPE = 1; int ACCOUNT_NAME = 2; int SOURCE_ID = 3; } private long lookupContactIdBySourceIds(SQLiteDatabase db, ArrayList segments) { StringBuilder sb = new StringBuilder(); sb.append(RawContacts.SOURCE_ID + " IN ("); for (int i = 0; i < segments.size(); i++) { LookupKeySegment segment = segments.get(i); if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) { DatabaseUtils.appendEscapedSQLString(sb, segment.key); sb.append(","); } } sb.setLength(sb.length() - 1); // Last comma sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, sb.toString(), null, null, null, null); try { while (c.moveToNext()) { String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE); String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); int accountHashCode = ContactLookupKey.getAccountHashCode(accountType, accountName); String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); for (int i = 0; i < segments.size(); i++) { LookupKeySegment segment = segments.get(i); if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID && accountHashCode == segment.accountHashCode && segment.key.equals(sourceId)) { segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); break; } } } } finally { c.close(); } return getMostReferencedContactId(segments); } private interface LookupByRawContactIdQuery { String TABLE = Tables.RAW_CONTACTS; String COLUMNS[] = { RawContacts.CONTACT_ID, RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME, RawContacts._ID, }; int CONTACT_ID = 0; int ACCOUNT_TYPE = 1; int ACCOUNT_NAME = 2; int ID = 3; } private long lookupContactIdByRawContactIds(SQLiteDatabase db, ArrayList segments) { StringBuilder sb = new StringBuilder(); sb.append(RawContacts._ID + " IN ("); for (int i = 0; i < segments.size(); i++) { LookupKeySegment segment = segments.get(i); if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { sb.append(segment.rawContactId); sb.append(","); } } sb.setLength(sb.length() - 1); // Last comma sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS, sb.toString(), null, null, null, null); try { while (c.moveToNext()) { String accountType = c.getString(LookupByRawContactIdQuery.ACCOUNT_TYPE); String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME); int accountHashCode = ContactLookupKey.getAccountHashCode(accountType, accountName); String rawContactId = c.getString(LookupByRawContactIdQuery.ID); for (int i = 0; i < segments.size(); i++) { LookupKeySegment segment = segments.get(i); if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID && accountHashCode == segment.accountHashCode && segment.rawContactId.equals(rawContactId)) { segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID); break; } } } } finally { c.close(); } return getMostReferencedContactId(segments); } private interface LookupByDisplayNameQuery { String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; String COLUMNS[] = { RawContacts.CONTACT_ID, RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME, NameLookupColumns.NORMALIZED_NAME }; int CONTACT_ID = 0; int ACCOUNT_TYPE = 1; int ACCOUNT_NAME = 2; int NORMALIZED_NAME = 3; } private long lookupContactIdByDisplayNames(SQLiteDatabase db, ArrayList segments) { StringBuilder sb = new StringBuilder(); sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); for (int i = 0; i < segments.size(); i++) { LookupKeySegment segment = segments.get(i); if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { DatabaseUtils.appendEscapedSQLString(sb, segment.key); sb.append(","); } } sb.setLength(sb.length() - 1); // Last comma sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, sb.toString(), null, null, null, null); try { while (c.moveToNext()) { String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE); String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); int accountHashCode = ContactLookupKey.getAccountHashCode(accountType, accountName); String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); for (int i = 0; i < segments.size(); i++) { LookupKeySegment segment = segments.get(i); if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) && accountHashCode == segment.accountHashCode && segment.key.equals(name)) { segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); break; } } } } finally { c.close(); } return getMostReferencedContactId(segments); } private boolean lookupKeyContainsType(ArrayList segments, int lookupType) { for (int i = 0; i < segments.size(); i++) { LookupKeySegment segment = segments.get(i); if (segment.lookupType == lookupType) { return true; } } return false; } public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) { mContactAggregator.updateLookupKeyForRawContact(db, rawContactId); } /** * Returns the contact ID that is mentioned the highest number of times. */ private long getMostReferencedContactId(ArrayList segments) { Collections.sort(segments); long bestContactId = -1; int bestRefCount = 0; long contactId = -1; int count = 0; int segmentCount = segments.size(); for (int i = 0; i < segmentCount; i++) { LookupKeySegment segment = segments.get(i); if (segment.contactId != -1) { if (segment.contactId == contactId) { count++; } else { if (count > bestRefCount) { bestContactId = contactId; bestRefCount = count; } contactId = segment.contactId; count = 1; } } } if (count > bestRefCount) { return contactId; } else { return bestContactId; } } private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, String[] projection) { StringBuilder sb = new StringBuilder(); appendContactsTables(sb, uri, projection); qb.setTables(sb.toString()); qb.setProjectionMap(sContactsProjectionMap); } /** * Finds name lookup records matching the supplied filter, picks one arbitrary match per * contact and joins that with other contacts tables. */ private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri, String[] projection, String filter) { StringBuilder sb = new StringBuilder(); appendContactsTables(sb, uri, projection); sb.append(" JOIN (SELECT " + RawContacts.CONTACT_ID + " AS snippet_contact_id"); if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA_ID)) { sb.append(", " + DataColumns.CONCRETE_ID + " AS " + SearchSnippetColumns.SNIPPET_DATA_ID); } if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA1)) { sb.append(", " + Data.DATA1 + " AS " + SearchSnippetColumns.SNIPPET_DATA1); } if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA2)) { sb.append(", " + Data.DATA2 + " AS " + SearchSnippetColumns.SNIPPET_DATA2); } if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA3)) { sb.append(", " + Data.DATA3 + " AS " + SearchSnippetColumns.SNIPPET_DATA3); } if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA4)) { sb.append(", " + Data.DATA4 + " AS " + SearchSnippetColumns.SNIPPET_DATA4); } if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_MIMETYPE)) { sb.append(", (" + "SELECT " + MimetypesColumns.MIMETYPE + " FROM " + Tables.MIMETYPES + " WHERE " + MimetypesColumns._ID + "=" + DataColumns.MIMETYPE_ID + ") AS " + SearchSnippetColumns.SNIPPET_MIMETYPE); } sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS + " WHERE "); if (!TextUtils.isEmpty(filter)) { sb.append(DataColumns.CONCRETE_ID + " IN ("); // Construct a query that gives us exactly one data _id per matching contact. // MIN stands in for ANY in this context. sb.append( "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" + " FROM " + Tables.NAME_LOOKUP + " JOIN " + Tables.RAW_CONTACTS + " ON (" + RawContactsColumns.CONCRETE_ID + "=" + Tables.NAME_LOOKUP + "." + NameLookupColumns.RAW_CONTACT_ID + ")" + " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '"); sb.append(NameNormalizer.normalize(filter)); sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" + " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID + ")"); } else { sb.append("0"); // Empty filter - return an empty set } sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)"); qb.setTables(sb.toString()); qb.setProjectionMap(sContactsProjectionWithSnippetMap); } private void appendContactsTables(StringBuilder sb, Uri uri, String[] projection) { boolean excludeRestrictedData = false; String requestingPackage = getQueryParameter(uri, ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); if (requestingPackage != null) { excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); } sb.append(mDbHelper.getContactView(excludeRestrictedData)); appendContactPresenceJoin(sb, projection, Contacts._ID); appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); } private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) { StringBuilder sb = new StringBuilder(); boolean excludeRestrictedData = false; String requestingPackage = getQueryParameter(uri, ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); if (requestingPackage != null) { excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); } sb.append(mDbHelper.getRawContactView(excludeRestrictedData)); qb.setTables(sb.toString()); qb.setProjectionMap(sRawContactsProjectionMap); appendAccountFromParameter(qb, uri); } private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) { qb.setTables(mDbHelper.getRawEntitiesView(shouldExcludeRestrictedData(uri))); qb.setProjectionMap(sRawEntityProjectionMap); appendAccountFromParameter(qb, uri); } private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, String[] projection, boolean distinct) { StringBuilder sb = new StringBuilder(); sb.append(mDbHelper.getDataView(shouldExcludeRestrictedData(uri))); sb.append(" data"); appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID); appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); qb.setTables(sb.toString()); qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap); appendAccountFromParameter(qb, uri); } private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb, String[] projection) { StringBuilder sb = new StringBuilder(); sb.append(mDbHelper.getDataView()); sb.append(" data"); appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); qb.setTables(sb.toString()); qb.setProjectionMap(sStatusUpdatesProjectionMap); } private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri, String[] projection) { StringBuilder sb = new StringBuilder(); sb.append(mDbHelper.getEntitiesView(shouldExcludeRestrictedData(uri))); sb.append(" data"); appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID); appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID); appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID); qb.setTables(sb.toString()); qb.setProjectionMap(sEntityProjectionMap); appendAccountFromParameter(qb, uri); } private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection, String lastStatusUpdateIdColumn) { if (mDbHelper.isInProjection(projection, Contacts.CONTACT_STATUS, Contacts.CONTACT_STATUS_RES_PACKAGE, Contacts.CONTACT_STATUS_ICON, Contacts.CONTACT_STATUS_LABEL, Contacts.CONTACT_STATUS_TIMESTAMP)) { sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " + ContactsStatusUpdatesColumns.ALIAS + " ON (" + lastStatusUpdateIdColumn + "=" + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); } } private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection, String dataIdColumn) { if (mDbHelper.isInProjection(projection, StatusUpdates.STATUS, StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_TIMESTAMP)) { sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "=" + dataIdColumn + ")"); } } private void appendContactPresenceJoin(StringBuilder sb, String[] projection, String contactIdColumn) { if (mDbHelper.isInProjection(projection, Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) { sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + " ON (" + contactIdColumn + " = " + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")"); } } private void appendDataPresenceJoin(StringBuilder sb, String[] projection, String dataIdColumn) { if (mDbHelper.isInProjection(projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) { sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")"); } } private boolean shouldExcludeRestrictedData(Uri uri) { // Note: currently, "export only" equals to "restricted", but may not in the future. boolean excludeRestrictedData = readBooleanQueryParameter(uri, Data.FOR_EXPORT_ONLY, false); if (excludeRestrictedData) { return true; } String requestingPackage = getQueryParameter(uri, ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); if (requestingPackage != null) { return !mDbHelper.hasAccessToRestrictedData(requestingPackage); } return false; } private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); if (partialUri) { // Throw when either account is incomplete throw new IllegalArgumentException(mDbHelper.exceptionMessage( "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); } // Accounts are valid by only checking one parameter, since we've // already ruled out partial accounts. final boolean validAccount = !TextUtils.isEmpty(accountName); if (validAccount) { qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" + DatabaseUtils.sqlEscapeString(accountName) + " AND " + RawContacts.ACCOUNT_TYPE + "=" + DatabaseUtils.sqlEscapeString(accountType)); } else { qb.appendWhere("1"); } } private String appendAccountToSelection(Uri uri, String selection) { final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); if (partialUri) { // Throw when either account is incomplete throw new IllegalArgumentException(mDbHelper.exceptionMessage( "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); } // Accounts are valid by only checking one parameter, since we've // already ruled out partial accounts. final boolean validAccount = !TextUtils.isEmpty(accountName); if (validAccount) { StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" + DatabaseUtils.sqlEscapeString(accountName) + " AND " + RawContacts.ACCOUNT_TYPE + "=" + DatabaseUtils.sqlEscapeString(accountType)); if (!TextUtils.isEmpty(selection)) { selectionSb.append(" AND ("); selectionSb.append(selection); selectionSb.append(')'); } return selectionSb.toString(); } else { return selection; } } /** * Gets the value of the "limit" URI query parameter. * * @return A string containing a non-negative integer, or null if * the parameter is not set, or is set to an invalid value. */ private String getLimit(Uri uri) { String limitParam = getQueryParameter(uri, "limit"); if (limitParam == null) { return null; } // make sure that the limit is a non-negative integer try { int l = Integer.parseInt(limitParam); if (l < 0) { Log.w(TAG, "Invalid limit parameter: " + limitParam); return null; } return String.valueOf(l); } catch (NumberFormatException ex) { Log.w(TAG, "Invalid limit parameter: " + limitParam); return null; } } /** * Returns true if all the characters are meaningful as digits * in a phone number -- letters, digits, and a few punctuation marks. */ private boolean isPhoneNumber(CharSequence cons) { int len = cons.length(); for (int i = 0; i < len; i++) { char c = cons.charAt(i); if ((c >= '0') && (c <= '9')) { continue; } if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+') || (c == '#') || (c == '*')) { continue; } if ((c >= 'A') && (c <= 'Z')) { continue; } if ((c >= 'a') && (c <= 'z')) { continue; } return false; } return true; } String getContactsRestrictions() { if (mDbHelper.hasAccessToRestrictedData()) { return "1"; } else { return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0"; } } public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) { if (mDbHelper.hasAccessToRestrictedData()) { return "1"; } else { return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0"; } } @Override public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { int match = sUriMatcher.match(uri); switch (match) { case CONTACTS_ID_PHOTO: { return openPhotoAssetFile(uri, mode, Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?", new String[]{uri.getPathSegments().get(1)}); } case DATA_ID: { return openPhotoAssetFile(uri, mode, Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'", new String[]{uri.getPathSegments().get(1)}); } case CONTACTS_AS_VCARD: { final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); mSelectionArgs1[0] = String.valueOf(lookupContactIdByLookupKey(mDb, lookupKey)); final String selection = Contacts._ID + "=?"; // When opening a contact as file, we pass back contents as a // vCard-encoded stream. We build into a local buffer first, // then pipe into MemoryFile once the exact size is known. final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); outputRawContactsAsVCard(localStream, selection, mSelectionArgs1); return buildAssetFileDescriptor(localStream); } case CONTACTS_AS_MULTI_VCARD: { final String lookupKeys = uri.getPathSegments().get(2); final String[] loopupKeyList = lookupKeys.split(":"); final StringBuilder inBuilder = new StringBuilder(); int index = 0; // SQLite has limits on how many parameters can be used // so the IDs are concatenated to a query string here instead for (String lookupKey : loopupKeyList) { if (index == 0) { inBuilder.append("("); } else { inBuilder.append(","); } inBuilder.append(lookupContactIdByLookupKey(mDb, lookupKey)); index++; } inBuilder.append(')'); final String selection = Contacts._ID + " IN " + inBuilder.toString(); // When opening a contact as file, we pass back contents as a // vCard-encoded stream. We build into a local buffer first, // then pipe into MemoryFile once the exact size is known. final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); outputRawContactsAsVCard(localStream, selection, null); return buildAssetFileDescriptor(localStream); } default: throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist", uri)); } } private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection, String[] selectionArgs) throws FileNotFoundException { if (!"r".equals(mode)) { throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode + " not supported.", uri)); } String sql = "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() + " WHERE " + selection; SQLiteDatabase db = mDbHelper.getReadableDatabase(); return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, selectionArgs); } private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile"; /** * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the * contents of the given {@link ByteArrayOutputStream}. */ private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) { AssetFileDescriptor fd = null; try { stream.flush(); final byte[] byteData = stream.toByteArray(); final int size = byteData.length; final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size); memoryFile.writeBytes(byteData, 0, 0, size); memoryFile.deactivate(); fd = AssetFileDescriptor.fromMemoryFile(memoryFile); } catch (IOException e) { Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString()); } return fd; } /** * Output {@link RawContacts} matching the requested selection in the vCard * format to the given {@link OutputStream}. This method returns silently if * any errors encountered. */ private void outputRawContactsAsVCard(OutputStream stream, String selection, String[] selectionArgs) { final Context context = this.getContext(); final VCardComposer composer = new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false); composer.addHandler(composer.new HandlerForOutputStream(stream)); // No extra checks since composer always uses restricted views if (!composer.init(selection, selectionArgs)) { Log.w(TAG, "Failed to init VCardComposer"); return; } while (!composer.isAfterLast()) { if (!composer.createOneEntry()) { Log.w(TAG, "Failed to output a contact."); } } composer.terminate(); } @Override public String getType(Uri uri) { final int match = sUriMatcher.match(uri); switch (match) { case CONTACTS: return Contacts.CONTENT_TYPE; case CONTACTS_LOOKUP: case CONTACTS_ID: case CONTACTS_LOOKUP_ID: return Contacts.CONTENT_ITEM_TYPE; case CONTACTS_AS_VCARD: case CONTACTS_AS_MULTI_VCARD: return Contacts.CONTENT_VCARD_TYPE; case CONTACTS_ID_PHOTO: return "image/png"; case RAW_CONTACTS: return RawContacts.CONTENT_TYPE; case RAW_CONTACTS_ID: return RawContacts.CONTENT_ITEM_TYPE; case DATA_ID: return mDbHelper.getDataMimeType(ContentUris.parseId(uri)); case PHONES: return Phone.CONTENT_TYPE; case PHONES_ID: return Phone.CONTENT_ITEM_TYPE; case PHONE_LOOKUP: return PhoneLookup.CONTENT_TYPE; case EMAILS: return Email.CONTENT_TYPE; case EMAILS_ID: return Email.CONTENT_ITEM_TYPE; case POSTALS: return StructuredPostal.CONTENT_TYPE; case POSTALS_ID: return StructuredPostal.CONTENT_ITEM_TYPE; case AGGREGATION_EXCEPTIONS: return AggregationExceptions.CONTENT_TYPE; case AGGREGATION_EXCEPTION_ID: return AggregationExceptions.CONTENT_ITEM_TYPE; case SETTINGS: return Settings.CONTENT_TYPE; case AGGREGATION_SUGGESTIONS: return Contacts.CONTENT_TYPE; case SEARCH_SUGGESTIONS: return SearchManager.SUGGEST_MIME_TYPE; case SEARCH_SHORTCUT: return SearchManager.SHORTCUT_MIME_TYPE; case DIRECTORIES: return Directory.CONTENT_TYPE; case DIRECTORIES_ID: return Directory.CONTENT_ITEM_TYPE; default: return mLegacyApiSupport.getType(uri); } } public String[] getDefaultProjection(Uri uri) { final int match = sUriMatcher.match(uri); switch (match) { case CONTACTS: case CONTACTS_LOOKUP: case CONTACTS_ID: case CONTACTS_LOOKUP_ID: case AGGREGATION_SUGGESTIONS: return sContactsProjectionMap.getColumnNames(); case CONTACTS_AS_VCARD: case CONTACTS_AS_MULTI_VCARD: return sContactsVCardProjectionMap.getColumnNames(); case RAW_CONTACTS: case RAW_CONTACTS_ID: return sRawContactsProjectionMap.getColumnNames(); case DATA_ID: case PHONES: case PHONES_ID: case EMAILS: case EMAILS_ID: case POSTALS: case POSTALS_ID: return sDataProjectionMap.getColumnNames(); case PHONE_LOOKUP: return sPhoneLookupProjectionMap.getColumnNames(); case AGGREGATION_EXCEPTIONS: case AGGREGATION_EXCEPTION_ID: return sAggregationExceptionsProjectionMap.getColumnNames(); case SETTINGS: return sSettingsProjectionMap.getColumnNames(); case DIRECTORIES: case DIRECTORIES_ID: return sDirectoryProjectionMap.getColumnNames(); default: return null; } } private void setDisplayName(long rawContactId, int displayNameSource, String displayNamePrimary, String displayNameAlternative, String phoneticName, int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) { mRawContactDisplayNameUpdate.bindLong(1, displayNameSource); bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary); bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative); bindString(mRawContactDisplayNameUpdate, 4, phoneticName); mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle); bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary); bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative); mRawContactDisplayNameUpdate.bindLong(8, rawContactId); mRawContactDisplayNameUpdate.execute(); } /** * Sets the {@link RawContacts#DIRTY} for the specified raw contact. */ private void setRawContactDirty(long rawContactId) { mDirtyRawContacts.add(rawContactId); } /* * Sets the given dataId record in the "data" table to primary, and resets all data records of * the same mimetype and under the same contact to not be primary. * * @param dataId the id of the data record to be set to primary. */ private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) { mSetPrimaryStatement.bindLong(1, dataId); mSetPrimaryStatement.bindLong(2, mimeTypeId); mSetPrimaryStatement.bindLong(3, rawContactId); mSetPrimaryStatement.execute(); } /* * Sets the given dataId record in the "data" table to "super primary", and resets all data * records of the same mimetype and under the same aggregate to not be "super primary". * * @param dataId the id of the data record to be set to primary. */ private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) { mSetSuperPrimaryStatement.bindLong(1, dataId); mSetSuperPrimaryStatement.bindLong(2, mimeTypeId); mSetSuperPrimaryStatement.bindLong(3, rawContactId); mSetSuperPrimaryStatement.execute(); } public String insertNameLookupForEmail(long rawContactId, long dataId, String email) { if (TextUtils.isEmpty(email)) { return null; } String address = mDbHelper.extractHandleFromEmailAddress(email); if (address == null) { return null; } insertNameLookup(rawContactId, dataId, NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address)); return address; } /** * Normalizes the nickname and inserts it in the name lookup table. */ public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) { if (TextUtils.isEmpty(nickname)) { return; } insertNameLookup(rawContactId, dataId, NameLookupType.NICKNAME, NameNormalizer.normalize(nickname)); } public void insertNameLookupForOrganization(long rawContactId, long dataId, String company, String title) { if (!TextUtils.isEmpty(company)) { insertNameLookup(rawContactId, dataId, NameLookupType.ORGANIZATION, NameNormalizer.normalize(company)); } if (!TextUtils.isEmpty(title)) { insertNameLookup(rawContactId, dataId, NameLookupType.ORGANIZATION, NameNormalizer.normalize(title)); } } public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name, int fullNameStyle) { mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle); } private class StructuredNameLookupBuilder extends NameLookupBuilder { public StructuredNameLookupBuilder(NameSplitter splitter) { super(splitter); } @Override protected void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) { ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name); } @Override protected String[] getCommonNicknameClusters(String normalizedName) { return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); } } public void insertNameLookupForPhoneticName(long rawContactId, long dataId, ContentValues values) { if (values.containsKey(StructuredName.PHONETIC_FAMILY_NAME) || values.containsKey(StructuredName.PHONETIC_GIVEN_NAME) || values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME)) { insertNameLookupForPhoneticName(rawContactId, dataId, values.getAsString(StructuredName.PHONETIC_FAMILY_NAME), values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME), values.getAsString(StructuredName.PHONETIC_GIVEN_NAME)); } } public void insertNameLookupForPhoneticName(long rawContactId, long dataId, String familyName, String middleName, String givenName) { mSb.setLength(0); if (familyName != null) { mSb.append(familyName.trim()); } if (middleName != null) { mSb.append(middleName.trim()); } if (givenName != null) { mSb.append(givenName.trim()); } if (mSb.length() > 0) { insertNameLookup(rawContactId, dataId, NameLookupType.NAME_COLLATION_KEY, NameNormalizer.normalize(mSb.toString())); } if (givenName != null) { // We want the phonetic given name to be used for search, but not for aggregation, // which is why we are using NAME_SHORTHAND rather than NAME_COLLATION_KEY insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND, NameNormalizer.normalize(givenName.trim())); } } /** * Inserts a record in the {@link Tables#NAME_LOOKUP} table. */ public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) { mNameLookupInsert.bindLong(1, rawContactId); mNameLookupInsert.bindLong(2, dataId); mNameLookupInsert.bindLong(3, lookupType); bindString(mNameLookupInsert, 4, name); mNameLookupInsert.executeInsert(); } /** * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element. */ public void deleteNameLookup(long dataId) { mNameLookupDelete.bindLong(1, dataId); mNameLookupDelete.execute(); } public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { sb.append("(" + "SELECT DISTINCT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + " JOIN " + Tables.NAME_LOOKUP + " ON(" + RawContactsColumns.CONCRETE_ID + "=" + NameLookupColumns.RAW_CONTACT_ID + ")" + " WHERE normalized_name GLOB '"); sb.append(NameNormalizer.normalize(filterParam)); sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))"); } public String getRawContactsByFilterAsNestedQuery(String filterParam) { StringBuilder sb = new StringBuilder(); appendRawContactsByFilterAsNestedQuery(sb, filterParam); return sb.toString(); } public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) { appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true); } private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, boolean allowEmailMatch) { sb.append("(" + "SELECT " + NameLookupColumns.RAW_CONTACT_ID + " FROM " + Tables.NAME_LOOKUP + " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '"); sb.append(normalizedName); sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN (" + NameLookupType.NAME_COLLATION_KEY + "," + NameLookupType.NICKNAME + "," + NameLookupType.NAME_SHORTHAND + "," + NameLookupType.ORGANIZATION + "," + NameLookupType.NAME_CONSONANTS); if (allowEmailMatch) { sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME); } sb.append("))"); } /** * Inserts an argument at the beginning of the selection arg list. */ private String[] insertSelectionArg(String[] selectionArgs, String arg) { if (selectionArgs == null) { return new String[] {arg}; } else { int newLength = selectionArgs.length + 1; String[] newSelectionArgs = new String[newLength]; newSelectionArgs[0] = arg; System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); return newSelectionArgs; } } private String[] appendProjectionArg(String[] projection, String arg) { if (projection == null) { return null; } final int length = projection.length; String[] newProjection = new String[length + 1]; System.arraycopy(projection, 0, newProjection, 0, length); newProjection[length] = arg; return newProjection; } protected Account getDefaultAccount() { AccountManager accountManager = AccountManager.get(getContext()); try { Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE, new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult(); if (accounts != null && accounts.length > 0) { return accounts[0]; } } catch (Throwable e) { Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); } return null; } /** * Returns true if the specified account type is writable. */ protected boolean isWritableAccount(String accountType) { if (accountType == null) { return true; } Boolean writable = mAccountWritability.get(accountType); if (writable != null) { return writable; } IContentService contentService = ContentResolver.getContentService(); try { for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) { if (ContactsContract.AUTHORITY.equals(sync.authority) && accountType.equals(sync.accountType)) { writable = sync.supportsUploading(); break; } } } catch (RemoteException e) { Log.e(TAG, "Could not acquire sync adapter types"); } if (writable == null) { writable = false; } mAccountWritability.put(accountType, writable); return writable; } /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter, boolean defaultValue) { // Manually parse the query, which is much faster than calling uri.getQueryParameter String query = uri.getEncodedQuery(); if (query == null) { return defaultValue; } int index = query.indexOf(parameter); if (index == -1) { return defaultValue; } index += parameter.length(); return !matchQueryParameter(query, index, "=0", false) && !matchQueryParameter(query, index, "=false", true); } private static boolean matchQueryParameter(String query, int index, String value, boolean ignoreCase) { int length = value.length(); return query.regionMatches(ignoreCase, index, value, 0, length) && (query.length() == index + length || query.charAt(index + length) == '&'); } /** * A fast re-implementation of {@link Uri#getQueryParameter} */ /* package */ static String getQueryParameter(Uri uri, String parameter) { String query = uri.getEncodedQuery(); if (query == null) { return null; } int queryLength = query.length(); int parameterLength = parameter.length(); String value; int index = 0; while (true) { index = query.indexOf(parameter, index); if (index == -1) { return null; } index += parameterLength; if (queryLength == index) { return null; } if (query.charAt(index) == '=') { index++; break; } } int ampIndex = query.indexOf('&', index); if (ampIndex == -1) { value = query.substring(index); } else { value = query.substring(index, ampIndex); } return Uri.decode(value); } private void bindString(SQLiteStatement stmt, int index, String value) { if (value == null) { stmt.bindNull(index); } else { stmt.bindString(index, value); } } private void bindLong(SQLiteStatement stmt, int index, Number value) { if (value == null) { stmt.bindNull(index); } else { stmt.bindLong(index, value.longValue()); } } }