| /* |
| * 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.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.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.MemoryFile; |
| import android.os.RemoteException; |
| import android.os.SystemProperties; |
| import android.pim.vcard.VCardComposer; |
| import android.pim.vcard.VCardConfig; |
| import android.preference.PreferenceManager; |
| import android.provider.BaseColumns; |
| import android.provider.ContactsContract; |
| import android.provider.LiveFolders; |
| import android.provider.OpenableColumns; |
| import android.provider.SyncStateContract; |
| import android.provider.ContactsContract.AggregationExceptions; |
| import android.provider.ContactsContract.ContactCounts; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Data; |
| 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.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.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; |
| |
| private static final String GOOGLE_MY_CONTACTS_GROUP_TITLE = "System Group: My Contacts"; |
| /** |
| * 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_CONTACED_SORT_COLUMN = "times_contacted_sort"; |
| |
| private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, " |
| + TIMES_CONTACED_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_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_PHOTO = 1009; |
| private static final int CONTACTS_AS_VCARD = 1010; |
| private static final int CONTACTS_AS_MULTI_VCARD = 1011; |
| |
| 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 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; |
| |
| |
| /** Contains just BaseColumns._COUNT */ |
| private static final HashMap<String, String> sCountProjectionMap; |
| /** Contains just the contacts columns */ |
| private static final HashMap<String, String> sContactsProjectionMap; |
| /** Contains just the contacts columns */ |
| private static final HashMap<String, String> sContactsProjectionWithSnippetMap; |
| |
| /** Used for pushing starred contacts to the top of a times contacted list **/ |
| private static final HashMap<String, String> sStrequentStarredProjectionMap; |
| private static final HashMap<String, String> sStrequentFrequentProjectionMap; |
| /** Contains just the contacts vCard columns */ |
| private static final HashMap<String, String> sContactsVCardProjectionMap; |
| /** Contains just the raw contacts columns */ |
| private static final HashMap<String, String> sRawContactsProjectionMap; |
| /** Contains the columns from the raw contacts entity view*/ |
| private static final HashMap<String, String> sRawContactsEntityProjectionMap; |
| /** Contains columns from the data view */ |
| private static final HashMap<String, String> sDataProjectionMap; |
| /** Contains columns from the data view */ |
| private static final HashMap<String, String> sDistinctDataProjectionMap; |
| /** Contains the data and contacts columns, for joined tables */ |
| private static final HashMap<String, String> sPhoneLookupProjectionMap; |
| /** Contains the just the {@link Groups} columns */ |
| private static final HashMap<String, String> sGroupsProjectionMap; |
| /** Contains {@link Groups} columns along with summary details */ |
| private static final HashMap<String, String> sGroupsSummaryProjectionMap; |
| /** Contains the agg_exceptions columns */ |
| private static final HashMap<String, String> sAggregationExceptionsProjectionMap; |
| /** Contains the agg_exceptions columns */ |
| private static final HashMap<String, String> sSettingsProjectionMap; |
| /** Contains StatusUpdates columns */ |
| private static final HashMap<String, String> sStatusUpdatesProjectionMap; |
| /** Contains Live Folders columns */ |
| private static final HashMap<String, String> sLiveFoldersProjectionMap; |
| |
| // 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<String> 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_DATA); |
| matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", |
| AGGREGATION_SUGGESTIONS); |
| matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*", |
| AGGREGATION_SUGGESTIONS); |
| matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO); |
| matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER); |
| matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP); |
| matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID); |
| 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); |
| } |
| |
| static { |
| sCountProjectionMap = new HashMap<String, String>(); |
| sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)"); |
| |
| sContactsProjectionMap = new HashMap<String, String>(); |
| sContactsProjectionMap.put(Contacts._ID, Contacts._ID); |
| sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY); |
| sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, |
| Contacts.DISPLAY_NAME_ALTERNATIVE); |
| sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE); |
| sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME); |
| sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE); |
| sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY); |
| sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE); |
| sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); |
| sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); |
| sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED); |
| sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); |
| sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); |
| sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); |
| sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER); |
| sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); |
| sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); |
| |
| // Handle projections for Contacts-level statuses |
| addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE, |
| Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE); |
| addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS); |
| addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); |
| addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); |
| addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL); |
| addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON); |
| |
| sContactsProjectionWithSnippetMap = new HashMap<String, String>(); |
| sContactsProjectionWithSnippetMap.putAll(sContactsProjectionMap); |
| sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_MIMETYPE, |
| SearchSnippetColumns.SNIPPET_MIMETYPE); |
| sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA_ID, |
| SearchSnippetColumns.SNIPPET_DATA_ID); |
| sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA1, |
| SearchSnippetColumns.SNIPPET_DATA1); |
| sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA2, |
| SearchSnippetColumns.SNIPPET_DATA2); |
| sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA3, |
| SearchSnippetColumns.SNIPPET_DATA3); |
| sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA4, |
| SearchSnippetColumns.SNIPPET_DATA4); |
| |
| sStrequentStarredProjectionMap = new HashMap<String, String>(sContactsProjectionMap); |
| sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, |
| Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN); |
| |
| sStrequentFrequentProjectionMap = new HashMap<String, String>(sContactsProjectionMap); |
| sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, |
| Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN); |
| |
| sContactsVCardProjectionMap = Maps.newHashMap(); |
| sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME |
| + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME); |
| sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "NULL AS " + OpenableColumns.SIZE); |
| |
| sRawContactsProjectionMap = new HashMap<String, String>(); |
| sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID); |
| sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); |
| sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); |
| sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); |
| sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); |
| sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); |
| sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); |
| sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED); |
| sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY, |
| RawContacts.DISPLAY_NAME_PRIMARY); |
| sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, |
| RawContacts.DISPLAY_NAME_ALTERNATIVE); |
| sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE, |
| RawContacts.DISPLAY_NAME_SOURCE); |
| sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME, |
| RawContacts.PHONETIC_NAME); |
| sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE, |
| RawContacts.PHONETIC_NAME_STYLE); |
| sRawContactsProjectionMap.put(RawContacts.NAME_VERIFIED, |
| RawContacts.NAME_VERIFIED); |
| sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY, |
| RawContacts.SORT_KEY_PRIMARY); |
| sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE, |
| RawContacts.SORT_KEY_ALTERNATIVE); |
| sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED); |
| sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED, |
| RawContacts.LAST_TIME_CONTACTED); |
| sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE); |
| sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL); |
| sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED); |
| sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE); |
| sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1); |
| sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2); |
| sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3); |
| sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4); |
| |
| sDataProjectionMap = new HashMap<String, String>(); |
| sDataProjectionMap.put(Data._ID, Data._ID); |
| sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID); |
| sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); |
| sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); |
| sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); |
| sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); |
| sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); |
| sDataProjectionMap.put(Data.DATA1, Data.DATA1); |
| sDataProjectionMap.put(Data.DATA2, Data.DATA2); |
| sDataProjectionMap.put(Data.DATA3, Data.DATA3); |
| sDataProjectionMap.put(Data.DATA4, Data.DATA4); |
| sDataProjectionMap.put(Data.DATA5, Data.DATA5); |
| sDataProjectionMap.put(Data.DATA6, Data.DATA6); |
| sDataProjectionMap.put(Data.DATA7, Data.DATA7); |
| sDataProjectionMap.put(Data.DATA8, Data.DATA8); |
| sDataProjectionMap.put(Data.DATA9, Data.DATA9); |
| sDataProjectionMap.put(Data.DATA10, Data.DATA10); |
| sDataProjectionMap.put(Data.DATA11, Data.DATA11); |
| sDataProjectionMap.put(Data.DATA12, Data.DATA12); |
| sDataProjectionMap.put(Data.DATA13, Data.DATA13); |
| sDataProjectionMap.put(Data.DATA14, Data.DATA14); |
| sDataProjectionMap.put(Data.DATA15, Data.DATA15); |
| sDataProjectionMap.put(Data.SYNC1, Data.SYNC1); |
| sDataProjectionMap.put(Data.SYNC2, Data.SYNC2); |
| sDataProjectionMap.put(Data.SYNC3, Data.SYNC3); |
| sDataProjectionMap.put(Data.SYNC4, Data.SYNC4); |
| sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID); |
| sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); |
| sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); |
| sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); |
| sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); |
| sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); |
| sDataProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED); |
| sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); |
| sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); |
| sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, |
| Contacts.DISPLAY_NAME_ALTERNATIVE); |
| sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE); |
| sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME); |
| sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE); |
| sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY); |
| sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE); |
| sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); |
| sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); |
| sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); |
| sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); |
| sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); |
| sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); |
| sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); |
| sDataProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID); |
| sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID); |
| |
| HashMap<String, String> columns; |
| columns = new HashMap<String, String>(); |
| columns.put(RawContacts._ID, RawContacts._ID); |
| columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); |
| columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); |
| columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); |
| columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); |
| columns.put(RawContacts.VERSION, RawContacts.VERSION); |
| columns.put(RawContacts.DIRTY, RawContacts.DIRTY); |
| columns.put(RawContacts.DELETED, RawContacts.DELETED); |
| columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED); |
| columns.put(RawContacts.SYNC1, RawContacts.SYNC1); |
| columns.put(RawContacts.SYNC2, RawContacts.SYNC2); |
| columns.put(RawContacts.SYNC3, RawContacts.SYNC3); |
| columns.put(RawContacts.SYNC4, RawContacts.SYNC4); |
| columns.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED); |
| columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE); |
| columns.put(Data.MIMETYPE, Data.MIMETYPE); |
| columns.put(Data.DATA1, Data.DATA1); |
| columns.put(Data.DATA2, Data.DATA2); |
| columns.put(Data.DATA3, Data.DATA3); |
| columns.put(Data.DATA4, Data.DATA4); |
| columns.put(Data.DATA5, Data.DATA5); |
| columns.put(Data.DATA6, Data.DATA6); |
| columns.put(Data.DATA7, Data.DATA7); |
| columns.put(Data.DATA8, Data.DATA8); |
| columns.put(Data.DATA9, Data.DATA9); |
| columns.put(Data.DATA10, Data.DATA10); |
| columns.put(Data.DATA11, Data.DATA11); |
| columns.put(Data.DATA12, Data.DATA12); |
| columns.put(Data.DATA13, Data.DATA13); |
| columns.put(Data.DATA14, Data.DATA14); |
| columns.put(Data.DATA15, Data.DATA15); |
| columns.put(Data.SYNC1, Data.SYNC1); |
| columns.put(Data.SYNC2, Data.SYNC2); |
| columns.put(Data.SYNC3, Data.SYNC3); |
| columns.put(Data.SYNC4, Data.SYNC4); |
| columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID); |
| columns.put(Data.STARRED, Data.STARRED); |
| columns.put(Data.DATA_VERSION, Data.DATA_VERSION); |
| columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY); |
| columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); |
| columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID); |
| sRawContactsEntityProjectionMap = columns; |
| |
| // Handle projections for Contacts-level statuses |
| addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE, |
| Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE); |
| addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS); |
| addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); |
| addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); |
| addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL); |
| addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON); |
| |
| // Handle projections for Data-level statuses |
| addProjection(sDataProjectionMap, Data.PRESENCE, |
| Tables.PRESENCE + "." + StatusUpdates.PRESENCE); |
| addProjection(sDataProjectionMap, Data.STATUS, |
| StatusUpdatesColumns.CONCRETE_STATUS); |
| addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP, |
| StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); |
| addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE, |
| StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); |
| addProjection(sDataProjectionMap, Data.STATUS_LABEL, |
| StatusUpdatesColumns.CONCRETE_STATUS_LABEL); |
| addProjection(sDataProjectionMap, Data.STATUS_ICON, |
| StatusUpdatesColumns.CONCRETE_STATUS_ICON); |
| |
| // Projection map for data grouped by contact (not raw contact) and some data field(s) |
| sDistinctDataProjectionMap = new HashMap<String, String>(); |
| sDistinctDataProjectionMap.put(Data._ID, |
| "MIN(" + Data._ID + ") AS " + Data._ID); |
| sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); |
| sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); |
| sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); |
| sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); |
| sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); |
| sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1); |
| sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2); |
| sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3); |
| sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4); |
| sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5); |
| sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6); |
| sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7); |
| sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8); |
| sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9); |
| sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10); |
| sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11); |
| sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12); |
| sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13); |
| sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14); |
| sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15); |
| sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1); |
| sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2); |
| sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3); |
| sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4); |
| sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); |
| sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); |
| sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); |
| sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, |
| Contacts.DISPLAY_NAME_ALTERNATIVE); |
| sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE); |
| sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME); |
| sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE); |
| sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY); |
| sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, |
| Contacts.SORT_KEY_ALTERNATIVE); |
| sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); |
| sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); |
| sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); |
| sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); |
| sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); |
| sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); |
| sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); |
| sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, |
| GroupMembership.GROUP_SOURCE_ID); |
| |
| // Handle projections for Contacts-level statuses |
| addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE, |
| Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE); |
| addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS); |
| addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); |
| addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); |
| addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL); |
| addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON, |
| ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON); |
| |
| // Handle projections for Data-level statuses |
| addProjection(sDistinctDataProjectionMap, Data.PRESENCE, |
| Tables.PRESENCE + "." + StatusUpdates.PRESENCE); |
| addProjection(sDistinctDataProjectionMap, Data.STATUS, |
| StatusUpdatesColumns.CONCRETE_STATUS); |
| addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP, |
| StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); |
| addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE, |
| StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); |
| addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL, |
| StatusUpdatesColumns.CONCRETE_STATUS_LABEL); |
| addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON, |
| StatusUpdatesColumns.CONCRETE_STATUS_ICON); |
| |
| sPhoneLookupProjectionMap = new HashMap<String, String>(); |
| sPhoneLookupProjectionMap.put(PhoneLookup._ID, |
| "contacts_view." + Contacts._ID |
| + " AS " + PhoneLookup._ID); |
| sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY, |
| "contacts_view." + Contacts.LOOKUP_KEY |
| + " AS " + PhoneLookup.LOOKUP_KEY); |
| sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME, |
| "contacts_view." + Contacts.DISPLAY_NAME |
| + " AS " + PhoneLookup.DISPLAY_NAME); |
| sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED, |
| "contacts_view." + Contacts.LAST_TIME_CONTACTED |
| + " AS " + PhoneLookup.LAST_TIME_CONTACTED); |
| sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED, |
| "contacts_view." + Contacts.TIMES_CONTACTED |
| + " AS " + PhoneLookup.TIMES_CONTACTED); |
| sPhoneLookupProjectionMap.put(PhoneLookup.STARRED, |
| "contacts_view." + Contacts.STARRED |
| + " AS " + PhoneLookup.STARRED); |
| sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP, |
| "contacts_view." + Contacts.IN_VISIBLE_GROUP |
| + " AS " + PhoneLookup.IN_VISIBLE_GROUP); |
| sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID, |
| "contacts_view." + Contacts.PHOTO_ID |
| + " AS " + PhoneLookup.PHOTO_ID); |
| sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE, |
| "contacts_view." + Contacts.CUSTOM_RINGTONE |
| + " AS " + PhoneLookup.CUSTOM_RINGTONE); |
| sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER, |
| "contacts_view." + Contacts.HAS_PHONE_NUMBER |
| + " AS " + PhoneLookup.HAS_PHONE_NUMBER); |
| sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL, |
| "contacts_view." + Contacts.SEND_TO_VOICEMAIL |
| + " AS " + PhoneLookup.SEND_TO_VOICEMAIL); |
| sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER, |
| Phone.NUMBER + " AS " + PhoneLookup.NUMBER); |
| sPhoneLookupProjectionMap.put(PhoneLookup.TYPE, |
| Phone.TYPE + " AS " + PhoneLookup.TYPE); |
| sPhoneLookupProjectionMap.put(PhoneLookup.LABEL, |
| Phone.LABEL + " AS " + PhoneLookup.LABEL); |
| |
| // Groups projection map |
| columns = new HashMap<String, String>(); |
| columns.put(Groups._ID, Groups._ID); |
| columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME); |
| columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE); |
| columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID); |
| columns.put(Groups.DIRTY, Groups.DIRTY); |
| columns.put(Groups.VERSION, Groups.VERSION); |
| columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE); |
| columns.put(Groups.TITLE, Groups.TITLE); |
| columns.put(Groups.TITLE_RES, Groups.TITLE_RES); |
| columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE); |
| columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID); |
| columns.put(Groups.DELETED, Groups.DELETED); |
| columns.put(Groups.NOTES, Groups.NOTES); |
| columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC); |
| columns.put(Groups.SYNC1, Groups.SYNC1); |
| columns.put(Groups.SYNC2, Groups.SYNC2); |
| columns.put(Groups.SYNC3, Groups.SYNC3); |
| columns.put(Groups.SYNC4, Groups.SYNC4); |
| sGroupsProjectionMap = columns; |
| |
| // RawContacts and groups projection map |
| columns = new HashMap<String, String>(); |
| columns.putAll(sGroupsProjectionMap); |
| columns.put(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 |
| + ") AS " + Groups.SUMMARY_COUNT); |
| columns.put(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 + ") AS " + Groups.SUMMARY_WITH_PHONES); |
| sGroupsSummaryProjectionMap = columns; |
| |
| // Aggregate exception projection map |
| columns = new HashMap<String, String>(); |
| columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id"); |
| columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE); |
| columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1); |
| columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2); |
| sAggregationExceptionsProjectionMap = columns; |
| |
| // Settings projection map |
| columns = new HashMap<String, String>(); |
| columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME); |
| columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE); |
| columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE); |
| columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC); |
| columns.put(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) AS " |
| + Settings.ANY_UNSYNCED); |
| columns.put(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 |
| + ")) AS " + Settings.UNGROUPED_COUNT); |
| columns.put(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 + ")) AS " |
| + Settings.UNGROUPED_WITH_PHONES); |
| sSettingsProjectionMap = columns; |
| |
| columns = new HashMap<String, String>(); |
| columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID); |
| columns.put(StatusUpdates.DATA_ID, |
| DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID); |
| columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT); |
| columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE); |
| columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL); |
| // We cannot allow a null in the custom protocol field, because SQLite3 does not |
| // properly enforce uniqueness of null values |
| columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL |
| + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS " |
| + StatusUpdates.CUSTOM_PROTOCOL); |
| columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE); |
| columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS); |
| columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP); |
| columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE); |
| columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON); |
| columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL); |
| sStatusUpdatesProjectionMap = columns; |
| |
| // Live folder projection |
| sLiveFoldersProjectionMap = new HashMap<String, String>(); |
| sLiveFoldersProjectionMap.put(LiveFolders._ID, |
| Contacts._ID + " AS " + LiveFolders._ID); |
| sLiveFoldersProjectionMap.put(LiveFolders.NAME, |
| Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME); |
| // TODO: Put contact photo back when we have a way to display a default icon |
| // for contacts without a photo |
| // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP, |
| // Photos.DATA + " AS " + LiveFolders.ICON_BITMAP); |
| } |
| |
| private static void addProjection(HashMap<String, String> map, String toField, String fromField) { |
| map.put(toField, fromField + " AS " + toField); |
| } |
| |
| /** |
| * 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 { |
| |
| 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); |
| updateVisibility(rawContactId); |
| return dataId; |
| } |
| |
| @Override |
| public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, |
| boolean callerIsSyncAdapter) { |
| long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); |
| resolveGroupSourceIdInValues(rawContactId, db, values, false); |
| if (!super.update(db, values, c, callerIsSyncAdapter)) { |
| return false; |
| } |
| updateVisibility(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); |
| 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<String, DataRowHandler> 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<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap(); |
| |
| 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<String, Boolean> mAccountWritability = Maps.newHashMap(); |
| |
| private int mProviderStatus = ProviderStatus.STATUS_NORMAL; |
| private long mEstimatedStorageRequirement = 0; |
| private volatile CountDownLatch mAccessLatch; |
| |
| private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap(); |
| private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet(); |
| private HashSet<Long> mDirtyRawContacts = Sets.newHashSet(); |
| private HashMap<Long, Object> 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(); |
| 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(); |
| } |
| |
| return (mDb != null); |
| } |
| |
| private void initDataRowHandlers() { |
| mDataRowHandlers = new HashMap<String, DataRowHandler>(); |
| |
| 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<Integer, Void, Void> task = new AsyncTask<Integer, Void, Void>() { |
| |
| 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 */ |
| protected Locale getLocale() { |
| return Locale.getDefault(); |
| } |
| |
| 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<ContentProviderOperation> 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<Long, Object> 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<Long> 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); |
| 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. |
| * @return the row ID of the newly created row |
| */ |
| private long insertRawContact(Uri uri, ContentValues values) { |
| 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); |
| |
| return rawContactId; |
| } |
| |
| /** |
| * 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<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId); |
| if (entries == null) { |
| entries = new ArrayList<GroupIdCacheEntry>(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); |
| |
| if (!callerIsSyncAdapter) { |
| mValues.put(Groups.DIRTY, 1); |
| } |
| |
| long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues); |
| |
| 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, |
| Contacts.IN_VISIBLE_GROUP + " 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)); |
| |
| // 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); |
| } |
| |
| case CONTACTS_LOOKUP: { |
| final List<String> 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); |
| } |
| |
| case CONTACTS_LOOKUP_ID: { |
| // lookup contact by id and lookup key to see if they still match the actual record |
| long contactId = ContentUris.parseId(uri); |
| final List<String> pathSegments = uri.getPathSegments(); |
| final String lookupKey = pathSegments.get(2); |
| SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); |
| setTablesAndProjectionMapForContacts(lookupQb, uri, null); |
| 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); |
| } 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) { |
| 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); |
| } |
| } 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); |
| } |
| } |
| |
| 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) { |
| 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); |
| } |
| |
| @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); |
| break; |
| } |
| |
| case CONTACTS_ID: { |
| count = updateContactOptions(ContentUris.parseId(uri), values); |
| break; |
| } |
| |
| case CONTACTS_LOOKUP: |
| case CONTACTS_LOOKUP_ID: { |
| final List<String> 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); |
| 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); |
| 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); |
| } else { |
| mSelectionArgs1[0] = String.valueOf(rawContactId); |
| count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1); |
| } |
| 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; |
| } |
| |
| 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); |
| 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) { |
| if (values.containsKey(RawContacts.CONTACT_ID)) { |
| throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + |
| "in content values. Contact IDs are assigned automatically"); |
| } |
| |
| 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); |
| count++; |
| } |
| } finally { |
| cursor.close(); |
| } |
| |
| return count; |
| } |
| |
| private int updateRawContact(long rawContactId, ContentValues values) { |
| 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)) { |
| mContactAggregator.updateStarred(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); |
| } |
| |
| 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) { |
| 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); |
| count++; |
| } |
| } finally { |
| cursor.close(); |
| } |
| |
| return count; |
| } |
| |
| private int updateContactOptions(long contactId, ContentValues values) { |
| |
| 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 + "=?", mSelectionArgs1); |
| |
| // 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; |
| } |
| |
| /** |
| * Check whether GOOGLE_MY_CONTACTS_GROUP exists, otherwise create it. |
| * |
| * @return the group id |
| */ |
| private long getOrCreateMyContactsGroupInTransaction(String accountName, String accountType) { |
| Cursor cursor = mDb.query(Tables.GROUPS, new String[] {"_id"}, |
| Groups.ACCOUNT_NAME + " =? AND " + Groups.ACCOUNT_TYPE + " =? AND " |
| + Groups.TITLE + " =?", |
| new String[] {accountName, accountType, GOOGLE_MY_CONTACTS_GROUP_TITLE}, |
| null, null, null); |
| try { |
| if(cursor.moveToNext()) { |
| return cursor.getLong(0); |
| } |
| } finally { |
| cursor.close(); |
| } |
| |
| ContentValues values = new ContentValues(); |
| values.put(Groups.TITLE, GOOGLE_MY_CONTACTS_GROUP_TITLE); |
| values.put(Groups.ACCOUNT_NAME, accountName); |
| values.put(Groups.ACCOUNT_TYPE, accountType); |
| values.put(Groups.GROUP_VISIBLE, "1"); |
| return mDb.insert(Tables.GROUPS, null, values); |
| } |
| |
| public void onAccountsUpdated(Account[] accounts) { |
| // TODO : Check the unit test. |
| HashSet<Account> existingAccounts = new HashSet<Account>(); |
| boolean hasUnassignedContacts[] = new boolean[]{false}; |
| mDb.beginTransaction(); |
| try { |
| findValidAccounts(existingAccounts, hasUnassignedContacts); |
| |
| // Add a row to the ACCOUNTS table for each new account |
| for (Account account : accounts) { |
| if (!existingAccounts.contains(account)) { |
| 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<Account> accountsToDelete = new HashSet<Account>(existingAccounts); |
| for (Account account : accounts) { |
| accountsToDelete.remove(account); |
| } |
| |
| 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); |
| } |
| |
| if (!accountsToDelete.isEmpty()) { |
| // 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 of photos. If so, fix up those contacts. |
| HashSet<Long> 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); |
| } |
| } |
| |
| if (hasUnassignedContacts[0]) { |
| |
| Account primaryAccount = null; |
| for (Account account : accounts) { |
| if (isWritableAccount(account.type)) { |
| primaryAccount = account; |
| break; |
| } |
| } |
| |
| if (primaryAccount != null) { |
| String[] params = new String[] {primaryAccount.name, primaryAccount.type}; |
| if (primaryAccount.type.equals(DEFAULT_ACCOUNT_TYPE)) { |
| long groupId = getOrCreateMyContactsGroupInTransaction( |
| primaryAccount.name, primaryAccount.type); |
| if (groupId != -1) { |
| long mimeTypeId = mDbHelper.getMimeTypeId( |
| GroupMembership.CONTENT_ITEM_TYPE); |
| mDb.execSQL( |
| "INSERT INTO " + Tables.DATA + "(" + DataColumns.MIMETYPE_ID + |
| ", " + Data.RAW_CONTACT_ID + ", " |
| + GroupMembership.GROUP_ROW_ID + ") " + |
| "SELECT " + mimeTypeId + ", " |
| + RawContacts._ID + ", " + groupId + |
| " FROM " + Tables.RAW_CONTACTS + |
| " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" + |
| " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL" |
| ); |
| } |
| } |
| mDb.execSQL( |
| "UPDATE " + Tables.RAW_CONTACTS + |
| " SET " + RawContacts.ACCOUNT_NAME + "=?," |
| + RawContacts.ACCOUNT_TYPE + "=?" + |
| " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" + |
| " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", params); |
| |
| // We don't currently support groups for unsynced accounts, so this is for |
| // the future |
| mDb.execSQL( |
| "UPDATE " + Tables.GROUPS + |
| " SET " + Groups.ACCOUNT_NAME + "=?," |
| + Groups.ACCOUNT_TYPE + "=?" + |
| " WHERE " + Groups.ACCOUNT_NAME + " IS NULL" + |
| " AND " + Groups.ACCOUNT_TYPE + " IS NULL", params); |
| |
| mDb.execSQL( |
| "DELETE FROM " + Tables.ACCOUNTS + |
| " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" + |
| " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL"); |
| } |
| } |
| |
| mDbHelper.updateAllVisible(); |
| |
| mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); |
| mDb.setTransactionSuccessful(); |
| } finally { |
| mDb.endTransaction(); |
| } |
| mAccountWritability.clear(); |
| } |
| |
| /** |
| * Finds all distinct accounts present in the specified table. |
| */ |
| private void findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts) { |
| 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)) { |
| hasUnassignedContacts[0] = true; |
| } else { |
| 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) { |
| 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); |
| 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<String> 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) { |
| // TODO: pull this out into a method and generalize to not require contactId |
| long contactId = Long.parseLong(pathSegments.get(3)); |
| SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); |
| setTablesAndProjectionMapForContacts(lookupQb, uri, projection); |
| 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 + "=?"); |
| Cursor c = query(db, lookupQb, projection, selection, args, sortOrder, |
| groupBy, limit); |
| if (c.getCount() != 0) { |
| return c; |
| } |
| |
| c.close(); |
| } |
| |
| setTablesAndProjectionMapForContacts(qb, uri, projection); |
| selectionArgs = insertSelectionArg(selectionArgs, |
| String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); |
| qb.appendWhere(Contacts._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); |
| 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_CONTACED_SORT_COLUMN); |
| frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_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_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_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 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: { |
| setTablesAndProjectionMapForRawContactsEntities(qb, uri); |
| break; |
| } |
| |
| case RAW_CONTACT_ENTITY_ID: { |
| long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); |
| setTablesAndProjectionMapForRawContactsEntities(qb, uri); |
| selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); |
| qb.appendWhere(" AND " + RawContacts._ID + "=?"); |
| break; |
| } |
| |
| case PROVIDER_STATUS: { |
| return queryProviderStatus(uri, projection); |
| } |
| |
| 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; |
| } |
| |
| |
| 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<String, String> 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<LookupKeySegment> 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<LookupKeySegment> 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<LookupKeySegment> 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<LookupKeySegment> 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<LookupKeySegment> 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<LookupKeySegment> 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 " + 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); |
| |
| 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)); |
| if (mDbHelper.isInProjection(projection, |
| Contacts.CONTACT_PRESENCE)) { |
| sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + |
| " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")"); |
| } |
| 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 (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" |
| + ContactsStatusUpdatesColumns.CONCRETE_DATA_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 setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) { |
| // Note: currently, "export only" equals to "restricted", but may not in the future. |
| boolean excludeRestrictedData = readBooleanQueryParameter(uri, |
| Data.FOR_EXPORT_ONLY, false); |
| |
| String requestingPackage = getQueryParameter(uri, |
| ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); |
| if (requestingPackage != null) { |
| excludeRestrictedData = excludeRestrictedData |
| || !mDbHelper.hasAccessToRestrictedData(requestingPackage); |
| } |
| qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData)); |
| qb.setProjectionMap(sRawContactsEntityProjectionMap); |
| appendAccountFromParameter(qb, uri); |
| } |
| |
| private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, |
| String[] projection, boolean distinct) { |
| StringBuilder sb = new StringBuilder(); |
| // Note: currently, "export only" equals to "restricted", but may not in the future. |
| boolean excludeRestrictedData = readBooleanQueryParameter(uri, |
| Data.FOR_EXPORT_ONLY, false); |
| |
| String requestingPackage = getQueryParameter(uri, |
| ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); |
| if (requestingPackage != null) { |
| excludeRestrictedData = excludeRestrictedData |
| || !mDbHelper.hasAccessToRestrictedData(requestingPackage); |
| } |
| |
| sb.append(mDbHelper.getDataView(excludeRestrictedData)); |
| sb.append(" data"); |
| |
| // Include aggregated presence when requested |
| if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) { |
| sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + |
| " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "=" |
| + RawContacts.CONTACT_ID + ")"); |
| } |
| |
| // Include aggregated status updates when requested |
| if (mDbHelper.isInProjection(projection, |
| Data.CONTACT_STATUS, |
| Data.CONTACT_STATUS_RES_PACKAGE, |
| Data.CONTACT_STATUS_ICON, |
| Data.CONTACT_STATUS_LABEL, |
| Data.CONTACT_STATUS_TIMESTAMP)) { |
| sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " |
| + ContactsStatusUpdatesColumns.ALIAS + |
| " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" |
| + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); |
| } |
| |
| // Include individual presence when requested |
| if (mDbHelper.isInProjection(projection, Data.PRESENCE)) { |
| sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + |
| " ON (" + StatusUpdates.DATA_ID + "=" |
| + DataColumns.CONCRETE_ID + ")"); |
| } |
| |
| // Include individual status updates when requested |
| if (mDbHelper.isInProjection(projection, |
| Data.STATUS, |
| Data.STATUS_RES_PACKAGE, |
| Data.STATUS_ICON, |
| Data.STATUS_LABEL, |
| Data.STATUS_TIMESTAMP)) { |
| sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + |
| " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "=" |
| + 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"); |
| |
| if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) { |
| sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + |
| " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID |
| + "=" + DataColumns.CONCRETE_ID + ")"); |
| } |
| |
| 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(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID |
| + "=" + DataColumns.CONCRETE_ID + ")"); |
| } |
| qb.setTables(sb.toString()); |
| qb.setProjectionMap(sStatusUpdatesProjectionMap); |
| } |
| |
| 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 <code>null</code> 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_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 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; |
| |
| default: |
| return mLegacyApiSupport.getType(uri); |
| } |
| } |
| |
| 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()); |
| } |
| } |
| } |