diff options
22 files changed, 442 insertions, 4850 deletions
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index afbec70..2fc1d82 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -105,12 +105,12 @@ import android.provider.ContactsContract.StatusUpdates; import android.provider.ContactsContract.StreamItemPhotos; import android.provider.ContactsContract.StreamItems; import android.provider.OpenableColumns; +import android.provider.Settings.Global; import android.provider.SyncStateContract; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; - import com.android.common.content.ProjectionMap; import com.android.common.content.SyncStateContentProviderHelper; import com.android.common.io.MoreCloseables; @@ -144,8 +144,10 @@ import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.android.providers.contacts.ContactsDatabaseHelper.ViewGroupsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Views; import com.android.providers.contacts.SearchIndexManager.FtsQueryBuilder; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator.AggregationSuggestionParameter; import com.android.providers.contacts.aggregation.ContactAggregator; -import com.android.providers.contacts.aggregation.ContactAggregator.AggregationSuggestionParameter; +import com.android.providers.contacts.aggregation.ContactAggregator2; import com.android.providers.contacts.aggregation.ProfileAggregator; import com.android.providers.contacts.aggregation.util.CommonNicknameCache; import com.android.providers.contacts.database.ContactsTableUtil; @@ -162,7 +164,6 @@ import com.google.android.collect.Maps; import com.google.android.collect.Sets; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; - import libcore.io.IoUtils; import java.io.BufferedWriter; @@ -1344,7 +1345,8 @@ public class ContactsProvider2 extends AbstractContactsProvider // Depending on whether the action being performed is for the profile or not, we will use one of // two aggregator instances. - private final ThreadLocal<ContactAggregator> mAggregator = new ThreadLocal<ContactAggregator>(); + private final ThreadLocal<AbstractContactAggregator> mAggregator = + new ThreadLocal<AbstractContactAggregator>(); // Depending on whether the action being performed is for the profile or not, we will use one of // two photo store instances (with their files stored in separate sub-directories). @@ -1405,7 +1407,7 @@ public class ContactsProvider2 extends AbstractContactsProvider private Account mAccount; - private ContactAggregator mContactAggregator; + private AbstractContactAggregator mContactAggregator; private ContactAggregator mProfileAggregator; // Duration in milliseconds that pre-authorized URIs will remain valid. @@ -1573,8 +1575,15 @@ public class ContactsProvider2 extends AbstractContactsProvider mPostalSplitter = new PostalSplitter(mCurrentLocales.getPrimaryLocale()); mCommonNicknameCache = new CommonNicknameCache(mContactsHelper.getReadableDatabase()); ContactLocaleUtils.setLocales(mCurrentLocales); - mContactAggregator = new ContactAggregator(this, mContactsHelper, - createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache); + + int value = android.provider.Settings.Global.getInt(context.getContentResolver(), + Global.NEW_CONTACT_AGGREGATOR, 0); + mContactAggregator = (value == 0) + ? new ContactAggregator(this, mContactsHelper, + createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache) + : new ContactAggregator2(this, mContactsHelper, + createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache); + mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); mProfileAggregator = new ProfileAggregator(this, mProfileHelper, createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache); @@ -1596,7 +1605,7 @@ public class ContactsProvider2 extends AbstractContactsProvider } private void initDataRowHandlers(Map<String, DataRowHandler> handlerMap, - ContactsDatabaseHelper dbHelper, ContactAggregator contactAggregator, + ContactsDatabaseHelper dbHelper, AbstractContactAggregator contactAggregator, PhotoStore photoStore) { Context context = getContext(); handlerMap.put(Email.CONTENT_ITEM_TYPE, @@ -4391,7 +4400,7 @@ public class ContactsProvider2 extends AbstractContactsProvider int count = db.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1); if (count != 0) { - final ContactAggregator aggregator = mAggregator.get(); + final AbstractContactAggregator aggregator = mAggregator.get(); int aggregationMode = getIntValue( values, RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT); @@ -4646,7 +4655,7 @@ public class ContactsProvider2 extends AbstractContactsProvider db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, exceptionValues); } - final ContactAggregator aggregator = mAggregator.get(); + final AbstractContactAggregator aggregator = mAggregator.get(); aggregator.invalidateAggregationExceptionCache(); aggregator.markForAggregation(rawContactId1, RawContacts.AGGREGATION_MODE_DEFAULT, true); aggregator.markForAggregation(rawContactId2, RawContacts.AGGREGATION_MODE_DEFAULT, true); diff --git a/src/com/android/providers/contacts/DataRowHandler.java b/src/com/android/providers/contacts/DataRowHandler.java index 8e3c20f..dbe8cbc 100644 --- a/src/com/android/providers/contacts/DataRowHandler.java +++ b/src/com/android/providers/contacts/DataRowHandler.java @@ -26,12 +26,11 @@ import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.Data; import android.text.TextUtils; - import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handles inserts and update for a specific Data type. @@ -74,14 +73,14 @@ public abstract class DataRowHandler { protected final Context mContext; protected final ContactsDatabaseHelper mDbHelper; - protected final ContactAggregator mContactAggregator; + protected final AbstractContactAggregator mContactAggregator; protected String[] mSelectionArgs1 = new String[1]; protected final String mMimetype; protected long mMimetypeId; @SuppressWarnings("all") public DataRowHandler(Context context, ContactsDatabaseHelper dbHelper, - ContactAggregator aggregator, String mimetype) { + AbstractContactAggregator aggregator, String mimetype) { mContext = context; mDbHelper = dbHelper; mContactAggregator = aggregator; diff --git a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java index 0bb17c2..063fcdb 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java +++ b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java @@ -21,8 +21,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.provider.ContactsContract.CommonDataKinds.BaseTypes; import android.text.TextUtils; - -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Superclass for data row handlers that deal with types (e.g. Home, Work, Other) and @@ -34,7 +33,8 @@ public class DataRowHandlerForCommonDataKind extends DataRowHandler { private final String mLabelColumn; public DataRowHandlerForCommonDataKind(Context context, ContactsDatabaseHelper dbHelper, - ContactAggregator aggregator, String mimetype, String typeColumn, String labelColumn) { + AbstractContactAggregator aggregator, String mimetype, String typeColumn, + String labelColumn) { super(context, dbHelper, aggregator, mimetype); mTypeColumn = typeColumn; mLabelColumn = labelColumn; diff --git a/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java b/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java index 502b835..1de0823 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java +++ b/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java @@ -16,13 +16,12 @@ package com.android.providers.contacts; import android.content.Context; - -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; public class DataRowHandlerForCustomMimetype extends DataRowHandler { - public DataRowHandlerForCustomMimetype(Context context, - ContactsDatabaseHelper dbHelper, ContactAggregator aggregator, String mimetype) { + public DataRowHandlerForCustomMimetype(Context context, ContactsDatabaseHelper dbHelper, + AbstractContactAggregator aggregator, String mimetype) { super(context, dbHelper, aggregator, mimetype); } } diff --git a/src/com/android/providers/contacts/DataRowHandlerForEmail.java b/src/com/android/providers/contacts/DataRowHandlerForEmail.java index 38cb2e1..539c959 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForEmail.java +++ b/src/com/android/providers/contacts/DataRowHandlerForEmail.java @@ -20,9 +20,8 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.provider.ContactsContract.CommonDataKinds.Email; - import com.android.providers.contacts.SearchIndexManager.IndexBuilder; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handler for email address data rows. @@ -30,7 +29,7 @@ import com.android.providers.contacts.aggregation.ContactAggregator; public class DataRowHandlerForEmail extends DataRowHandlerForCommonDataKind { public DataRowHandlerForEmail( - Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) { + Context context, ContactsDatabaseHelper dbHelper, AbstractContactAggregator aggregator) { super(context, dbHelper, aggregator, Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL); } diff --git a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java index 0d2427a..e291986 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java +++ b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java @@ -23,7 +23,6 @@ import android.database.sqlite.SQLiteDatabase; import android.provider.ContactsContract.CommonDataKinds.GroupMembership; import android.provider.ContactsContract.Groups; import android.provider.ContactsContract.RawContacts; - import com.android.providers.contacts.ContactsDatabaseHelper.Clauses; import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns; @@ -31,7 +30,7 @@ import com.android.providers.contacts.ContactsDatabaseHelper.Projections; import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.android.providers.contacts.ContactsProvider2.GroupIdCacheEntry; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; import java.util.ArrayList; import java.util.HashMap; @@ -66,7 +65,7 @@ public class DataRowHandlerForGroupMembership extends DataRowHandler { private final HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache; public DataRowHandlerForGroupMembership(Context context, ContactsDatabaseHelper dbHelper, - ContactAggregator aggregator, + AbstractContactAggregator aggregator, HashMap<String, ArrayList<GroupIdCacheEntry>> groupIdCache) { super(context, dbHelper, aggregator, GroupMembership.CONTENT_ITEM_TYPE); mGroupIdCache = groupIdCache; diff --git a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java index 48ce5e4..32e9757 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java +++ b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java @@ -20,15 +20,14 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.provider.ContactsContract.CommonDataKinds.Identity; - -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handler for Identity data rows. */ public class DataRowHandlerForIdentity extends DataRowHandler { - public DataRowHandlerForIdentity( - Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) { + public DataRowHandlerForIdentity(Context context, ContactsDatabaseHelper dbHelper, + AbstractContactAggregator aggregator) { super(context, dbHelper, aggregator, Identity.CONTENT_ITEM_TYPE); } diff --git a/src/com/android/providers/contacts/DataRowHandlerForIm.java b/src/com/android/providers/contacts/DataRowHandlerForIm.java index faf10ad..9a2a56e 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForIm.java +++ b/src/com/android/providers/contacts/DataRowHandlerForIm.java @@ -18,17 +18,16 @@ package com.android.providers.contacts; import android.content.ContentValues; import android.content.Context; import android.provider.ContactsContract.CommonDataKinds.Im; - import com.android.providers.contacts.SearchIndexManager.IndexBuilder; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handler for IM address data rows. */ public class DataRowHandlerForIm extends DataRowHandlerForCommonDataKind { - public DataRowHandlerForIm( - Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) { + public DataRowHandlerForIm(Context context, ContactsDatabaseHelper dbHelper, + AbstractContactAggregator aggregator) { super(context, dbHelper, aggregator, Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL); } diff --git a/src/com/android/providers/contacts/DataRowHandlerForNickname.java b/src/com/android/providers/contacts/DataRowHandlerForNickname.java index 9c32df9..03b96a3 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForNickname.java +++ b/src/com/android/providers/contacts/DataRowHandlerForNickname.java @@ -21,17 +21,16 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.provider.ContactsContract.CommonDataKinds.Nickname; import android.text.TextUtils; - import com.android.providers.contacts.SearchIndexManager.IndexBuilder; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handler for nickname data rows. */ public class DataRowHandlerForNickname extends DataRowHandlerForCommonDataKind { - public DataRowHandlerForNickname( - Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) { + public DataRowHandlerForNickname(Context context, ContactsDatabaseHelper dbHelper, + AbstractContactAggregator aggregator) { super(context, dbHelper, aggregator, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL); } diff --git a/src/com/android/providers/contacts/DataRowHandlerForNote.java b/src/com/android/providers/contacts/DataRowHandlerForNote.java index ea73637..fc602f1 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForNote.java +++ b/src/com/android/providers/contacts/DataRowHandlerForNote.java @@ -18,17 +18,16 @@ package com.android.providers.contacts; import android.content.ContentValues; import android.content.Context; import android.provider.ContactsContract.CommonDataKinds.Note; - import com.android.providers.contacts.SearchIndexManager.IndexBuilder; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handler for note data rows. */ public class DataRowHandlerForNote extends DataRowHandler { - public DataRowHandlerForNote( - Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) { + public DataRowHandlerForNote(Context context, ContactsDatabaseHelper dbHelper, + AbstractContactAggregator aggregator) { super(context, dbHelper, aggregator, Note.CONTENT_ITEM_TYPE); } diff --git a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java index 629d949..66a3b1b 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java +++ b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java @@ -22,10 +22,9 @@ import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.Data; - import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.android.providers.contacts.SearchIndexManager.IndexBuilder; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handler for organization data rows. @@ -33,7 +32,7 @@ import com.android.providers.contacts.aggregation.ContactAggregator; public class DataRowHandlerForOrganization extends DataRowHandlerForCommonDataKind { public DataRowHandlerForOrganization(Context context, ContactsDatabaseHelper dbHelper, - ContactAggregator aggregator) { + AbstractContactAggregator aggregator) { super(context, dbHelper, aggregator, Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL); } diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java index 16faf2a..052252e 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java +++ b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java @@ -26,7 +26,7 @@ import android.text.TextUtils; import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.android.providers.contacts.SearchIndexManager.IndexBuilder; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handler for phone number data rows. @@ -34,7 +34,7 @@ import com.android.providers.contacts.aggregation.ContactAggregator; public class DataRowHandlerForPhoneNumber extends DataRowHandlerForCommonDataKind { public DataRowHandlerForPhoneNumber(Context context, - ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) { + ContactsDatabaseHelper dbHelper, AbstractContactAggregator aggregator) { super(context, dbHelper, aggregator, Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL); } diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java index bfaa501..532a852 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java +++ b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java @@ -22,7 +22,7 @@ import android.database.sqlite.SQLiteDatabase; import android.provider.ContactsContract.CommonDataKinds.Photo; import android.util.Log; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; import java.io.IOException; @@ -46,7 +46,7 @@ public class DataRowHandlerForPhoto extends DataRowHandler { /* package */ static final String SKIP_PROCESSING_KEY = "skip_processing"; public DataRowHandlerForPhoto( - Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator, + Context context, ContactsDatabaseHelper dbHelper, AbstractContactAggregator aggregator, PhotoStore photoStore, int maxDisplayPhotoDim, int maxThumbnailPhotoDim) { super(context, dbHelper, aggregator, Photo.CONTENT_ITEM_TYPE); mPhotoStore = photoStore; diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java index ba6777d..044e972 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java +++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java @@ -25,7 +25,7 @@ import android.provider.ContactsContract.PhoneticNameStyle; import android.text.TextUtils; import com.android.providers.contacts.SearchIndexManager.IndexBuilder; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handler for email address data rows. @@ -36,7 +36,7 @@ public class DataRowHandlerForStructuredName extends DataRowHandler { private final StringBuilder mSb = new StringBuilder(); public DataRowHandlerForStructuredName(Context context, ContactsDatabaseHelper dbHelper, - ContactAggregator aggregator, NameSplitter splitter, + AbstractContactAggregator aggregator, NameSplitter splitter, NameLookupBuilder nameLookupBuilder) { super(context, dbHelper, aggregator, StructuredName.CONTENT_ITEM_TYPE); mSplitter = splitter; diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java index 26483ed..7fc97b7 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java +++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java @@ -23,7 +23,7 @@ import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.text.TextUtils; import com.android.providers.contacts.SearchIndexManager.IndexBuilder; -import com.android.providers.contacts.aggregation.ContactAggregator; +import com.android.providers.contacts.aggregation.AbstractContactAggregator; /** * Handler for postal address data rows. @@ -46,7 +46,7 @@ public class DataRowHandlerForStructuredPostal extends DataRowHandler { private final PostalSplitter mSplitter; public DataRowHandlerForStructuredPostal(Context context, ContactsDatabaseHelper dbHelper, - ContactAggregator aggregator, PostalSplitter splitter) { + AbstractContactAggregator aggregator, PostalSplitter splitter) { super(context, dbHelper, aggregator, StructuredPostal.CONTENT_ITEM_TYPE); mSplitter = splitter; } diff --git a/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java index 2eeea25..6948f23 100644 --- a/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java +++ b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java @@ -23,12 +23,12 @@ import android.database.sqlite.SQLiteQueryBuilder; import android.database.sqlite.SQLiteStatement; import android.net.Uri; import android.provider.ContactsContract.AggregationExceptions; +import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Identity; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Photo; import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Contacts.AggregationSuggestions; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.DisplayNameSources; import android.provider.ContactsContract.FullNameStyle; @@ -39,7 +39,6 @@ import android.provider.ContactsContract.StatusUpdates; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; - import com.android.internal.annotations.VisibleForTesting; import com.android.providers.contacts.ContactLookupKey; import com.android.providers.contacts.ContactsDatabaseHelper; @@ -65,14 +64,11 @@ import com.android.providers.contacts.TransactionContext; import com.android.providers.contacts.aggregation.util.CommonNicknameCache; import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper; import com.android.providers.contacts.aggregation.util.ContactMatcher; -import com.android.providers.contacts.aggregation.util.ContactMatcher.MatchScore; -import com.android.providers.contacts.database.ContactsTableUtil; +import com.android.providers.contacts.aggregation.util.MatchScore; import com.android.providers.contacts.util.Clock; - import com.google.android.collect.Maps; -import com.google.android.collect.Sets; -import com.google.common.collect.Multimap; import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; import java.util.ArrayList; import java.util.Collections; @@ -84,18 +80,16 @@ import java.util.Locale; import java.util.Set; /** - * ContactAggregator deals with aggregating contact information coming from different sources. - * Two John Doe contacts from two disjoint sources are presumed to be the same - * person unless the user declares otherwise. + * Base class of contact aggregator and profile aggregator */ -public class AbstractContactAggregator { +public abstract class AbstractContactAggregator { - private static final String TAG = "ContactAggregator"; + protected static final String TAG = "ContactAggregator"; - private static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG); - private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); + protected static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG); + protected static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); - private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL = + protected static final String STRUCTURED_NAME_BASED_LOOKUP_SQL = NameLookupColumns.NAME_TYPE + " IN (" + NameLookupType.NAME_EXACT + "," + NameLookupType.NAME_VARIANT + "," @@ -106,7 +100,7 @@ public class AbstractContactAggregator { * SQL statement that sets the {@link ContactsColumns#LAST_STATUS_UPDATE_ID} column * on the contact to point to the latest social status update. */ - private static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL = + protected static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL = "UPDATE " + Tables.CONTACTS + " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" + "(SELECT " + DataColumns.CONCRETE_ID + @@ -129,13 +123,13 @@ public class AbstractContactAggregator { public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747; // If we encounter more than this many contacts with matching names, aggregate only this many - private static final int PRIMARY_HIT_LIMIT = 15; - private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT); + protected static final int PRIMARY_HIT_LIMIT = 15; + protected static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT); // If we encounter more than this many contacts with matching phone number or email, // don't attempt to aggregate - this is likely an error or a shared corporate data element. - private static final int SECONDARY_HIT_LIMIT = 20; - private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT); + protected static final int SECONDARY_HIT_LIMIT = 20; + protected static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT); // If we encounter no less than this many raw contacts in the best matching contact during // aggregation, don't attempt to aggregate - this is likely an error or a shared corporate @@ -145,54 +139,50 @@ public class AbstractContactAggregator { // If we encounter more than this many contacts with matching name during aggregation // suggestion lookup, ignore the remaining results. - private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100; - - // Return code for the canJoinIntoContact method. - private static final int JOIN = 1; - private static final int KEEP_SEPARATE = 0; - private static final int RE_AGGREGATE = -1; - - private final ContactsProvider2 mContactsProvider; - private final ContactsDatabaseHelper mDbHelper; - private PhotoPriorityResolver mPhotoPriorityResolver; - private final NameSplitter mNameSplitter; - private final CommonNicknameCache mCommonNicknameCache; - - private boolean mEnabled = true; - - /** Precompiled sql statement for setting an aggregated presence */ - private SQLiteStatement mAggregatedPresenceReplace; - private SQLiteStatement mPresenceContactIdUpdate; - private SQLiteStatement mRawContactCountQuery; - private SQLiteStatement mAggregatedPresenceDelete; - private SQLiteStatement mMarkForAggregation; - private SQLiteStatement mPhotoIdUpdate; - private SQLiteStatement mDisplayNameUpdate; - private SQLiteStatement mLookupKeyUpdate; - private SQLiteStatement mStarredUpdate; - private SQLiteStatement mPinnedUpdate; - private SQLiteStatement mContactIdAndMarkAggregatedUpdate; - private SQLiteStatement mContactIdUpdate; - private SQLiteStatement mMarkAggregatedUpdate; - private SQLiteStatement mContactUpdate; - private SQLiteStatement mContactInsert; - private SQLiteStatement mResetPinnedForRawContact; - - private HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap(); - - private String[] mSelectionArgs1 = new String[1]; - private String[] mSelectionArgs2 = new String[2]; - - private long mMimeTypeIdIdentity; - private long mMimeTypeIdEmail; - private long mMimeTypeIdPhoto; - private long mMimeTypeIdPhone; - private String mRawContactsQueryByRawContactId; - private String mRawContactsQueryByContactId; - private StringBuilder mSb = new StringBuilder(); - private MatchCandidateList mCandidates = new MatchCandidateList(); - private ContactMatcher mMatcher = new ContactMatcher(); - private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate(); + protected static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100; + + protected final ContactsProvider2 mContactsProvider; + protected final ContactsDatabaseHelper mDbHelper; + protected PhotoPriorityResolver mPhotoPriorityResolver; + protected final NameSplitter mNameSplitter; + protected final CommonNicknameCache mCommonNicknameCache; + + protected boolean mEnabled = true; + + /** + * Precompiled sql statement for setting an aggregated presence + */ + protected SQLiteStatement mRawContactCountQuery; + protected SQLiteStatement mAggregatedPresenceDelete; + protected SQLiteStatement mAggregatedPresenceReplace; + protected SQLiteStatement mPresenceContactIdUpdate; + protected SQLiteStatement mMarkForAggregation; + protected SQLiteStatement mPhotoIdUpdate; + protected SQLiteStatement mDisplayNameUpdate; + protected SQLiteStatement mLookupKeyUpdate; + protected SQLiteStatement mStarredUpdate; + protected SQLiteStatement mPinnedUpdate; + protected SQLiteStatement mContactIdAndMarkAggregatedUpdate; + protected SQLiteStatement mContactIdUpdate; + protected SQLiteStatement mMarkAggregatedUpdate; + protected SQLiteStatement mContactUpdate; + protected SQLiteStatement mContactInsert; + protected SQLiteStatement mResetPinnedForRawContact; + + protected HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap(); + + protected String[] mSelectionArgs1 = new String[1]; + protected String[] mSelectionArgs2 = new String[2]; + + protected long mMimeTypeIdIdentity; + protected long mMimeTypeIdEmail; + protected long mMimeTypeIdPhoto; + protected long mMimeTypeIdPhone; + protected String mRawContactsQueryByRawContactId; + protected String mRawContactsQueryByContactId; + protected StringBuilder mSb = new StringBuilder(); + protected MatchCandidateList mCandidates = new MatchCandidateList(); + protected DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate(); /** * Parameter for the suggestion lookup query. @@ -212,7 +202,7 @@ public class AbstractContactAggregator { * constructs a bunch of NameMatchCandidate objects for various potential matches * and then executes the search in bulk. */ - private static class NameMatchCandidate { + protected static class NameMatchCandidate { String mName; int mLookupType; @@ -226,9 +216,9 @@ public class AbstractContactAggregator { * A list of {@link NameMatchCandidate} that keeps its elements even when the list is * truncated. This is done for optimization purposes to avoid excessive object allocation. */ - private static class MatchCandidateList { - private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>(); - private int mCount; + protected static class MatchCandidateList { + protected final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>(); + protected int mCount; /** * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists. @@ -412,7 +402,7 @@ public class AbstractContactAggregator { return mEnabled; } - private interface AggregationQuery { + protected interface AggregationQuery { String SQL = "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID + ", " + RawContactsColumns.ACCOUNT_ID + @@ -488,7 +478,7 @@ public class AbstractContactAggregator { for (int i = 0; i < actualCount; i++) { aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i], - mCandidates, mMatcher); + mCandidates); } long elapsedTime = System.currentTimeMillis() - start; @@ -565,7 +555,7 @@ public class AbstractContactAggregator { private static class RawContactIdAndAggregationModeQuery { public static final String TABLE = Tables.RAW_CONTACTS; - public static final String[] COLUMNS = { RawContacts._ID, RawContacts.AGGREGATION_MODE }; + public static final String[] COLUMNS = {RawContacts._ID, RawContacts.AGGREGATION_MODE}; public static final String SELECTION = RawContacts.CONTACT_ID + "=?"; @@ -576,7 +566,7 @@ public class AbstractContactAggregator { /** * Marks all constituent raw contacts of an aggregated contact for re-aggregation. */ - private void markContactForAggregation(SQLiteDatabase db, long contactId) { + protected void markContactForAggregation(SQLiteDatabase db, long contactId) { mSelectionArgs1[0] = String.valueOf(contactId); Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE, RawContactIdAndAggregationModeQuery.COLUMNS, @@ -676,7 +666,6 @@ public class AbstractContactAggregator { } MatchCandidateList candidates = new MatchCandidateList(); - ContactMatcher matcher = new ContactMatcher(); long contactId = 0; long accountId = 0; @@ -694,7 +683,7 @@ public class AbstractContactAggregator { } aggregateContact(txContext, db, rawContactId, accountId, contactId, - candidates, matcher); + candidates); } public void updateAggregateData(TransactionContext txContext, long contactId) { @@ -711,7 +700,7 @@ public class AbstractContactAggregator { updateAggregatedStatusUpdate(contactId); } - private void updateAggregatedStatusUpdate(long contactId) { + protected void updateAggregatedStatusUpdate(long contactId) { mAggregatedPresenceReplace.bindLong(1, contactId); mAggregatedPresenceReplace.bindLong(2, contactId); mAggregatedPresenceReplace.execute(); @@ -731,302 +720,21 @@ public class AbstractContactAggregator { * Given a specific raw contact, finds all matching aggregate contacts and chooses the one * with the highest match score. If no such contact is found, creates a new contact. */ - private synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db, - long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates, - ContactMatcher matcher) { - - if (VERBOSE_LOGGING) { - Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId); - } - - int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; - - Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId); - if (aggModeObject != null) { - aggregationMode = aggModeObject; - } - - long contactId = -1; // Best matching contact ID. - boolean needReaggregate = false; - - final Set<Long> rawContactIdsInSameAccount = new HashSet<Long>(); - final Set<Long> rawContactIdsInOtherAccount = new HashSet<Long>(); - if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { - candidates.clear(); - matcher.clear(); - - contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher); - if (contactId == -1) { - - // If this is a newly inserted contact or a visible contact, look for - // data matches. - if (currentContactId == 0 - || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) { - contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher); - } - - // If we found an best matched contact, find out if the raw contact can be joined - // into it - if (contactId != -1 && contactId != currentContactId) { - // List all raw contact ID and their account ID mappings in contact - // [contactId] excluding raw_contact [rawContactId]. - - // Based on the mapping, create two sets of raw contact IDs in - // [rawContactAccountId] and not in [rawContactAccountId]. We don't always - // need them, so lazily initialize them. - mSelectionArgs2[0] = String.valueOf(contactId); - mSelectionArgs2[1] = String.valueOf(rawContactId); - final Cursor rawContactsToAccountsCursor = db.rawQuery( - "SELECT " + RawContacts._ID + ", " + RawContactsColumns.ACCOUNT_ID + - " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContacts.CONTACT_ID + "=?" + - " AND " + RawContacts._ID + "!=?", - mSelectionArgs2); - try { - rawContactsToAccountsCursor.moveToPosition(-1); - while (rawContactsToAccountsCursor.moveToNext()) { - final long rcId = rawContactsToAccountsCursor.getLong(0); - final long rc_accountId = rawContactsToAccountsCursor.getLong(1); - if (rc_accountId == accountId) { - rawContactIdsInSameAccount.add(rcId); - } else { - rawContactIdsInOtherAccount.add(rcId); - } - } - } finally { - rawContactsToAccountsCursor.close(); - } - final int actionCode; - final int totalNumOfRawContactsInCandidate = rawContactIdsInSameAccount.size() - + rawContactIdsInOtherAccount.size(); - if (totalNumOfRawContactsInCandidate >= AGGREGATION_CONTACT_SIZE_LIMIT) { - if (VERBOSE_LOGGING) { - Log.v(TAG, "Too many raw contacts (" + totalNumOfRawContactsInCandidate - + ") in the best matching contact, so skip aggregation"); - } - actionCode = KEEP_SEPARATE; - } else { - actionCode = canJoinIntoContact(db, rawContactId, - rawContactIdsInSameAccount, rawContactIdsInOtherAccount); - } - if (actionCode == KEEP_SEPARATE) { - contactId = -1; - } else if (actionCode == RE_AGGREGATE) { - needReaggregate = true; - } - } - } - } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) { - return; - } - - // # of raw_contacts in the [currentContactId] contact excluding the [rawContactId] - // raw_contact. - long currentContactContentsCount = 0; - - if (currentContactId != 0) { - mRawContactCountQuery.bindLong(1, currentContactId); - mRawContactCountQuery.bindLong(2, rawContactId); - currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong(); - } - - // If there are no other raw contacts in the current aggregate, we might as well reuse it. - // Also, if the aggregation mode is SUSPENDED, we must reuse the same aggregate. - if (contactId == -1 - && currentContactId != 0 - && (currentContactContentsCount == 0 - || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) { - contactId = currentContactId; - } - - if (contactId == currentContactId) { - // Aggregation unchanged - markAggregated(rawContactId); - if (VERBOSE_LOGGING) { - Log.v(TAG, "Aggregation unchanged"); - } - } else if (contactId == -1) { - // create new contact for [rawContactId] - createContactForRawContacts(db, txContext, Sets.newHashSet(rawContactId), null); - if (currentContactContentsCount > 0) { - updateAggregateData(txContext, currentContactId); - } - if (VERBOSE_LOGGING) { - Log.v(TAG, "create new contact for rid=" + rawContactId); - } - } else if (needReaggregate) { - // re-aggregate - final Set<Long> allRawContactIdSet = new HashSet<Long>(); - allRawContactIdSet.addAll(rawContactIdsInSameAccount); - allRawContactIdSet.addAll(rawContactIdsInOtherAccount); - // If there is no other raw contacts aggregated with the given raw contact currently, - // we might as well reuse it. - currentContactId = (currentContactId != 0 && currentContactContentsCount == 0) - ? currentContactId : 0; - reAggregateRawContacts(txContext, db, contactId, currentContactId, rawContactId, - allRawContactIdSet); - if (VERBOSE_LOGGING) { - Log.v(TAG, "Re-aggregating rid=" + rawContactId + " and cid=" + contactId); - } - } else { - // Joining with an existing aggregate - if (currentContactContentsCount == 0) { - // Delete a previous aggregate if it only contained this raw contact - ContactsTableUtil.deleteContact(db, currentContactId); - - mAggregatedPresenceDelete.bindLong(1, currentContactId); - mAggregatedPresenceDelete.execute(); - } - - clearSuperPrimarySetting(db, contactId, rawContactId); - setContactIdAndMarkAggregated(rawContactId, contactId); - computeAggregateData(db, contactId, mContactUpdate); - mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId); - mContactUpdate.execute(); - mDbHelper.updateContactVisible(txContext, contactId); - updateAggregatedStatusUpdate(contactId); - // Make sure the raw contact does not contribute to the current contact - if (currentContactId != 0) { - updateAggregateData(txContext, currentContactId); - } - if (VERBOSE_LOGGING) { - Log.v(TAG, "Join rid=" + rawContactId + " with cid=" + contactId); - } - } - } - - /** - * Find out which mime-types are shared by raw contact of {@code rawContactId} and raw contacts - * of {@code contactId}. Clear the is_super_primary settings for these mime-types. - */ - private void clearSuperPrimarySetting(SQLiteDatabase db, long contactId, long rawContactId) { - final String[] args = {String.valueOf(contactId), String.valueOf(rawContactId)}; - - // Find out which mime-types exist with is_super_primary=true on both the raw contact of - // rawContactId and raw contacts of contactId - int index = 0; - final StringBuilder mimeTypeCondition = new StringBuilder(); - mimeTypeCondition.append(" AND " + DataColumns.MIMETYPE_ID + " IN ("); - - final Cursor c = db.rawQuery( - "SELECT DISTINCT(a." + DataColumns.MIMETYPE_ID + ")" + - " FROM (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE " + - Data.IS_SUPER_PRIMARY + " =1 AND " + - Data.RAW_CONTACT_ID + " IN (SELECT " + RawContacts._ID + " FROM " + - Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=?1)) AS a" + - " JOIN (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE " + - Data.IS_SUPER_PRIMARY + " =1 AND " + - Data.RAW_CONTACT_ID + "=?2) AS b" + - " ON a." + DataColumns.MIMETYPE_ID + "=b." + DataColumns.MIMETYPE_ID, - args); - try { - c.moveToPosition(-1); - while (c.moveToNext()) { - if (index > 0) { - mimeTypeCondition.append(','); - } - mimeTypeCondition.append(c.getLong((0))); - index++; - } - } finally { - c.close(); - } - - if (index == 0) { - return; - } - - // Clear is_super_primary setting for all the mime-types with is_super_primary=true - // in both raw contact of rawContactId and raw contacts of contactId - String superPrimaryUpdateSql = "UPDATE " + Tables.DATA + - " SET " + Data.IS_SUPER_PRIMARY + "=0" + - " WHERE (" + Data.RAW_CONTACT_ID + - " IN (SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContacts.CONTACT_ID + "=?1)" + - " OR " + Data.RAW_CONTACT_ID + "=?2)"; - - mimeTypeCondition.append(')'); - superPrimaryUpdateSql += mimeTypeCondition.toString(); - db.execSQL(superPrimaryUpdateSql, args); - } - - /** - * @return JOIN if the raw contact of {@code rawContactId} can be joined into the existing - * contact of {@code contactId}. KEEP_SEPARATE if the raw contact of {@code rawContactId} - * cannot be joined into the existing contact of {@code contactId}. RE_AGGREGATE if raw contact - * of {@code rawContactId} and all the raw contacts of contact of {@code contactId} need to be - * re-aggregated. - * - * If contact of {@code contactId} doesn't contain any raw contacts from the same account as - * raw contact of {@code rawContactId}, join raw contact with contact if there is no identity - * mismatch between them on the same namespace, otherwise, keep them separate. - * - * If contact of {@code contactId} contains raw contacts from the same account as raw contact of - * {@code rawContactId}, join raw contact with contact if there's at least one raw contact in - * those raw contacts that shares at least one email address, phone number, or identity; - * otherwise, re-aggregate raw contact and all the raw contacts of contact. - */ - private int canJoinIntoContact(SQLiteDatabase db, long rawContactId, - Set<Long> rawContactIdsInSameAccount, Set<Long> rawContactIdsInOtherAccount ) { - - if (rawContactIdsInSameAccount.isEmpty()) { - final String rid = String.valueOf(rawContactId); - final String ridsInOtherAccts = TextUtils.join(",", rawContactIdsInOtherAccount); - // If there is no identity match between raw contact of [rawContactId] and - // any raw contact in other accounts on the same namespace, and there is at least - // one identity mismatch exist, keep raw contact separate from contact. - if (DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts, - /* isIdentityMatching =*/ true, /* countOnly =*/ true), null) == 0 && - DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts, - /* isIdentityMatching =*/ false, /* countOnly =*/ true), null) > 0) { - if (VERBOSE_LOGGING) { - Log.v(TAG, "canJoinIntoContact: no duplicates, but has no matching identity " + - "and has mis-matching identity on the same namespace between rid=" + - rid + " and ridsInOtherAccts=" + ridsInOtherAccts); - } - return KEEP_SEPARATE; // has identity and identity doesn't match - } else { - if (VERBOSE_LOGGING) { - Log.v(TAG, "canJoinIntoContact: can join the first raw contact from the same " + - "account without any identity mismatch."); - } - return JOIN; // no identity or identity match - } - } - if (VERBOSE_LOGGING) { - Log.v(TAG, "canJoinIntoContact: " + rawContactIdsInSameAccount.size() + - " duplicate(s) found"); - } + abstract void aggregateContact(TransactionContext txContext, SQLiteDatabase db, + long rawContactId, long accountId, long currentContactId, + MatchCandidateList candidates); - final Set<Long> rawContactIdSet = new HashSet<Long>(); - rawContactIdSet.add(rawContactId); - if (rawContactIdsInSameAccount.size() > 0 && - isDataMaching(db, rawContactIdSet, rawContactIdsInSameAccount)) { - if (VERBOSE_LOGGING) { - Log.v(TAG, "canJoinIntoContact: join if there is a data matching found in the " + - "same account"); - } - return JOIN; - } else { - if (VERBOSE_LOGGING) { - Log.v(TAG, "canJoinIntoContact: re-aggregate rid=" + rawContactId + - " with its best matching contact to connected component"); - } - return RE_AGGREGATE; - } - } - - private interface RawContactMatchingSelectionStatement { - String SELECT_COUNT = "SELECT count(*) " ; - String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2." + Data.RAW_CONTACT_ID ; + protected interface RawContactMatchingSelectionStatement { + String SELECT_COUNT = "SELECT count(*) "; + String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2." + Data.RAW_CONTACT_ID; } /** * Build sql to check if there is any identity match/mis-match between two sets of raw contact * ids on the same namespace. */ - private String buildIdentityMatchingSql(String rawContactIdSet1, String rawContactIdSet2, + protected String buildIdentityMatchingSql(String rawContactIdSet1, String rawContactIdSet2, boolean isIdentityMatching, boolean countOnly) { final String identityType = String.valueOf(mMimeTypeIdIdentity); final String matchingOperator = (isIdentityMatching) ? "=" : "!="; @@ -1044,7 +752,7 @@ public class AbstractContactAggregator { RawContactMatchingSelectionStatement.SELECT_ID + sql; } - private String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2, + protected String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2, boolean countOnly) { final String emailType = String.valueOf(mMimeTypeIdEmail); final String sql = @@ -1059,7 +767,7 @@ public class AbstractContactAggregator { RawContactMatchingSelectionStatement.SELECT_ID + sql; } - private String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2, + protected String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2, boolean countOnly) { // It's a bit tricker because it has to be consistent with // updateMatchScoresBasedOnPhoneMatches(). @@ -1083,120 +791,29 @@ public class AbstractContactAggregator { RawContactMatchingSelectionStatement.SELECT_ID + sql; } - private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2) { + protected String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2) { return "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " + AggregationExceptions.RAW_CONTACT_ID2 + " FROM " + Tables.AGGREGATION_EXCEPTIONS + " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" + - rawContactIdSet1 + ")" + + rawContactIdSet1 + ")" + " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" + " AND " + AggregationExceptions.TYPE + "=" + - AggregationExceptions.TYPE_KEEP_TOGETHER ; + AggregationExceptions.TYPE_KEEP_TOGETHER ; } - private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) { + protected boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) { return DatabaseUtils.longForQuery(db, query, null) > 0; } /** - * If there's any identity, email address or a phone number matching between two raw contact - * sets. - */ - private boolean isDataMaching(SQLiteDatabase db, Set<Long> rawContactIdSet1, - Set<Long> rawContactIdSet2) { - final String rawContactIds1 = TextUtils.join(",", rawContactIdSet1); - final String rawContactIds2 = TextUtils.join(",", rawContactIdSet2); - // First, check for the identity - if (isFirstColumnGreaterThanZero(db, buildIdentityMatchingSql( - rawContactIds1, rawContactIds2, /* isIdentityMatching =*/ true, - /* countOnly =*/true))) { - if (VERBOSE_LOGGING) { - Log.v(TAG, "canJoinIntoContact: identity match found between " + rawContactIds1 + - " and " + rawContactIds2); - } - return true; - } - - // Next, check for the email address. - if (isFirstColumnGreaterThanZero(db, - buildEmailMatchingSql(rawContactIds1, rawContactIds2, true))) { - if (VERBOSE_LOGGING) { - Log.v(TAG, "canJoinIntoContact: email match found between " + rawContactIds1 + - " and " + rawContactIds2); - } - return true; - } - - // Lastly, the phone number. - if (isFirstColumnGreaterThanZero(db, - buildPhoneMatchingSql(rawContactIds1, rawContactIds2, true))) { - if (VERBOSE_LOGGING) { - Log.v(TAG, "canJoinIntoContact: phone match found between " + rawContactIds1 + - " and " + rawContactIds2); - } - return true; - } - return false; - } - - /** - * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of - * {@code existingRawContactIds} into connected components. This only happens when a given - * raw contacts cannot be joined with its best matching contacts directly. - * - * Two raw contacts are considered connected if they share at least one email address, phone - * number or identity. Create new contact for each connected component except the very first - * one that doesn't contain rawContactId of {@code rawContactId}. - */ - private void reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db, - long contactId, long currentContactId, long rawContactId, - Set<Long> existingRawContactIds) { - // Find the connected component based on the aggregation exceptions or - // identity/email/phone matching for all the raw contacts of [contactId] and the give - // raw contact. - final Set<Long> allIds = new HashSet<Long>(); - allIds.add(rawContactId); - allIds.addAll(existingRawContactIds); - final Set<Set<Long>> connectedRawContactSets = findConnectedRawContacts(db, allIds); - - if (connectedRawContactSets.size() == 1) { - // If everything is connected, create one contact with [contactId] - createContactForRawContacts(db, txContext, connectedRawContactSets.iterator().next(), - contactId); - } else { - for (Set<Long> connectedRawContactIds : connectedRawContactSets) { - if (connectedRawContactIds.contains(rawContactId)) { - // crate contact for connect component containing [rawContactId], reuse - // [currentContactId] if possible. - createContactForRawContacts(db, txContext, connectedRawContactIds, - currentContactId == 0 ? null : currentContactId); - connectedRawContactSets.remove(connectedRawContactIds); - break; - } - } - // Create new contact for each connected component except the last one. The last one - // will reuse [contactId]. Only the last one can reuse [contactId] when all other raw - // contacts has already been assigned new contact Id, so that the contact aggregation - // stats could be updated correctly. - int index = connectedRawContactSets.size(); - for (Set<Long> connectedRawContactIds : connectedRawContactSets) { - if (index > 1) { - createContactForRawContacts(db, txContext, connectedRawContactIds, null); - index--; - } else { - createContactForRawContacts(db, txContext, connectedRawContactIds, contactId); - } - } - } - } - - /** * Partition the given raw contact Ids to connected component based on aggregation exception, * identity matching, email matching or phone matching. */ - private Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long> rawContactIdSet) { + protected Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long> + rawContactIdSet) { // Connections between two raw contacts - final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create(); + final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create(); String rawContactIds = TextUtils.join(",", rawContactIdSet); findIdPairs(db, buildExceptionMatchingSql(rawContactIds, rawContactIds), matchingRawIdPairs); @@ -1215,7 +832,7 @@ public class AbstractContactAggregator { * method will put two entries into the given result map for each pair of different IDs, one * keyed by each ID. */ - private void findIdPairs(SQLiteDatabase db, String query, Multimap<Long, Long> results) { + protected void findIdPairs(SQLiteDatabase db, String query, Multimap<Long, Long> results) { Cursor cursor = db.rawQuery(query, null); try { cursor.moveToPosition(-1); @@ -1236,7 +853,7 @@ public class AbstractContactAggregator { * Creates a new Contact for a given set of the raw contacts of {@code rawContactIds} if the * given contactId is null. Otherwise, regroup them into contact with {@code contactId}. */ - private void createContactForRawContacts(SQLiteDatabase db, TransactionContext txContext, + protected void createContactForRawContacts(SQLiteDatabase db, TransactionContext txContext, Set<Long> rawContactIds, Long contactId) { if (rawContactIds.isEmpty()) { // No raw contact id is provided. @@ -1245,7 +862,7 @@ public class AbstractContactAggregator { // If contactId is not provided, generates a new one. if (contactId == null) { - mSelectionArgs1[0]= String.valueOf(rawContactIds.iterator().next()); + mSelectionArgs1[0] = String.valueOf(rawContactIds.iterator().next()); computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert); contactId = mContactInsert.executeInsert(); @@ -1259,66 +876,12 @@ public class AbstractContactAggregator { updateAggregateData(txContext, contactId); } - private static class RawContactIdQuery { + protected static class RawContactIdQuery { public static final String TABLE = Tables.RAW_CONTACTS; - public static final String[] COLUMNS = { RawContacts._ID }; + public static final String[] COLUMNS = {RawContacts._ID, RawContactsColumns.ACCOUNT_ID }; public static final String SELECTION = RawContacts.CONTACT_ID + "=?"; public static final int RAW_CONTACT_ID = 0; - } - - /** - * Ensures that automatic aggregation rules are followed after a contact - * becomes visible or invisible. Specifically, consider this case: there are - * three contacts named Foo. Two of them come from account A1 and one comes - * from account A2. The aggregation rules say that in this case none of the - * three Foo's should be aggregated: two of them are in the same account, so - * they don't get aggregated; the third has two affinities, so it does not - * join either of them. - * <p> - * Consider what happens if one of the "Foo"s from account A1 becomes - * invisible. Nothing stands in the way of aggregating the other two - * anymore, so they should get joined. - * <p> - * What if the invisible "Foo" becomes visible after that? We should split the - * aggregate between the other two. - */ - public void updateAggregationAfterVisibilityChange(long contactId) { - SQLiteDatabase db = mDbHelper.getWritableDatabase(); - boolean visible = mDbHelper.isContactInDefaultDirectory(db, contactId); - if (visible) { - markContactForAggregation(db, contactId); - } else { - // Find all contacts that _could be_ aggregated with this one and - // rerun aggregation for all of them - mSelectionArgs1[0] = String.valueOf(contactId); - Cursor cursor = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS, - RawContactIdQuery.SELECTION, mSelectionArgs1, null, null, null); - try { - while (cursor.moveToNext()) { - long rawContactId = cursor.getLong(RawContactIdQuery.RAW_CONTACT_ID); - mMatcher.clear(); - - updateMatchScoresBasedOnIdentityMatch(db, rawContactId, mMatcher); - updateMatchScoresBasedOnNameMatches(db, rawContactId, mMatcher); - List<MatchScore> bestMatches = - mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_PRIMARY); - for (MatchScore matchScore : bestMatches) { - markContactForAggregation(db, matchScore.getContactId()); - } - - mMatcher.clear(); - updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher); - updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher); - bestMatches = - mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SECONDARY); - for (MatchScore matchScore : bestMatches) { - markContactForAggregation(db, matchScore.getContactId()); - } - } - } finally { - cursor.close(); - } - } + public static final int ACCOUNT_ID = 1; } /** @@ -1333,7 +896,7 @@ public class AbstractContactAggregator { /** * Marks the specified raw contact ID as aggregated */ - private void markAggregated(long rawContactId) { + protected void markAggregated(long rawContactId) { mMarkAggregatedUpdate.bindLong(1, rawContactId); mMarkAggregatedUpdate.execute(); } @@ -1362,8 +925,8 @@ public class AbstractContactAggregator { String TABLE = Tables.AGGREGATION_EXCEPTIONS; String[] COLUMNS = { - AggregationExceptions.RAW_CONTACT_ID1, - AggregationExceptions.RAW_CONTACT_ID2, + AggregationExceptions.RAW_CONTACT_ID1, + AggregationExceptions.RAW_CONTACT_ID2, }; int RAW_CONTACT_ID1 = 0; @@ -1371,8 +934,8 @@ public class AbstractContactAggregator { } // A set of raw contact IDs for which there are aggregation exceptions - private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>(); - private boolean mAggregationExceptionIdsValid; + protected final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>(); + protected boolean mAggregationExceptionIdsValid; public void invalidateAggregationExceptionCache() { mAggregationExceptionIdsValid = false; @@ -1384,7 +947,7 @@ public class AbstractContactAggregator { * the agg_exceptions table if it is known that there are no records there for a given * raw contact ID. */ - private void prefetchAggregationExceptionIds(SQLiteDatabase db) { + protected void prefetchAggregationExceptionIds(SQLiteDatabase db) { mAggregationExceptionIds.clear(); final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE, AggregateExceptionPrefetchQuery.COLUMNS, @@ -1404,152 +967,7 @@ public class AbstractContactAggregator { mAggregationExceptionIdsValid = true; } - interface AggregateExceptionQuery { - String TABLE = Tables.AGGREGATION_EXCEPTIONS - + " JOIN raw_contacts raw_contacts1 " - + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) " - + " JOIN raw_contacts raw_contacts2 " - + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) "; - - String[] COLUMNS = { - AggregationExceptions.TYPE, - AggregationExceptions.RAW_CONTACT_ID1, - "raw_contacts1." + RawContacts.CONTACT_ID, - "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED, - "raw_contacts2." + RawContacts.CONTACT_ID, - "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED, - }; - - int TYPE = 0; - int RAW_CONTACT_ID1 = 1; - int CONTACT_ID1 = 2; - int AGGREGATION_NEEDED_1 = 3; - int CONTACT_ID2 = 4; - int AGGREGATION_NEEDED_2 = 5; - } - - /** - * Computes match scores based on exceptions entered by the user: always match and never match. - * Returns the aggregate contact with the always match exception if any. - */ - private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId, - ContactMatcher matcher) { - if (!mAggregationExceptionIdsValid) { - prefetchAggregationExceptionIds(db); - } - - // If there are no aggregation exceptions involving this raw contact, there is no need to - // run a query and we can just return -1, which stands for "nothing found" - if (!mAggregationExceptionIds.contains(rawContactId)) { - return -1; - } - - final Cursor c = db.query(AggregateExceptionQuery.TABLE, - AggregateExceptionQuery.COLUMNS, - AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId - + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId, - null, null, null, null); - - try { - while (c.moveToNext()) { - int type = c.getInt(AggregateExceptionQuery.TYPE); - long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1); - long contactId = -1; - if (rawContactId == rawContactId1) { - if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0 - && !c.isNull(AggregateExceptionQuery.CONTACT_ID2)) { - contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2); - } - } else { - if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0 - && !c.isNull(AggregateExceptionQuery.CONTACT_ID1)) { - contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1); - } - } - if (contactId != -1) { - if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) { - matcher.keepIn(contactId); - } else { - matcher.keepOut(contactId); - } - } - } - } finally { - c.close(); - } - - return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true); - } - - /** - * Picks the best matching contact based on matches between data elements. It considers - * name match to be primary and phone, email etc matches to be secondary. A good primary - * match triggers aggregation, while a good secondary match only triggers aggregation in - * the absence of a strong primary mismatch. - * <p> - * Consider these examples: - * <p> - * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should - * be aggregated (same number, similar names). - * <p> - * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should - * not be aggregated (same number, different names). - */ - private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId, - MatchCandidateList candidates, ContactMatcher matcher) { - - // Find good matches based on name alone - long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, matcher); - if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) { - // We found multiple matches on the name - do not aggregate because of the ambiguity - return -1; - } else if (bestMatch == -1) { - // We haven't found a good match on name, see if we have any matches on phone, email etc - bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher); - if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) { - return -1; - } - } - - return bestMatch; - } - - - /** - * Picks the best matching contact based on secondary data matches. The method loads - * structured names for all candidate contacts and recomputes match scores using approximate - * matching. - */ - private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db, - long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) { - List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates( - ContactMatcher.SCORE_THRESHOLD_PRIMARY); - if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) { - return -1; - } - - loadNameMatchCandidates(db, rawContactId, candidates, true); - - mSb.setLength(0); - mSb.append(RawContacts.CONTACT_ID).append(" IN ("); - for (int i = 0; i < secondaryContactIds.size(); i++) { - if (i != 0) { - mSb.append(','); - } - mSb.append(secondaryContactIds.get(i)); - } - - // We only want to compare structured names to structured names - // at this stage, we need to ignore all other sources of name lookup data. - mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL); - - matchAllCandidates(db, mSb.toString(), candidates, matcher, - ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null); - - return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false); - } - - private interface NameLookupQuery { + protected interface NameLookupQuery { String TABLE = Tables.NAME_LOOKUP; String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?"; @@ -1565,7 +983,7 @@ public class AbstractContactAggregator { int NAME_TYPE = 1; } - private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId, + protected void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, boolean structuredNameBased) { candidates.clear(); mSelectionArgs1[0] = String.valueOf(rawContactId); @@ -1585,26 +1003,7 @@ public class AbstractContactAggregator { } } - /** - * Computes scores for contacts that have matching data rows. - */ - private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId, - ContactMatcher matcher) { - - updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher); - updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher); - long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY, false); - if (bestMatch != -1) { - return bestMatch; - } - - updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher); - updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher); - - return -1; - } - - private interface IdentityLookupMatchQuery { + protected interface IdentityLookupMatchQuery { final String TABLE = Tables.DATA + " dataA" + " JOIN " + Tables.DATA + " dataB" + " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE + @@ -1622,34 +1021,45 @@ public class AbstractContactAggregator { + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; final String[] COLUMNS = new String[] { - RawContacts.CONTACT_ID + RawContacts._ID, RawContacts.CONTACT_ID, RawContactsColumns.ACCOUNT_ID }; - int CONTACT_ID = 0; + int RAW_CONTACT_ID = 0; + int CONTACT_ID = 1; + int ACCOUNT_ID = 2; } - /** - * Finds contacts with exact identity matches to the the specified raw contact. - */ - private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId, - ContactMatcher matcher) { - mSelectionArgs2[0] = String.valueOf(rawContactId); - mSelectionArgs2[1] = String.valueOf(mMimeTypeIdIdentity); - Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS, - IdentityLookupMatchQuery.SELECTION, - mSelectionArgs2, RawContacts.CONTACT_ID, null, null); - try { - while (c.moveToNext()) { - final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID); - matcher.matchIdentity(contactId); - } - } finally { - c.close(); - } + interface AggregateExceptionQuery { + String TABLE = Tables.AGGREGATION_EXCEPTIONS + + " JOIN raw_contacts raw_contacts1 " + + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) " + + " JOIN raw_contacts raw_contacts2 " + + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) "; + String[] COLUMNS = { + AggregationExceptions.TYPE, + AggregationExceptions.RAW_CONTACT_ID1, + "raw_contacts1." + RawContacts.CONTACT_ID, + "raw_contacts1." + RawContactsColumns.ACCOUNT_ID, + "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED, + AggregationExceptions.RAW_CONTACT_ID2, + "raw_contacts2." + RawContacts.CONTACT_ID, + "raw_contacts2." + RawContactsColumns.ACCOUNT_ID, + "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED, + }; + + int TYPE = 0; + int RAW_CONTACT_ID1 = 1; + int CONTACT_ID1 = 2; + int ACCOUNT_ID1 = 3; + int AGGREGATION_NEEDED_1 = 4; + int RAW_CONTACT_ID2 = 5; + int CONTACT_ID2 = 6; + int ACCOUNT_ID2 = 7; + int AGGREGATION_NEEDED_2 = 8; } - private interface NameLookupMatchQuery { + protected interface NameLookupMatchQuery { String TABLE = Tables.NAME_LOOKUP + " nameA" + " JOIN " + Tables.NAME_LOOKUP + " nameB" + " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "=" @@ -1663,63 +1073,44 @@ public class AbstractContactAggregator { + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; String[] COLUMNS = new String[] { - RawContacts.CONTACT_ID, - "nameA." + NameLookupColumns.NORMALIZED_NAME, - "nameA." + NameLookupColumns.NAME_TYPE, - "nameB." + NameLookupColumns.NAME_TYPE, + RawContacts._ID, + RawContacts.CONTACT_ID, + RawContactsColumns.ACCOUNT_ID, + "nameA." + NameLookupColumns.NORMALIZED_NAME, + "nameA." + NameLookupColumns.NAME_TYPE, + "nameB." + NameLookupColumns.NAME_TYPE, }; - int CONTACT_ID = 0; - int NAME = 1; - int NAME_TYPE_A = 2; - int NAME_TYPE_B = 3; - } - - /** - * Finds contacts with names matching the name of the specified raw contact. - */ - private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId, - ContactMatcher matcher) { - mSelectionArgs1[0] = String.valueOf(rawContactId); - Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS, - NameLookupMatchQuery.SELECTION, - mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING); - try { - while (c.moveToNext()) { - long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID); - String name = c.getString(NameLookupMatchQuery.NAME); - int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A); - int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B); - matcher.matchName(contactId, nameTypeA, name, - nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT); - if (nameTypeA == NameLookupType.NICKNAME && - nameTypeB == NameLookupType.NICKNAME) { - matcher.updateScoreWithNicknameMatch(contactId); - } - } - } finally { - c.close(); - } + int RAW_CONTACT_ID = 0; + int CONTACT_ID = 1; + int ACCOUNT_ID = 2; + int NAME = 3; + int NAME_TYPE_A = 4; + int NAME_TYPE_B = 5; } - private interface NameLookupMatchQueryWithParameter { + protected interface NameLookupMatchQueryWithParameter { String TABLE = Tables.NAME_LOOKUP + " JOIN " + Tables.RAW_CONTACTS + " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = " + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; String[] COLUMNS = new String[] { - RawContacts.CONTACT_ID, - NameLookupColumns.NORMALIZED_NAME, - NameLookupColumns.NAME_TYPE, + RawContacts._ID, + RawContacts.CONTACT_ID, + RawContactsColumns.ACCOUNT_ID, + NameLookupColumns.NORMALIZED_NAME, + NameLookupColumns.NAME_TYPE, }; - int CONTACT_ID = 0; - int NAME = 1; - int NAME_TYPE = 2; + int RAW_CONTACT_ID = 0; + int CONTACT_ID = 1; + int ACCOUNT_ID = 2; + int NAME = 3; + int NAME_TYPE = 4; } - private final class NameLookupSelectionBuilder extends NameLookupBuilder { + protected final class NameLookupSelectionBuilder extends NameLookupBuilder { private final MatchCandidateList mNameLookupCandidates; @@ -1768,7 +1159,7 @@ public class AbstractContactAggregator { /** * Finds contacts with names matching the specified name. */ - private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query, + protected void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query, MatchCandidateList candidates, ContactMatcher matcher) { candidates.clear(); NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder( @@ -1798,7 +1189,7 @@ public class AbstractContactAggregator { } } - private interface EmailLookupQuery { + protected interface EmailLookupQuery { String TABLE = Tables.DATA + " dataA" + " JOIN " + Tables.DATA + " dataB" + " ON lower(" + "dataA." + Email.DATA + ")=lower(dataB." + Email.DATA + ")" @@ -1814,30 +1205,17 @@ public class AbstractContactAggregator { + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; String[] COLUMNS = new String[] { - RawContacts.CONTACT_ID + Tables.RAW_CONTACTS + "." + RawContacts._ID, + RawContacts.CONTACT_ID, + RawContactsColumns.ACCOUNT_ID }; - int CONTACT_ID = 0; - } - - private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, - ContactMatcher matcher) { - mSelectionArgs2[0] = String.valueOf(rawContactId); - mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail); - Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS, - EmailLookupQuery.SELECTION, - mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING); - try { - while (c.moveToNext()) { - long contactId = c.getLong(EmailLookupQuery.CONTACT_ID); - matcher.updateScoreWithEmailMatch(contactId); - } - } finally { - c.close(); - } + int RAW_CONTACT_ID = 0; + int CONTACT_ID = 1; + int ACCOUNT_ID = 2; } - private interface PhoneLookupQuery { + protected interface PhoneLookupQuery { String TABLE = Tables.PHONE_LOOKUP + " phoneA" + " JOIN " + Tables.DATA + " dataA" + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")" @@ -1857,61 +1235,20 @@ public class AbstractContactAggregator { + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; String[] COLUMNS = new String[] { - RawContacts.CONTACT_ID + Tables.RAW_CONTACTS + "." + RawContacts._ID, + RawContacts.CONTACT_ID, + RawContactsColumns.ACCOUNT_ID }; - int CONTACT_ID = 0; - } - - private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId, - ContactMatcher matcher) { - mSelectionArgs2[0] = String.valueOf(rawContactId); - mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter(); - Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS, - PhoneLookupQuery.SELECTION, - mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING); - try { - while (c.moveToNext()) { - long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID); - matcher.updateScoreWithPhoneNumberMatch(contactId); - } - } finally { - c.close(); - } - } - - /** - * Loads name lookup rows for approximate name matching and updates match scores based on that - * data. - */ - private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates, - ContactMatcher matcher) { - HashSet<String> firstLetters = new HashSet<String>(); - for (int i = 0; i < candidates.mCount; i++) { - final NameMatchCandidate candidate = candidates.mList.get(i); - if (candidate.mName.length() >= 2) { - String firstLetter = candidate.mName.substring(0, 2); - if (!firstLetters.contains(firstLetter)) { - firstLetters.add(firstLetter); - final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '" - + firstLetter + "*') AND " - + "(" + NameLookupColumns.NAME_TYPE + " IN(" - + NameLookupType.NAME_COLLATION_KEY + "," - + NameLookupType.EMAIL_BASED_NICKNAME + "," - + NameLookupType.NICKNAME + ")) AND " - + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; - matchAllCandidates(db, selection, candidates, matcher, - ContactMatcher.MATCHING_ALGORITHM_APPROXIMATE, - String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT)); - } - } - } + int RAW_CONTACT_ID = 0; + int CONTACT_ID = 1; + int ACCOUNT_ID = 2; } private interface ContactNameLookupQuery { String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; - String[] COLUMNS = new String[] { + String[] COLUMNS = new String[]{ RawContacts.CONTACT_ID, NameLookupColumns.NORMALIZED_NAME, NameLookupColumns.NAME_TYPE @@ -2017,7 +1354,7 @@ public class AbstractContactAggregator { int HAS_SUPER_PRIMARY_NAME = 17; } - private interface ContactReplaceSqlStatement { + protected interface ContactReplaceSqlStatement { String UPDATE_SQL = "UPDATE " + Tables.CONTACTS + " SET " @@ -2070,7 +1407,7 @@ public class AbstractContactAggregator { /** * Computes aggregate-level data for the specified aggregate contact ID. */ - private void computeAggregateData(SQLiteDatabase db, long contactId, + protected void computeAggregateData(SQLiteDatabase db, long contactId, SQLiteStatement statement) { mSelectionArgs1[0] = String.valueOf(contactId); computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement); @@ -2089,7 +1426,7 @@ public class AbstractContactAggregator { /** * Computes aggregate-level data from constituent raw contacts. */ - private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs, + protected void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs, SQLiteStatement statement) { long currentRawContactId = -1; long bestPhotoId = -1; @@ -2748,58 +2085,8 @@ public class AbstractContactAggregator { * descending order of match score. * @param parameters */ - private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId, - ArrayList<AggregationSuggestionParameter> parameters) { - - MatchCandidateList candidates = new MatchCandidateList(); - ContactMatcher matcher = new ContactMatcher(); - - // Don't aggregate a contact with itself - matcher.keepOut(contactId); - - if (parameters == null || parameters.size() == 0) { - final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS, - RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null); - try { - while (c.moveToNext()) { - long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID); - updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates, - matcher); - } - } finally { - c.close(); - } - } else { - updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates, - matcher, parameters); - } - - return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST); - } - - /** - * Computes scores for contacts that have matching data rows. - */ - private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, - long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) { + protected abstract List<MatchScore> findMatchingContacts(final SQLiteDatabase db, + long contactId, ArrayList<AggregationSuggestionParameter> parameters); - updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher); - updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher); - updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher); - updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher); - loadNameMatchCandidates(db, rawContactId, candidates, false); - lookupApproximateNameMatches(db, candidates, matcher); - } - - private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, - MatchCandidateList candidates, ContactMatcher matcher, - ArrayList<AggregationSuggestionParameter> parameters) { - for (AggregationSuggestionParameter parameter : parameters) { - if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) { - updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher); - } - - // TODO: add support for other parameter kinds - } - } + public abstract void updateAggregationAfterVisibilityChange(long contactId); } diff --git a/src/com/android/providers/contacts/aggregation/ContactAggregator.java b/src/com/android/providers/contacts/aggregation/ContactAggregator.java index 253adf0..d2108b6 100644 --- a/src/com/android/providers/contacts/aggregation/ContactAggregator.java +++ b/src/com/android/providers/contacts/aggregation/ContactAggregator.java @@ -19,68 +19,32 @@ package com.android.providers.contacts.aggregation; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.database.sqlite.SQLiteStatement; -import android.net.Uri; import android.provider.ContactsContract.AggregationExceptions; -import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Identity; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.Photo; -import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Contacts.AggregationSuggestions; import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.DisplayNameSources; -import android.provider.ContactsContract.FullNameStyle; -import android.provider.ContactsContract.PhotoFiles; -import android.provider.ContactsContract.PinnedPositions; import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.StatusUpdates; import android.text.TextUtils; -import android.util.EventLog; import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.providers.contacts.ContactLookupKey; import com.android.providers.contacts.ContactsDatabaseHelper; -import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; -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.StatusUpdatesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; -import com.android.providers.contacts.ContactsDatabaseHelper.Views; import com.android.providers.contacts.ContactsProvider2; -import com.android.providers.contacts.NameLookupBuilder; -import com.android.providers.contacts.NameNormalizer; import com.android.providers.contacts.NameSplitter; import com.android.providers.contacts.PhotoPriorityResolver; -import com.android.providers.contacts.ReorderingCursorWrapper; import com.android.providers.contacts.TransactionContext; import com.android.providers.contacts.aggregation.util.CommonNicknameCache; -import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper; import com.android.providers.contacts.aggregation.util.ContactMatcher; -import com.android.providers.contacts.aggregation.util.ContactMatcher.MatchScore; +import com.android.providers.contacts.aggregation.util.MatchScore; import com.android.providers.contacts.database.ContactsTableUtil; -import com.android.providers.contacts.util.Clock; - -import com.google.android.collect.Maps; import com.google.android.collect.Sets; -import com.google.common.collect.Multimap; -import com.google.common.collect.HashMultimap; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.Set; /** @@ -88,194 +52,14 @@ import java.util.Set; * Two John Doe contacts from two disjoint sources are presumed to be the same * person unless the user declares otherwise. */ -public class ContactAggregator { - - private static final String TAG = "ContactAggregator"; - - private static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG); - private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); - - private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL = - NameLookupColumns.NAME_TYPE + " IN (" - + NameLookupType.NAME_EXACT + "," - + NameLookupType.NAME_VARIANT + "," - + NameLookupType.NAME_COLLATION_KEY + ")"; - - - /** - * SQL statement that sets the {@link ContactsColumns#LAST_STATUS_UPDATE_ID} column - * on the contact to point to the latest social status update. - */ - private static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL = - "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 + "=?"; - - // From system/core/logcat/event-log-tags - // aggregator [time, count] will be logged for each aggregator cycle. - // For the query (as opposed to the merge), count will be negative - public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747; - - // If we encounter more than this many contacts with matching names, aggregate only this many - private static final int PRIMARY_HIT_LIMIT = 15; - private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT); - - // If we encounter more than this many contacts with matching phone number or email, - // don't attempt to aggregate - this is likely an error or a shared corporate data element. - private static final int SECONDARY_HIT_LIMIT = 20; - private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT); - - // If we encounter no less than this many raw contacts in the best matching contact during - // aggregation, don't attempt to aggregate - this is likely an error or a shared corporate - // data element. - @VisibleForTesting - static final int AGGREGATION_CONTACT_SIZE_LIMIT = 50; - - // If we encounter more than this many contacts with matching name during aggregation - // suggestion lookup, ignore the remaining results. - private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100; +public class ContactAggregator extends AbstractContactAggregator { // Return code for the canJoinIntoContact method. private static final int JOIN = 1; private static final int KEEP_SEPARATE = 0; private static final int RE_AGGREGATE = -1; - private final ContactsProvider2 mContactsProvider; - private final ContactsDatabaseHelper mDbHelper; - private PhotoPriorityResolver mPhotoPriorityResolver; - private final NameSplitter mNameSplitter; - private final CommonNicknameCache mCommonNicknameCache; - - private boolean mEnabled = true; - - /** Precompiled sql statement for setting an aggregated presence */ - private SQLiteStatement mAggregatedPresenceReplace; - private SQLiteStatement mPresenceContactIdUpdate; - private SQLiteStatement mRawContactCountQuery; - private SQLiteStatement mAggregatedPresenceDelete; - private SQLiteStatement mMarkForAggregation; - private SQLiteStatement mPhotoIdUpdate; - private SQLiteStatement mDisplayNameUpdate; - private SQLiteStatement mLookupKeyUpdate; - private SQLiteStatement mStarredUpdate; - private SQLiteStatement mPinnedUpdate; - private SQLiteStatement mContactIdAndMarkAggregatedUpdate; - private SQLiteStatement mContactIdUpdate; - private SQLiteStatement mMarkAggregatedUpdate; - private SQLiteStatement mContactUpdate; - private SQLiteStatement mContactInsert; - private SQLiteStatement mResetPinnedForRawContact; - - private HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap(); - - private String[] mSelectionArgs1 = new String[1]; - private String[] mSelectionArgs2 = new String[2]; - - private long mMimeTypeIdIdentity; - private long mMimeTypeIdEmail; - private long mMimeTypeIdPhoto; - private long mMimeTypeIdPhone; - private String mRawContactsQueryByRawContactId; - private String mRawContactsQueryByContactId; - private StringBuilder mSb = new StringBuilder(); - private MatchCandidateList mCandidates = new MatchCandidateList(); private ContactMatcher mMatcher = new ContactMatcher(); - private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate(); - - /** - * Parameter for the suggestion lookup query. - */ - public static final class AggregationSuggestionParameter { - public final String kind; - public final String value; - - public AggregationSuggestionParameter(String kind, String value) { - this.kind = kind; - this.value = value; - } - } - - /** - * Captures a potential match for a given name. The matching algorithm - * constructs a bunch of NameMatchCandidate objects for various potential matches - * and then executes the search in bulk. - */ - private static class NameMatchCandidate { - String mName; - int mLookupType; - - public NameMatchCandidate(String name, int nameLookupType) { - mName = name; - mLookupType = nameLookupType; - } - } - - /** - * A list of {@link NameMatchCandidate} that keeps its elements even when the list is - * truncated. This is done for optimization purposes to avoid excessive object allocation. - */ - private static class MatchCandidateList { - private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>(); - private int mCount; - - /** - * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists. - */ - public void add(String name, int nameLookupType) { - if (mCount >= mList.size()) { - mList.add(new NameMatchCandidate(name, nameLookupType)); - } else { - NameMatchCandidate candidate = mList.get(mCount); - candidate.mName = name; - candidate.mLookupType = nameLookupType; - } - mCount++; - } - - public void clear() { - mCount = 0; - } - - public boolean isEmpty() { - return mCount == 0; - } - } - - /** - * A convenience class used in the algorithm that figures out which of available - * display names to use for an aggregate contact. - */ - private static class DisplayNameCandidate { - long rawContactId; - String displayName; - int displayNameSource; - boolean isNameSuperPrimary; - boolean writableAccount; - - public DisplayNameCandidate() { - clear(); - } - - public void clear() { - rawContactId = -1; - displayName = null; - displayNameSource = DisplayNameSources.UNDEFINED; - isNameSuperPrimary = false; - writableAccount = false; - } - } /** * Constructor. @@ -284,456 +68,17 @@ public class ContactAggregator { ContactsDatabaseHelper contactsDatabaseHelper, PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter, CommonNicknameCache commonNicknameCache) { - mContactsProvider = contactsProvider; - mDbHelper = contactsDatabaseHelper; - mPhotoPriorityResolver = photoPriorityResolver; - mNameSplitter = nameSplitter; - mCommonNicknameCache = commonNicknameCache; - - SQLiteDatabase db = mDbHelper.getReadableDatabase(); - - // Since we have no way of determining which custom status was set last, - // we'll just pick one randomly. We are using MAX as an approximation of randomness - final String replaceAggregatePresenceSql = - "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "(" - + AggregatedPresenceColumns.CONTACT_ID + ", " - + StatusUpdates.PRESENCE + ", " - + StatusUpdates.CHAT_CAPABILITY + ")" - + " SELECT " + PresenceColumns.CONTACT_ID + "," - + StatusUpdates.PRESENCE + "," - + StatusUpdates.CHAT_CAPABILITY - + " FROM " + Tables.PRESENCE - + " WHERE " - + " (" + StatusUpdates.PRESENCE - + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" - + " = (SELECT " - + "MAX (" + StatusUpdates.PRESENCE - + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" - + " FROM " + Tables.PRESENCE - + " WHERE " + PresenceColumns.CONTACT_ID - + "=?)" - + " AND " + PresenceColumns.CONTACT_ID - + "=?;"; - mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql); - - mRawContactCountQuery = db.compileStatement( - "SELECT COUNT(" + RawContacts._ID + ")" + - " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContacts.CONTACT_ID + "=?" - + " AND " + RawContacts._ID + "<>?"); - - mAggregatedPresenceDelete = db.compileStatement( - "DELETE FROM " + Tables.AGGREGATED_PRESENCE + - " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?"); - - mMarkForAggregation = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" + - " WHERE " + RawContacts._ID + "=?" - + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"); - - mPhotoIdUpdate = db.compileStatement( - "UPDATE " + Tables.CONTACTS + - " SET " + Contacts.PHOTO_ID + "=?," + Contacts.PHOTO_FILE_ID + "=? " + - " WHERE " + Contacts._ID + "=?"); - - mDisplayNameUpdate = db.compileStatement( - "UPDATE " + Tables.CONTACTS + - " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " + - " WHERE " + Contacts._ID + "=?"); - - mLookupKeyUpdate = db.compileStatement( - "UPDATE " + Tables.CONTACTS + - " SET " + Contacts.LOOKUP_KEY + "=? " + - " WHERE " + Contacts._ID + "=?"); - - mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET " - + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED - + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE " - + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND " - + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?"); - - mPinnedUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET " - + Contacts.PINNED + " = IFNULL((SELECT MIN(" + RawContacts.PINNED + ") FROM " - + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=" - + ContactsColumns.CONCRETE_ID + " AND " + RawContacts.PINNED + ">" - + PinnedPositions.UNPINNED + ")," + PinnedPositions.UNPINNED + ") " - + "WHERE " + Contacts._ID + "=?"); - - mContactIdAndMarkAggregatedUpdate = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContacts.CONTACT_ID + "=?, " - + RawContactsColumns.AGGREGATION_NEEDED + "=0" + - " WHERE " + RawContacts._ID + "=?"); - - mContactIdUpdate = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContacts.CONTACT_ID + "=?" + - " WHERE " + RawContacts._ID + "=?"); - - mMarkAggregatedUpdate = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" + - " WHERE " + RawContacts._ID + "=?"); - - mPresenceContactIdUpdate = db.compileStatement( - "UPDATE " + Tables.PRESENCE + - " SET " + PresenceColumns.CONTACT_ID + "=?" + - " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?"); - - mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL); - mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL); - - mResetPinnedForRawContact = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContacts.PINNED + "=" + PinnedPositions.UNPINNED + - " WHERE " + RawContacts._ID + "=?"); - - mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); - mMimeTypeIdIdentity = mDbHelper.getMimeTypeId(Identity.CONTENT_ITEM_TYPE); - mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE); - mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); - - // Query used to retrieve data from raw contacts to populate the corresponding aggregate - mRawContactsQueryByRawContactId = String.format(Locale.US, - RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID, - mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone); - - mRawContactsQueryByContactId = String.format(Locale.US, - RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID, - mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone); - } - - public void setEnabled(boolean enabled) { - mEnabled = enabled; - } - - public boolean isEnabled() { - return mEnabled; - } - - private interface AggregationQuery { - String SQL = - "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID + - ", " + RawContactsColumns.ACCOUNT_ID + - " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContacts._ID + " IN("; - - int _ID = 0; - int CONTACT_ID = 1; - int ACCOUNT_ID = 2; + super(contactsProvider, contactsDatabaseHelper, photoPriorityResolver, nameSplitter, + commonNicknameCache); } - /** - * Aggregate all raw contacts that were marked for aggregation in the current transaction. - * Call just before committing the transaction. - */ - public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) { - final int markedCount = mRawContactsMarkedForAggregation.size(); - if (markedCount == 0) { - return; - } - - final long start = System.currentTimeMillis(); - if (DEBUG_LOGGING) { - Log.d(TAG, "aggregateInTransaction for " + markedCount + " contacts"); - } - - EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -markedCount); - - int index = 0; - - // We don't use the cached string builder (namely mSb) here, as this string can be very - // long when upgrading (where we re-aggregate all visible contacts) and StringBuilder won't - // shrink the internal storage. - // Note: don't use selection args here. We just include all IDs directly in the selection, - // because there's a limit for the number of parameters in a query. - final StringBuilder sbQuery = new StringBuilder(); - sbQuery.append(AggregationQuery.SQL); - for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) { - if (index > 0) { - sbQuery.append(','); - } - sbQuery.append(rawContactId); - index++; - } - - sbQuery.append(')'); - - final long[] rawContactIds; - final long[] contactIds; - final long[] accountIds; - final int actualCount; - final Cursor c = db.rawQuery(sbQuery.toString(), null); - try { - actualCount = c.getCount(); - rawContactIds = new long[actualCount]; - contactIds = new long[actualCount]; - accountIds = new long[actualCount]; - - index = 0; - while (c.moveToNext()) { - rawContactIds[index] = c.getLong(AggregationQuery._ID); - contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID); - accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID); - index++; - } - } finally { - c.close(); - } - - if (DEBUG_LOGGING) { - Log.d(TAG, "aggregateInTransaction: initial query done."); - } - - for (int i = 0; i < actualCount; i++) { - aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i], - mCandidates, mMatcher); - } - - long elapsedTime = System.currentTimeMillis() - start; - EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, actualCount); - - if (DEBUG_LOGGING) { - Log.d(TAG, "Contact aggregation complete: " + actualCount + - (actualCount == 0 ? "" : ", " + (elapsedTime / actualCount) - + " ms per raw contact")); - } - } - - @SuppressWarnings("deprecation") - public void triggerAggregation(TransactionContext txContext, long rawContactId) { - if (!mEnabled) { - return; - } - - int aggregationMode = mDbHelper.getAggregationMode(rawContactId); - switch (aggregationMode) { - case RawContacts.AGGREGATION_MODE_DISABLED: - break; - - case RawContacts.AGGREGATION_MODE_DEFAULT: { - markForAggregation(rawContactId, aggregationMode, false); - break; - } - - case RawContacts.AGGREGATION_MODE_SUSPENDED: { - long contactId = mDbHelper.getContactId(rawContactId); - - if (contactId != 0) { - updateAggregateData(txContext, contactId); - } - break; - } - - case RawContacts.AGGREGATION_MODE_IMMEDIATE: { - aggregateContact(txContext, mDbHelper.getWritableDatabase(), rawContactId); - break; - } - } - } - - public void clearPendingAggregations() { - // HashMap woulnd't shrink the internal table once expands it, so let's just re-create - // a new one instead of clear()ing it. - mRawContactsMarkedForAggregation = Maps.newHashMap(); - } - - public void markNewForAggregation(long rawContactId, int aggregationMode) { - mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode); - } - - public void markForAggregation(long rawContactId, int aggregationMode, boolean force) { - final int effectiveAggregationMode; - if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) { - // As per ContactsContract documentation, default aggregation mode - // does not override a previously set mode - if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { - effectiveAggregationMode = mRawContactsMarkedForAggregation.get(rawContactId); - } else { - effectiveAggregationMode = aggregationMode; - } - } else { - mMarkForAggregation.bindLong(1, rawContactId); - mMarkForAggregation.execute(); - effectiveAggregationMode = aggregationMode; - } - - mRawContactsMarkedForAggregation.put(rawContactId, effectiveAggregationMode); - } - - private static class RawContactIdAndAggregationModeQuery { - public static final String TABLE = Tables.RAW_CONTACTS; - - public static final String[] COLUMNS = { RawContacts._ID, RawContacts.AGGREGATION_MODE }; - - public static final String SELECTION = RawContacts.CONTACT_ID + "=?"; - - public static final int _ID = 0; - public static final int AGGREGATION_MODE = 1; - } - - /** - * Marks all constituent raw contacts of an aggregated contact for re-aggregation. - */ - private void markContactForAggregation(SQLiteDatabase db, long contactId) { - mSelectionArgs1[0] = String.valueOf(contactId); - Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE, - RawContactIdAndAggregationModeQuery.COLUMNS, - RawContactIdAndAggregationModeQuery.SELECTION, mSelectionArgs1, null, null, null); - try { - if (cursor.moveToFirst()) { - long rawContactId = cursor.getLong(RawContactIdAndAggregationModeQuery._ID); - int aggregationMode = cursor.getInt( - RawContactIdAndAggregationModeQuery.AGGREGATION_MODE); - // Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED. - // (Also just ignore deprecated AGGREGATION_MODE_IMMEDIATE) - if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { - markForAggregation(rawContactId, aggregationMode, true); - } - } - } finally { - cursor.close(); - } - } - - /** - * Mark all visible contacts for re-aggregation. - * - * - Set {@link RawContactsColumns#AGGREGATION_NEEDED} For all visible raw_contacts with - * {@link RawContacts#AGGREGATION_MODE_DEFAULT}. - * - Also put them into {@link #mRawContactsMarkedForAggregation}. - */ - public int markAllVisibleForAggregation(SQLiteDatabase db) { - final long start = System.currentTimeMillis(); - - // Set AGGREGATION_NEEDED for all visible raw_cotnacts with AGGREGATION_MODE_DEFAULT. - // (Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED) - db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " + - RawContactsColumns.AGGREGATION_NEEDED + "=1" + - " WHERE " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + - " AND " + RawContacts.AGGREGATION_MODE + "=" + RawContacts.AGGREGATION_MODE_DEFAULT - ); - - final int count; - final Cursor cursor = db.rawQuery("SELECT " + RawContacts._ID + - " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1", null); - try { - count = cursor.getCount(); - cursor.moveToPosition(-1); - while (cursor.moveToNext()) { - final long rawContactId = cursor.getLong(0); - mRawContactsMarkedForAggregation.put(rawContactId, - RawContacts.AGGREGATION_MODE_DEFAULT); - } - } finally { - cursor.close(); - } - - final long end = System.currentTimeMillis(); - Log.i(TAG, "Marked all visible contacts for aggregation: " + count + " raw contacts, " + - (end - start) + " ms"); - return count; - } - - /** - * Creates a new contact based on the given raw contact. Does not perform aggregation. Returns - * the ID of the contact that was created. - */ - public long onRawContactInsert( - TransactionContext txContext, SQLiteDatabase db, long rawContactId) { - long contactId = insertContact(db, rawContactId); - setContactId(rawContactId, contactId); - mDbHelper.updateContactVisible(txContext, contactId); - return contactId; - } - - protected long insertContact(SQLiteDatabase db, long rawContactId) { - mSelectionArgs1[0] = String.valueOf(rawContactId); - computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert); - return mContactInsert.executeInsert(); - } - - private static final class RawContactIdAndAccountQuery { - public static final String TABLE = Tables.RAW_CONTACTS; - - public static final String[] COLUMNS = { - RawContacts.CONTACT_ID, - RawContactsColumns.ACCOUNT_ID - }; - - public static final String SELECTION = RawContacts._ID + "=?"; - - public static final int CONTACT_ID = 0; - public static final int ACCOUNT_ID = 1; - } - - public void aggregateContact( - TransactionContext txContext, SQLiteDatabase db, long rawContactId) { - if (!mEnabled) { - return; - } - - MatchCandidateList candidates = new MatchCandidateList(); - ContactMatcher matcher = new ContactMatcher(); - - long contactId = 0; - long accountId = 0; - mSelectionArgs1[0] = String.valueOf(rawContactId); - Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE, - RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION, - mSelectionArgs1, null, null, null); - try { - if (cursor.moveToFirst()) { - contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID); - accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID); - } - } finally { - cursor.close(); - } - - aggregateContact(txContext, db, rawContactId, accountId, contactId, - candidates, matcher); - } - - public void updateAggregateData(TransactionContext txContext, long contactId) { - if (!mEnabled) { - return; - } - - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - computeAggregateData(db, contactId, mContactUpdate); - mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId); - mContactUpdate.execute(); - - mDbHelper.updateContactVisible(txContext, contactId); - updateAggregatedStatusUpdate(contactId); - } - - private void updateAggregatedStatusUpdate(long contactId) { - mAggregatedPresenceReplace.bindLong(1, contactId); - mAggregatedPresenceReplace.bindLong(2, contactId); - mAggregatedPresenceReplace.execute(); - updateLastStatusUpdateId(contactId); - } - - /** - * Adjusts the reference to the latest status update for the specified contact. - */ - public void updateLastStatusUpdateId(long contactId) { - String contactIdString = String.valueOf(contactId); - mDbHelper.getWritableDatabase().execSQL(UPDATE_LAST_STATUS_UPDATE_ID_SQL, - new String[]{contactIdString, contactIdString}); - } - - /** + /** * Given a specific raw contact, finds all matching aggregate contacts and chooses the one * with the highest match score. If no such contact is found, creates a new contact. */ - private synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db, - long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates, - ContactMatcher matcher) { + synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db, + long rawContactId, long accountId, long currentContactId, + MatchCandidateList candidates) { if (VERBOSE_LOGGING) { Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId); @@ -749,6 +94,7 @@ public class ContactAggregator { long contactId = -1; // Best matching contact ID. boolean needReaggregate = false; + final ContactMatcher matcher = new ContactMatcher(); final Set<Long> rawContactIdsInSameAccount = new HashSet<Long>(); final Set<Long> rawContactIdsInOtherAccount = new HashSet<Long>(); if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { @@ -1017,87 +363,6 @@ public class ContactAggregator { } } - private interface RawContactMatchingSelectionStatement { - String SELECT_COUNT = "SELECT count(*) " ; - String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2." + Data.RAW_CONTACT_ID ; - } - - /** - * Build sql to check if there is any identity match/mis-match between two sets of raw contact - * ids on the same namespace. - */ - private String buildIdentityMatchingSql(String rawContactIdSet1, String rawContactIdSet2, - boolean isIdentityMatching, boolean countOnly) { - final String identityType = String.valueOf(mMimeTypeIdIdentity); - final String matchingOperator = (isIdentityMatching) ? "=" : "!="; - final String sql = - " FROM " + Tables.DATA + " AS d1" + - " JOIN " + Tables.DATA + " AS d2" + - " ON (d1." + Identity.IDENTITY + matchingOperator + - " d2." + Identity.IDENTITY + " AND" + - " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" + - " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + identityType + - " AND d2." + DataColumns.MIMETYPE_ID + " = " + identityType + - " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" + - " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")"; - return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql : - RawContactMatchingSelectionStatement.SELECT_ID + sql; - } - - private String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2, - boolean countOnly) { - final String emailType = String.valueOf(mMimeTypeIdEmail); - final String sql = - " FROM " + Tables.DATA + " AS d1" + - " JOIN " + Tables.DATA + " AS d2" + - " ON lower(d1." + Email.ADDRESS + ")= lower(d2." + Email.ADDRESS + ")" + - " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + emailType + - " AND d2." + DataColumns.MIMETYPE_ID + " = " + emailType + - " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" + - " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")"; - return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql : - RawContactMatchingSelectionStatement.SELECT_ID + sql; - } - - private String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2, - boolean countOnly) { - // It's a bit tricker because it has to be consistent with - // updateMatchScoresBasedOnPhoneMatches(). - final String phoneType = String.valueOf(mMimeTypeIdPhone); - final String sql = - " FROM " + Tables.PHONE_LOOKUP + " AS p1" + - " JOIN " + Tables.DATA + " AS d1 ON " + - "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" + - " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH + - "=p2." + PhoneLookupColumns.MIN_MATCH + ")" + - " JOIN " + Tables.DATA + " AS d2 ON " + - "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" + - " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + phoneType + - " AND d2." + DataColumns.MIMETYPE_ID + " = " + phoneType + - " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" + - " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")" + - " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + "," + - String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter()) + - ")"; - return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql : - RawContactMatchingSelectionStatement.SELECT_ID + sql; - } - - private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2) { - return "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " + - AggregationExceptions.RAW_CONTACT_ID2 + - " FROM " + Tables.AGGREGATION_EXCEPTIONS + - " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" + - rawContactIdSet1 + ")" + - " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" + - " AND " + AggregationExceptions.TYPE + "=" + - AggregationExceptions.TYPE_KEEP_TOGETHER ; - } - - private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) { - return DatabaseUtils.longForQuery(db, query, null) > 0; - } - /** * If there's any identity, email address or a phone number matching between two raw contact * sets. @@ -1191,82 +456,6 @@ public class ContactAggregator { } /** - * Partition the given raw contact Ids to connected component based on aggregation exception, - * identity matching, email matching or phone matching. - */ - private Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long> rawContactIdSet) { - // Connections between two raw contacts - final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create(); - String rawContactIds = TextUtils.join(",", rawContactIdSet); - findIdPairs(db, buildExceptionMatchingSql(rawContactIds, rawContactIds), - matchingRawIdPairs); - findIdPairs(db, buildIdentityMatchingSql(rawContactIds, rawContactIds, - /* isIdentityMatching =*/ true, /* countOnly =*/false), matchingRawIdPairs); - findIdPairs(db, buildEmailMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false), - matchingRawIdPairs); - findIdPairs(db, buildPhoneMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false), - matchingRawIdPairs); - - return ContactAggregatorHelper.findConnectedComponents(rawContactIdSet, matchingRawIdPairs); - } - - /** - * Given a query which will return two non-null IDs in the first two columns as results, this - * method will put two entries into the given result map for each pair of different IDs, one - * keyed by each ID. - */ - private void findIdPairs(SQLiteDatabase db, String query, Multimap<Long, Long> results) { - Cursor cursor = db.rawQuery(query, null); - try { - cursor.moveToPosition(-1); - while (cursor.moveToNext()) { - long idA = cursor.getLong(0); - long idB = cursor.getLong(1); - if (idA != idB) { - results.put(idA, idB); - results.put(idB, idA); - } - } - } finally { - cursor.close(); - } - } - - /** - * Creates a new Contact for a given set of the raw contacts of {@code rawContactIds} if the - * given contactId is null. Otherwise, regroup them into contact with {@code contactId}. - */ - private void createContactForRawContacts(SQLiteDatabase db, TransactionContext txContext, - Set<Long> rawContactIds, Long contactId) { - if (rawContactIds.isEmpty()) { - // No raw contact id is provided. - return; - } - - // If contactId is not provided, generates a new one. - if (contactId == null) { - mSelectionArgs1[0]= String.valueOf(rawContactIds.iterator().next()); - computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, - mContactInsert); - contactId = mContactInsert.executeInsert(); - } - for (Long rawContactId : rawContactIds) { - // Regrouped contacts should automatically be unpinned. - unpinRawContact(rawContactId); - setContactIdAndMarkAggregated(rawContactId, contactId); - setPresenceContactId(rawContactId, contactId); - } - updateAggregateData(txContext, contactId); - } - - private static class RawContactIdQuery { - public static final String TABLE = Tables.RAW_CONTACTS; - public static final String[] COLUMNS = { RawContacts._ID }; - public static final String SELECTION = RawContacts.CONTACT_ID + "=?"; - public static final int RAW_CONTACT_ID = 0; - } - - /** * Ensures that automatic aggregation rules are followed after a contact * becomes visible or invisible. Specifically, consider this case: there are * three contacts named Foo. Two of them come from account A1 and one comes @@ -1331,14 +520,6 @@ public class ContactAggregator { } /** - * Marks the specified raw contact ID as aggregated - */ - private void markAggregated(long rawContactId) { - mMarkAggregatedUpdate.bindLong(1, rawContactId); - mMarkAggregatedUpdate.execute(); - } - - /** * Updates the contact ID for the specified contact and marks the raw contact as aggregated. */ private void setContactIdAndMarkAggregated(long rawContactId, long contactId) { @@ -1347,63 +528,14 @@ public class ContactAggregator { mContactIdAndMarkAggregatedUpdate.execute(); } - private void setPresenceContactId(long rawContactId, long contactId) { - mPresenceContactIdUpdate.bindLong(1, contactId); - mPresenceContactIdUpdate.bindLong(2, rawContactId); - mPresenceContactIdUpdate.execute(); - } - - private void unpinRawContact(long rawContactId) { - mResetPinnedForRawContact.bindLong(1, rawContactId); - mResetPinnedForRawContact.execute(); - } - - interface AggregateExceptionPrefetchQuery { - String TABLE = Tables.AGGREGATION_EXCEPTIONS; - - String[] COLUMNS = { - AggregationExceptions.RAW_CONTACT_ID1, - AggregationExceptions.RAW_CONTACT_ID2, - }; - - int RAW_CONTACT_ID1 = 0; - int RAW_CONTACT_ID2 = 1; - } // A set of raw contact IDs for which there are aggregation exceptions - private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>(); private boolean mAggregationExceptionIdsValid; public void invalidateAggregationExceptionCache() { mAggregationExceptionIdsValid = false; } - /** - * Finds all raw contact IDs for which there are aggregation exceptions. The list of - * ids is used as an optimization in aggregation: there is no point to run a query against - * the agg_exceptions table if it is known that there are no records there for a given - * raw contact ID. - */ - private void prefetchAggregationExceptionIds(SQLiteDatabase db) { - mAggregationExceptionIds.clear(); - final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE, - AggregateExceptionPrefetchQuery.COLUMNS, - null, null, null, null, null); - - try { - while (c.moveToNext()) { - long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1); - long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2); - mAggregationExceptionIds.add(rawContactId1); - mAggregationExceptionIds.add(rawContactId2); - } - } finally { - c.close(); - } - - mAggregationExceptionIdsValid = true; - } - interface AggregateExceptionQuery { String TABLE = Tables.AGGREGATION_EXCEPTIONS + " JOIN raw_contacts raw_contacts1 " @@ -1478,7 +610,7 @@ public class ContactAggregator { c.close(); } - return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true); + return matcher.pickBestMatch(MatchScore.MAX_SCORE, true); } /** @@ -1549,42 +681,6 @@ public class ContactAggregator { return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false); } - private interface NameLookupQuery { - String TABLE = Tables.NAME_LOOKUP; - - String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?"; - String SELECTION_STRUCTURED_NAME_BASED = - SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL; - - String[] COLUMNS = new String[] { - NameLookupColumns.NORMALIZED_NAME, - NameLookupColumns.NAME_TYPE - }; - - int NORMALIZED_NAME = 0; - int NAME_TYPE = 1; - } - - private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId, - MatchCandidateList candidates, boolean structuredNameBased) { - candidates.clear(); - mSelectionArgs1[0] = String.valueOf(rawContactId); - Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS, - structuredNameBased - ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED - : NameLookupQuery.SELECTION, - mSelectionArgs1, null, null, null); - try { - while (c.moveToNext()) { - String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME); - int type = c.getInt(NameLookupQuery.NAME_TYPE); - candidates.add(normalizedName, type); - } - } finally { - c.close(); - } - } - /** * Computes scores for contacts that have matching data rows. */ @@ -1702,124 +798,6 @@ public class ContactAggregator { } } - private interface NameLookupMatchQueryWithParameter { - String TABLE = Tables.NAME_LOOKUP - + " JOIN " + Tables.RAW_CONTACTS + - " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = " - + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - - String[] COLUMNS = new String[] { - RawContacts.CONTACT_ID, - NameLookupColumns.NORMALIZED_NAME, - NameLookupColumns.NAME_TYPE, - }; - - int CONTACT_ID = 0; - int NAME = 1; - int NAME_TYPE = 2; - } - - private final class NameLookupSelectionBuilder extends NameLookupBuilder { - - private final MatchCandidateList mNameLookupCandidates; - - private StringBuilder mSelection = new StringBuilder( - NameLookupColumns.NORMALIZED_NAME + " IN("); - - - public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) { - super(splitter); - this.mNameLookupCandidates = candidates; - } - - @Override - protected String[] getCommonNicknameClusters(String normalizedName) { - return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); - } - - @Override - protected void insertNameLookup( - long rawContactId, long dataId, int lookupType, String string) { - mNameLookupCandidates.add(string, lookupType); - DatabaseUtils.appendEscapedSQLString(mSelection, string); - mSelection.append(','); - } - - public boolean isEmpty() { - return mNameLookupCandidates.isEmpty(); - } - - public String getSelection() { - mSelection.setLength(mSelection.length() - 1); // Strip last comma - mSelection.append(')'); - return mSelection.toString(); - } - - public int getLookupType(String name) { - for (int i = 0; i < mNameLookupCandidates.mCount; i++) { - if (mNameLookupCandidates.mList.get(i).mName.equals(name)) { - return mNameLookupCandidates.mList.get(i).mLookupType; - } - } - throw new IllegalStateException(); - } - } - - /** - * Finds contacts with names matching the specified name. - */ - private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query, - MatchCandidateList candidates, ContactMatcher matcher) { - candidates.clear(); - NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder( - mNameSplitter, candidates); - builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED); - if (builder.isEmpty()) { - return; - } - - Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE, - NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null, - null, PRIMARY_HIT_LIMIT_STRING); - try { - while (c.moveToNext()) { - long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID); - String name = c.getString(NameLookupMatchQueryWithParameter.NAME); - int nameTypeA = builder.getLookupType(name); - int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE); - matcher.matchName(contactId, nameTypeA, name, nameTypeB, name, - ContactMatcher.MATCHING_ALGORITHM_EXACT); - if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) { - matcher.updateScoreWithNicknameMatch(contactId); - } - } - } finally { - c.close(); - } - } - - private interface EmailLookupQuery { - String TABLE = Tables.DATA + " dataA" - + " JOIN " + Tables.DATA + " dataB" + - " ON lower(" + "dataA." + Email.DATA + ")=lower(dataB." + Email.DATA + ")" - + " JOIN " + Tables.RAW_CONTACTS + - " ON (dataB." + Data.RAW_CONTACT_ID + " = " - + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - - String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1" - + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2" - + " AND dataA." + Email.DATA + " NOT NULL" - + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2" - + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" - + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; - - String[] COLUMNS = new String[] { - RawContacts.CONTACT_ID - }; - - int CONTACT_ID = 0; - } - private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, ContactMatcher matcher) { mSelectionArgs2[0] = String.valueOf(rawContactId); @@ -1837,32 +815,6 @@ public class ContactAggregator { } } - private interface PhoneLookupQuery { - String TABLE = Tables.PHONE_LOOKUP + " phoneA" - + " JOIN " + Tables.DATA + " dataA" - + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")" - + " JOIN " + Tables.PHONE_LOOKUP + " phoneB" - + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "=" - + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")" - + " JOIN " + Tables.DATA + " dataB" - + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")" - + " JOIN " + Tables.RAW_CONTACTS - + " ON (dataB." + Data.RAW_CONTACT_ID + " = " - + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - - String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" - + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", " - + "dataB." + Phone.NUMBER + ",?)" - + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" - + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; - - String[] COLUMNS = new String[] { - RawContacts.CONTACT_ID - }; - - int CONTACT_ID = 0; - } - private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId, ContactMatcher matcher) { mSelectionArgs2[0] = String.valueOf(rawContactId); @@ -1951,804 +903,12 @@ public class ContactAggregator { } } - private interface RawContactsQuery { - String SQL_FORMAT_HAS_SUPER_PRIMARY_NAME = - " EXISTS(SELECT 1 " + - " FROM " + Tables.DATA + " d " + - " WHERE d." + DataColumns.MIMETYPE_ID + "=%d " + - " AND d." + Data.RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + - " AND d." + Data.IS_SUPER_PRIMARY + "=1)"; - - String SQL_FORMAT = - "SELECT " - + RawContactsColumns.CONCRETE_ID + "," - + RawContactsColumns.DISPLAY_NAME + "," - + RawContactsColumns.DISPLAY_NAME_SOURCE + "," - + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "," - + AccountsColumns.CONCRETE_ACCOUNT_NAME + "," - + AccountsColumns.CONCRETE_DATA_SET + "," - + RawContacts.SOURCE_ID + "," - + RawContacts.CUSTOM_RINGTONE + "," - + RawContacts.SEND_TO_VOICEMAIL + "," - + RawContacts.LAST_TIME_CONTACTED + "," - + RawContacts.TIMES_CONTACTED + "," - + RawContacts.STARRED + "," - + RawContacts.PINNED + "," - + DataColumns.CONCRETE_ID + "," - + DataColumns.CONCRETE_MIMETYPE_ID + "," - + Data.IS_SUPER_PRIMARY + "," - + Photo.PHOTO_FILE_ID + "," - + SQL_FORMAT_HAS_SUPER_PRIMARY_NAME + - " FROM " + Tables.RAW_CONTACTS + - " JOIN " + Tables.ACCOUNTS + " ON (" - + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID - + ")" + - " LEFT OUTER JOIN " + Tables.DATA + - " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID - + " AND ((" + DataColumns.MIMETYPE_ID + "=%d" - + " AND " + Photo.PHOTO + " NOT NULL)" - + " OR (" + DataColumns.MIMETYPE_ID + "=%d" - + " AND " + Phone.NUMBER + " NOT NULL)))"; - - String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT + - " WHERE " + RawContactsColumns.CONCRETE_ID + "=?"; - - String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT + - " WHERE " + RawContacts.CONTACT_ID + "=?" - + " AND " + RawContacts.DELETED + "=0"; - - int RAW_CONTACT_ID = 0; - int DISPLAY_NAME = 1; - int DISPLAY_NAME_SOURCE = 2; - int ACCOUNT_TYPE = 3; - int ACCOUNT_NAME = 4; - int DATA_SET = 5; - int SOURCE_ID = 6; - int CUSTOM_RINGTONE = 7; - int SEND_TO_VOICEMAIL = 8; - int LAST_TIME_CONTACTED = 9; - int TIMES_CONTACTED = 10; - int STARRED = 11; - int PINNED = 12; - int DATA_ID = 13; - int MIMETYPE_ID = 14; - int IS_SUPER_PRIMARY = 15; - int PHOTO_FILE_ID = 16; - int HAS_SUPER_PRIMARY_NAME = 17; - } - - private interface ContactReplaceSqlStatement { - String UPDATE_SQL = - "UPDATE " + Tables.CONTACTS + - " SET " - + Contacts.NAME_RAW_CONTACT_ID + "=?, " - + Contacts.PHOTO_ID + "=?, " - + Contacts.PHOTO_FILE_ID + "=?, " - + Contacts.SEND_TO_VOICEMAIL + "=?, " - + Contacts.CUSTOM_RINGTONE + "=?, " - + Contacts.LAST_TIME_CONTACTED + "=?, " - + Contacts.TIMES_CONTACTED + "=?, " - + Contacts.STARRED + "=?, " - + Contacts.PINNED + "=?, " - + Contacts.HAS_PHONE_NUMBER + "=?, " - + Contacts.LOOKUP_KEY + "=?, " - + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=? " + - " WHERE " + Contacts._ID + "=?"; - - String INSERT_SQL = - "INSERT INTO " + Tables.CONTACTS + " (" - + Contacts.NAME_RAW_CONTACT_ID + ", " - + Contacts.PHOTO_ID + ", " - + Contacts.PHOTO_FILE_ID + ", " - + Contacts.SEND_TO_VOICEMAIL + ", " - + Contacts.CUSTOM_RINGTONE + ", " - + Contacts.LAST_TIME_CONTACTED + ", " - + Contacts.TIMES_CONTACTED + ", " - + Contacts.STARRED + ", " - + Contacts.PINNED + ", " - + Contacts.HAS_PHONE_NUMBER + ", " - + Contacts.LOOKUP_KEY + ", " - + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP - + ") " + - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"; - - int NAME_RAW_CONTACT_ID = 1; - int PHOTO_ID = 2; - int PHOTO_FILE_ID = 3; - int SEND_TO_VOICEMAIL = 4; - int CUSTOM_RINGTONE = 5; - int LAST_TIME_CONTACTED = 6; - int TIMES_CONTACTED = 7; - int STARRED = 8; - int PINNED = 9; - int HAS_PHONE_NUMBER = 10; - int LOOKUP_KEY = 11; - int CONTACT_LAST_UPDATED_TIMESTAMP = 12; - int CONTACT_ID = 13; - } - - /** - * Computes aggregate-level data for the specified aggregate contact ID. - */ - private void computeAggregateData(SQLiteDatabase db, long contactId, - SQLiteStatement statement) { - mSelectionArgs1[0] = String.valueOf(contactId); - computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement); - } - - /** - * Indicates whether the given photo entry and priority gives this photo a higher overall - * priority than the current best photo entry and priority. - */ - private boolean hasHigherPhotoPriority(PhotoEntry photoEntry, int priority, - PhotoEntry bestPhotoEntry, int bestPriority) { - int photoComparison = photoEntry.compareTo(bestPhotoEntry); - return photoComparison < 0 || photoComparison == 0 && priority > bestPriority; - } - - /** - * Computes aggregate-level data from constituent raw contacts. - */ - private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs, - SQLiteStatement statement) { - long currentRawContactId = -1; - long bestPhotoId = -1; - long bestPhotoFileId = 0; - PhotoEntry bestPhotoEntry = null; - boolean foundSuperPrimaryPhoto = false; - int photoPriority = -1; - int totalRowCount = 0; - int contactSendToVoicemail = 0; - String contactCustomRingtone = null; - long contactLastTimeContacted = 0; - int contactTimesContacted = 0; - int contactStarred = 0; - int contactPinned = Integer.MAX_VALUE; - int hasPhoneNumber = 0; - StringBuilder lookupKey = new StringBuilder(); - - mDisplayNameCandidate.clear(); - - Cursor c = db.rawQuery(sql, sqlArgs); - try { - while (c.moveToNext()) { - long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID); - if (rawContactId != currentRawContactId) { - currentRawContactId = rawContactId; - totalRowCount++; - - // Assemble sub-account. - String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); - String dataSet = c.getString(RawContactsQuery.DATA_SET); - String accountWithDataSet = (!TextUtils.isEmpty(dataSet)) - ? accountType + "/" + dataSet - : accountType; - - // Display name - String displayName = c.getString(RawContactsQuery.DISPLAY_NAME); - int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE); - int isNameSuperPrimary = c.getInt(RawContactsQuery.HAS_SUPER_PRIMARY_NAME); - processDisplayNameCandidate(rawContactId, displayName, displayNameSource, - mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet), - isNameSuperPrimary != 0); - - // Contact options - if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) { - boolean sendToVoicemail = - (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0); - if (sendToVoicemail) { - contactSendToVoicemail++; - } - } - - if (contactCustomRingtone == null - && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) { - contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE); - } - - long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED); - if (lastTimeContacted > contactLastTimeContacted) { - contactLastTimeContacted = lastTimeContacted; - } - - int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED); - if (timesContacted > contactTimesContacted) { - contactTimesContacted = timesContacted; - } - - if (c.getInt(RawContactsQuery.STARRED) != 0) { - contactStarred = 1; - } - - // contactPinned should be the lowest value of its constituent raw contacts, - // excluding negative integers - final int rawContactPinned = c.getInt(RawContactsQuery.PINNED); - if (rawContactPinned > PinnedPositions.UNPINNED) { - contactPinned = Math.min(contactPinned, rawContactPinned); - } - - appendLookupKey( - lookupKey, - accountWithDataSet, - c.getString(RawContactsQuery.ACCOUNT_NAME), - rawContactId, - c.getString(RawContactsQuery.SOURCE_ID), - displayName); - } - - if (!c.isNull(RawContactsQuery.DATA_ID)) { - long dataId = c.getLong(RawContactsQuery.DATA_ID); - long photoFileId = c.getLong(RawContactsQuery.PHOTO_FILE_ID); - int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID); - boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0; - if (mimetypeId == mMimeTypeIdPhoto) { - if (!foundSuperPrimaryPhoto) { - // Lookup the metadata for the photo, if available. Note that data set - // does not come into play here, since accounts are looked up in the - // account manager in the priority resolver. - PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId); - String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); - int priority = mPhotoPriorityResolver.getPhotoPriority(accountType); - if (superPrimary || hasHigherPhotoPriority( - photoEntry, priority, bestPhotoEntry, photoPriority)) { - bestPhotoEntry = photoEntry; - photoPriority = priority; - bestPhotoId = dataId; - bestPhotoFileId = photoFileId; - foundSuperPrimaryPhoto |= superPrimary; - } - } - } else if (mimetypeId == mMimeTypeIdPhone) { - hasPhoneNumber = 1; - } - } - } - } finally { - c.close(); - } - - if (contactPinned == Integer.MAX_VALUE) { - contactPinned = PinnedPositions.UNPINNED; - } - - statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID, - mDisplayNameCandidate.rawContactId); - - if (bestPhotoId != -1) { - statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId); - } else { - statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID); - } - - if (bestPhotoFileId != 0) { - statement.bindLong(ContactReplaceSqlStatement.PHOTO_FILE_ID, bestPhotoFileId); - } else { - statement.bindNull(ContactReplaceSqlStatement.PHOTO_FILE_ID); - } - - statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL, - totalRowCount == contactSendToVoicemail ? 1 : 0); - DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE, - contactCustomRingtone); - statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED, - contactLastTimeContacted); - statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED, - contactTimesContacted); - statement.bindLong(ContactReplaceSqlStatement.STARRED, - contactStarred); - statement.bindLong(ContactReplaceSqlStatement.PINNED, - contactPinned); - statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER, - hasPhoneNumber); - statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY, - Uri.encode(lookupKey.toString())); - statement.bindLong(ContactReplaceSqlStatement.CONTACT_LAST_UPDATED_TIMESTAMP, - Clock.getInstance().currentTimeMillis()); - } - - /** - * Builds a lookup key using the given data. - */ - protected void appendLookupKey(StringBuilder sb, String accountTypeWithDataSet, - String accountName, long rawContactId, String sourceId, String displayName) { - ContactLookupKey.appendToLookupKey(sb, accountTypeWithDataSet, accountName, rawContactId, - sourceId, displayName); - } - - /** - * Uses the supplied values to determine if they represent a "better" display name - * for the aggregate contact currently evaluated. If so, it updates - * {@link #mDisplayNameCandidate} with the new values. - */ - private void processDisplayNameCandidate(long rawContactId, String displayName, - int displayNameSource, boolean writableAccount, boolean isNameSuperPrimary) { - - boolean replace = false; - if (mDisplayNameCandidate.rawContactId == -1) { - // No previous values available - replace = true; - } else if (!TextUtils.isEmpty(displayName)) { - if (isNameSuperPrimary) { - // A super primary name is better than any other name - replace = true; - } else if (mDisplayNameCandidate.isNameSuperPrimary == isNameSuperPrimary) { - if (mDisplayNameCandidate.displayNameSource < displayNameSource) { - // New values come from an superior source, e.g. structured name vs phone number - replace = true; - } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) { - if (!mDisplayNameCandidate.writableAccount && writableAccount) { - replace = true; - } else if (mDisplayNameCandidate.writableAccount == writableAccount) { - if (NameNormalizer.compareComplexity(displayName, - mDisplayNameCandidate.displayName) > 0) { - // New name is more complex than the previously found one - replace = true; - } - } - } - } - } - - if (replace) { - mDisplayNameCandidate.rawContactId = rawContactId; - mDisplayNameCandidate.displayName = displayName; - mDisplayNameCandidate.displayNameSource = displayNameSource; - mDisplayNameCandidate.isNameSuperPrimary = isNameSuperPrimary; - mDisplayNameCandidate.writableAccount = writableAccount; - } - } - - private interface PhotoIdQuery { - final String[] COLUMNS = new String[] { - AccountsColumns.CONCRETE_ACCOUNT_TYPE, - DataColumns.CONCRETE_ID, - Data.IS_SUPER_PRIMARY, - Photo.PHOTO_FILE_ID, - }; - - int ACCOUNT_TYPE = 0; - int DATA_ID = 1; - int IS_SUPER_PRIMARY = 2; - int PHOTO_FILE_ID = 3; - } - - public void updatePhotoId(SQLiteDatabase db, long rawContactId) { - - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - long bestPhotoId = -1; - long bestPhotoFileId = 0; - int photoPriority = -1; - - long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE); - - String tables = Tables.RAW_CONTACTS - + " JOIN " + Tables.ACCOUNTS + " ON (" - + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID - + ")" - + " JOIN " + Tables.DATA + " ON(" - + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID - + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND " - + Photo.PHOTO + " NOT NULL))"; - - mSelectionArgs1[0] = String.valueOf(contactId); - final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS, - RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null); - try { - PhotoEntry bestPhotoEntry = null; - while (c.moveToNext()) { - long dataId = c.getLong(PhotoIdQuery.DATA_ID); - long photoFileId = c.getLong(PhotoIdQuery.PHOTO_FILE_ID); - boolean superPrimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0; - PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId); - - // Note that data set does not come into play here, since accounts are looked up in - // the account manager in the priority resolver. - String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE); - int priority = mPhotoPriorityResolver.getPhotoPriority(accountType); - if (superPrimary || hasHigherPhotoPriority( - photoEntry, priority, bestPhotoEntry, photoPriority)) { - bestPhotoEntry = photoEntry; - photoPriority = priority; - bestPhotoId = dataId; - bestPhotoFileId = photoFileId; - if (superPrimary) { - break; - } - } - } - } finally { - c.close(); - } - - if (bestPhotoId == -1) { - mPhotoIdUpdate.bindNull(1); - } else { - mPhotoIdUpdate.bindLong(1, bestPhotoId); - } - - if (bestPhotoFileId == 0) { - mPhotoIdUpdate.bindNull(2); - } else { - mPhotoIdUpdate.bindLong(2, bestPhotoFileId); - } - - mPhotoIdUpdate.bindLong(3, contactId); - mPhotoIdUpdate.execute(); - } - - private interface PhotoFileQuery { - final String[] COLUMNS = new String[] { - PhotoFiles.HEIGHT, - PhotoFiles.WIDTH, - PhotoFiles.FILESIZE - }; - - int HEIGHT = 0; - int WIDTH = 1; - int FILESIZE = 2; - } - - private class PhotoEntry implements Comparable<PhotoEntry> { - // Pixel count (width * height) for the image. - final int pixelCount; - - // File size (in bytes) of the image. Not populated if the image is a thumbnail. - final int fileSize; - - private PhotoEntry(int pixelCount, int fileSize) { - this.pixelCount = pixelCount; - this.fileSize = fileSize; - } - - @Override - public int compareTo(PhotoEntry pe) { - if (pe == null) { - return -1; - } - if (pixelCount == pe.pixelCount) { - return pe.fileSize - fileSize; - } else { - return pe.pixelCount - pixelCount; - } - } - } - - private PhotoEntry getPhotoMetadata(SQLiteDatabase db, long photoFileId) { - if (photoFileId == 0) { - // Assume standard thumbnail size. Don't bother getting a file size for priority; - // we should fall back to photo priority resolver if all we have are thumbnails. - int thumbDim = mContactsProvider.getMaxThumbnailDim(); - return new PhotoEntry(thumbDim * thumbDim, 0); - } else { - Cursor c = db.query(Tables.PHOTO_FILES, PhotoFileQuery.COLUMNS, PhotoFiles._ID + "=?", - new String[]{String.valueOf(photoFileId)}, null, null, null); - try { - if (c.getCount() == 1) { - c.moveToFirst(); - int pixelCount = - c.getInt(PhotoFileQuery.HEIGHT) * c.getInt(PhotoFileQuery.WIDTH); - return new PhotoEntry(pixelCount, c.getInt(PhotoFileQuery.FILESIZE)); - } - } finally { - c.close(); - } - } - return new PhotoEntry(0, 0); - } - - private interface DisplayNameQuery { - String SQL_HAS_SUPER_PRIMARY_NAME = - " EXISTS(SELECT 1 " + - " FROM " + Tables.DATA + " d " + - " WHERE d." + DataColumns.MIMETYPE_ID + "=? " + - " AND d." + Data.RAW_CONTACT_ID + "=" + Views.RAW_CONTACTS - + "." + RawContacts._ID + - " AND d." + Data.IS_SUPER_PRIMARY + "=1)"; - - String SQL = - "SELECT " - + RawContacts._ID + "," - + RawContactsColumns.DISPLAY_NAME + "," - + RawContactsColumns.DISPLAY_NAME_SOURCE + "," - + SQL_HAS_SUPER_PRIMARY_NAME + "," - + RawContacts.SOURCE_ID + "," - + RawContacts.ACCOUNT_TYPE_AND_DATA_SET + - " FROM " + Views.RAW_CONTACTS + - " WHERE " + RawContacts.CONTACT_ID + "=? "; - - int _ID = 0; - int DISPLAY_NAME = 1; - int DISPLAY_NAME_SOURCE = 2; - int HAS_SUPER_PRIMARY_NAME = 3; - int SOURCE_ID = 4; - int ACCOUNT_TYPE_AND_DATA_SET = 5; - } - - public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) { - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - updateDisplayNameForContact(db, contactId); - } - - public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) { - boolean lookupKeyUpdateNeeded = false; - - mDisplayNameCandidate.clear(); - - mSelectionArgs2[0] = String.valueOf(mDbHelper.getMimeTypeIdForStructuredName()); - mSelectionArgs2[1] = String.valueOf(contactId); - final Cursor c = db.rawQuery(DisplayNameQuery.SQL, mSelectionArgs2); - try { - while (c.moveToNext()) { - long rawContactId = c.getLong(DisplayNameQuery._ID); - String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME); - int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE); - int isNameSuperPrimary = c.getInt(DisplayNameQuery.HAS_SUPER_PRIMARY_NAME); - String accountTypeAndDataSet = c.getString( - DisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET); - processDisplayNameCandidate(rawContactId, displayName, displayNameSource, - mContactsProvider.isWritableAccountWithDataSet(accountTypeAndDataSet), - isNameSuperPrimary != 0); - - // If the raw contact has no source id, the lookup key is based on the display - // name, so the lookup key needs to be updated. - lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID); - } - } finally { - c.close(); - } - - if (mDisplayNameCandidate.rawContactId != -1) { - mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId); - mDisplayNameUpdate.bindLong(2, contactId); - mDisplayNameUpdate.execute(); - } - - if (lookupKeyUpdateNeeded) { - updateLookupKeyForContact(db, contactId); - } - } - - - /** - * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the - * specified raw contact. - */ - public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) { - - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - final SQLiteStatement hasPhoneNumberUpdate = db.compileStatement( - "UPDATE " + Tables.CONTACTS + - " SET " + Contacts.HAS_PHONE_NUMBER + "=" - + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)" - + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS - + " WHERE " + DataColumns.MIMETYPE_ID + "=?" - + " AND " + Phone.NUMBER + " NOT NULL" - + " AND " + RawContacts.CONTACT_ID + "=?)" + - " WHERE " + Contacts._ID + "=?"); - try { - hasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE)); - hasPhoneNumberUpdate.bindLong(2, contactId); - hasPhoneNumberUpdate.bindLong(3, contactId); - hasPhoneNumberUpdate.execute(); - } finally { - hasPhoneNumberUpdate.close(); - } - } - - private interface LookupKeyQuery { - String TABLE = Views.RAW_CONTACTS; - String[] COLUMNS = new String[] { - RawContacts._ID, - RawContactsColumns.DISPLAY_NAME, - RawContacts.ACCOUNT_TYPE_AND_DATA_SET, - RawContacts.ACCOUNT_NAME, - RawContacts.SOURCE_ID, - }; - - int ID = 0; - int DISPLAY_NAME = 1; - int ACCOUNT_TYPE_AND_DATA_SET = 2; - int ACCOUNT_NAME = 3; - int SOURCE_ID = 4; - } - - public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) { - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - updateLookupKeyForContact(db, contactId); - } - - private void updateLookupKeyForContact(SQLiteDatabase db, long contactId) { - String lookupKey = computeLookupKeyForContact(db, contactId); - - if (lookupKey == null) { - mLookupKeyUpdate.bindNull(1); - } else { - mLookupKeyUpdate.bindString(1, Uri.encode(lookupKey)); - } - mLookupKeyUpdate.bindLong(2, contactId); - - mLookupKeyUpdate.execute(); - } - - protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) { - StringBuilder sb = new StringBuilder(); - mSelectionArgs1[0] = String.valueOf(contactId); - final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS, - RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID); - try { - while (c.moveToNext()) { - ContactLookupKey.appendToLookupKey(sb, - c.getString(LookupKeyQuery.ACCOUNT_TYPE_AND_DATA_SET), - c.getString(LookupKeyQuery.ACCOUNT_NAME), - c.getLong(LookupKeyQuery.ID), - c.getString(LookupKeyQuery.SOURCE_ID), - c.getString(LookupKeyQuery.DISPLAY_NAME)); - } - } finally { - c.close(); - } - return sb.length() == 0 ? null : sb.toString(); - } - - /** - * Execute {@link SQLiteStatement} that will update the - * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}. - */ - public void updateStarred(long rawContactId) { - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - mStarredUpdate.bindLong(1, contactId); - mStarredUpdate.execute(); - } - - /** - * Execute {@link SQLiteStatement} that will update the - * {@link Contacts#PINNED} flag for the given {@link RawContacts#_ID}. - */ - public void updatePinned(long rawContactId) { - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - mPinnedUpdate.bindLong(1, contactId); - mPinnedUpdate.execute(); - } - - /** - * Finds matching contacts and returns a cursor on those. - */ - public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb, - String[] projection, long contactId, int maxSuggestions, String filter, - ArrayList<AggregationSuggestionParameter> parameters) { - final SQLiteDatabase db = mDbHelper.getReadableDatabase(); - db.beginTransaction(); - try { - List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters); - return queryMatchingContacts(qb, db, projection, bestMatches, maxSuggestions, filter); - } finally { - db.endTransaction(); - } - } - - private interface ContactIdQuery { - String[] COLUMNS = new String[] { - Contacts._ID - }; - - int _ID = 0; - } - - /** - * Loads contacts with specified IDs and returns them in the order of IDs in the - * supplied list. - */ - private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db, - String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) { - StringBuilder sb = new StringBuilder(); - sb.append(Contacts._ID); - sb.append(" IN ("); - for (int i = 0; i < bestMatches.size(); i++) { - MatchScore matchScore = bestMatches.get(i); - if (i != 0) { - sb.append(","); - } - sb.append(matchScore.getContactId()); - } - sb.append(")"); - - if (!TextUtils.isEmpty(filter)) { - sb.append(" AND " + Contacts._ID + " IN "); - mContactsProvider.appendContactFilterAsNestedQuery(sb, filter); - } - - // Run a query and find ids of best matching contacts satisfying the filter (if any) - HashSet<Long> foundIds = new HashSet<Long>(); - Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(), - null, null, null, null); - try { - while(cursor.moveToNext()) { - foundIds.add(cursor.getLong(ContactIdQuery._ID)); - } - } finally { - cursor.close(); - } - - // Exclude all contacts that did not match the filter - Iterator<MatchScore> iter = bestMatches.iterator(); - while (iter.hasNext()) { - long id = iter.next().getContactId(); - if (!foundIds.contains(id)) { - iter.remove(); - } - } - - // Limit the number of returned suggestions - final List<MatchScore> limitedMatches; - if (bestMatches.size() > maxSuggestions) { - limitedMatches = bestMatches.subList(0, maxSuggestions); - } else { - limitedMatches = bestMatches; - } - - // Build an in-clause with the remaining contact IDs - sb.setLength(0); - sb.append(Contacts._ID); - sb.append(" IN ("); - for (int i = 0; i < limitedMatches.size(); i++) { - MatchScore matchScore = limitedMatches.get(i); - if (i != 0) { - sb.append(","); - } - sb.append(matchScore.getContactId()); - } - sb.append(")"); - - // Run the final query with the required projection and contact IDs found by the first query - cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID); - - // Build a sorted list of discovered IDs - ArrayList<Long> sortedContactIds = new ArrayList<Long>(limitedMatches.size()); - for (MatchScore matchScore : limitedMatches) { - sortedContactIds.add(matchScore.getContactId()); - } - - Collections.sort(sortedContactIds); - - // Map cursor indexes according to the descending order of match scores - int[] positionMap = new int[limitedMatches.size()]; - for (int i = 0; i < positionMap.length; i++) { - long id = limitedMatches.get(i).getContactId(); - positionMap[i] = sortedContactIds.indexOf(id); - } - - return new ReorderingCursorWrapper(cursor, positionMap); - } - /** * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the * descending order of match score. * @param parameters */ - private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId, + protected List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId, ArrayList<AggregationSuggestionParameter> parameters) { MatchCandidateList candidates = new MatchCandidateList(); diff --git a/src/com/android/providers/contacts/aggregation/ContactAggregator2.java b/src/com/android/providers/contacts/aggregation/ContactAggregator2.java index 879dcbc..ce54bec 100644 --- a/src/com/android/providers/contacts/aggregation/ContactAggregator2.java +++ b/src/com/android/providers/contacts/aggregation/ContactAggregator2.java @@ -16,273 +16,60 @@ package com.android.providers.contacts.aggregation; -import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_PRIMARY; -import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SECONDARY; -import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SUGGEST; - import android.database.Cursor; -import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.database.sqlite.SQLiteStatement; -import android.net.Uri; import android.provider.ContactsContract.AggregationExceptions; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.Identity; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.Photo; -import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Contacts.AggregationSuggestions; import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.DisplayNameSources; import android.provider.ContactsContract.FullNameStyle; import android.provider.ContactsContract.PhotoFiles; -import android.provider.ContactsContract.PinnedPositions; import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.StatusUpdates; import android.text.TextUtils; -import android.util.EventLog; import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.providers.contacts.ContactLookupKey; import com.android.providers.contacts.ContactsDatabaseHelper; -import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; -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.StatusUpdatesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; -import com.android.providers.contacts.ContactsDatabaseHelper.Views; import com.android.providers.contacts.ContactsProvider2; -import com.android.providers.contacts.NameLookupBuilder; -import com.android.providers.contacts.NameNormalizer; import com.android.providers.contacts.NameSplitter; import com.android.providers.contacts.PhotoPriorityResolver; -import com.android.providers.contacts.ReorderingCursorWrapper; import com.android.providers.contacts.TransactionContext; import com.android.providers.contacts.aggregation.util.CommonNicknameCache; import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper; +import com.android.providers.contacts.aggregation.util.MatchScore; import com.android.providers.contacts.aggregation.util.RawContactMatcher; -import com.android.providers.contacts.aggregation.util.RawContactMatcher.MatchScore; import com.android.providers.contacts.aggregation.util.RawContactMatchingCandidates; import com.android.providers.contacts.database.ContactsTableUtil; -import com.android.providers.contacts.util.Clock; - -import com.google.android.collect.Maps; import com.google.android.collect.Sets; -import com.google.common.collect.Multimap; -import com.google.common.collect.HashMultimap; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; +import static com.android.providers.contacts.aggregation.util.RawContactMatcher + .SCORE_THRESHOLD_PRIMARY; +import static com.android.providers.contacts.aggregation.util.RawContactMatcher + .SCORE_THRESHOLD_SECONDARY; +import static com.android.providers.contacts.aggregation.util.RawContactMatcher + .SCORE_THRESHOLD_SUGGEST; + /** * ContactAggregator2 deals with aggregating contact information with sufficient matching data * points. E.g., two John Doe contacts with same phone numbers are presumed to be the same * person unless the user declares otherwise. */ -public class ContactAggregator2 { - - private static final String TAG = "ContactAggregator2"; - - private static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG); - private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); - - private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL = - NameLookupColumns.NAME_TYPE + " IN (" - + NameLookupType.NAME_EXACT + "," - + NameLookupType.NAME_VARIANT + "," - + NameLookupType.NAME_COLLATION_KEY + ")"; - - - /** - * SQL statement that sets the {@link ContactsColumns#LAST_STATUS_UPDATE_ID} column - * on the contact to point to the latest social status update. - */ - private static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL = - "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 + "=?"; - - // From system/core/logcat/event-log-tags - // aggregator [time, count] will be logged for each aggregator cycle. - // For the query (as opposed to the merge), count will be negative - public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747; - - // If we encounter more than this many contacts with matching names, aggregate only this many - private static final int PRIMARY_HIT_LIMIT = 15; - private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT); - - // If we encounter more than this many contacts with matching phone number or email, - // don't attempt to aggregate - this is likely an error or a shared corporate data element. - private static final int SECONDARY_HIT_LIMIT = 20; - private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT); - - // If we encounter no less than this many raw contacts in the best matching contact during - // aggregation, don't attempt to aggregate - this is likely an error or a shared corporate - // data element. - @VisibleForTesting - static final int AGGREGATION_CONTACT_SIZE_LIMIT = 50; - - // If we encounter more than this many contacts with matching name during aggregation - // suggestion lookup, ignore the remaining results. - private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100; +public class ContactAggregator2 extends AbstractContactAggregator { // Possible operation types for contacts aggregation. private static final int CREATE_NEW_CONTACT = 1; private static final int KEEP_INTACT = 0; private static final int RE_AGGREGATE = -1; - private final ContactsProvider2 mContactsProvider; - private final ContactsDatabaseHelper mDbHelper; - private final PhotoPriorityResolver mPhotoPriorityResolver; - private final NameSplitter mNameSplitter; - private final CommonNicknameCache mCommonNicknameCache; - private final MatchCandidateList mCandidates = new MatchCandidateList(); private final RawContactMatcher mMatcher = new RawContactMatcher(); - private final DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate(); - - private boolean mEnabled = true; - - /** Precompiled sql statement for setting an aggregated presence */ - private SQLiteStatement mAggregatedPresenceReplace; - private SQLiteStatement mPresenceContactIdUpdate; - private SQLiteStatement mRawContactCountQuery; - private SQLiteStatement mAggregatedPresenceDelete; - private SQLiteStatement mMarkForAggregation; - private SQLiteStatement mPhotoIdUpdate; - private SQLiteStatement mDisplayNameUpdate; - private SQLiteStatement mLookupKeyUpdate; - private SQLiteStatement mStarredUpdate; - private SQLiteStatement mPinnedUpdate; - private SQLiteStatement mContactIdAndMarkAggregatedUpdate; - private SQLiteStatement mContactIdUpdate; - private SQLiteStatement mMarkAggregatedUpdate; - private SQLiteStatement mContactUpdate; - private SQLiteStatement mContactInsert; - private SQLiteStatement mResetPinnedForRawContact; - - private HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap(); - - private String[] mSelectionArgs1 = new String[1]; - private String[] mSelectionArgs2 = new String[2]; - - private long mMimeTypeIdIdentity; - private long mMimeTypeIdEmail; - private long mMimeTypeIdPhoto; - private long mMimeTypeIdPhone; - private String mRawContactsQueryByRawContactId; - private String mRawContactsQueryByContactId; - private StringBuilder mSb = new StringBuilder(); - - - /** - * Parameter for the suggestion lookup query. - */ - public static final class AggregationSuggestionParameter { - public final String kind; - public final String value; - - public AggregationSuggestionParameter(String kind, String value) { - this.kind = kind; - this.value = value; - } - } - - /** - * Captures a potential match for a given name. The matching algorithm - * constructs a bunch of NameMatchCandidate objects for various potential matches - * and then executes the search in bulk. - */ - private static class NameMatchCandidate { - String mName; - int mLookupType; - - public NameMatchCandidate(String name, int nameLookupType) { - mName = name; - mLookupType = nameLookupType; - } - } - - /** - * A list of {@link NameMatchCandidate} that keeps its elements even when the list is - * truncated. This is done for optimization purposes to avoid excessive object allocation. - */ - private static class MatchCandidateList { - private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>(); - private int mCount; - - /** - * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists. - */ - public void add(String name, int nameLookupType) { - if (mCount >= mList.size()) { - mList.add(new NameMatchCandidate(name, nameLookupType)); - } else { - NameMatchCandidate candidate = mList.get(mCount); - candidate.mName = name; - candidate.mLookupType = nameLookupType; - } - mCount++; - } - - public void clear() { - mCount = 0; - } - - public boolean isEmpty() { - return mCount == 0; - } - } - - /** - * A convenience class used in the algorithm that figures out which of available - * display names to use for an aggregate contact. - */ - private static class DisplayNameCandidate { - long rawContactId; - String displayName; - int displayNameSource; - boolean isNameSuperPrimary; - boolean writableAccount; - - public DisplayNameCandidate() { - clear(); - } - - public void clear() { - rawContactId = -1; - displayName = null; - displayNameSource = DisplayNameSources.UNDEFINED; - isNameSuperPrimary = false; - writableAccount = false; - } - } /** * Constructor. @@ -291,221 +78,8 @@ public class ContactAggregator2 { ContactsDatabaseHelper contactsDatabaseHelper, PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter, CommonNicknameCache commonNicknameCache) { - mContactsProvider = contactsProvider; - mDbHelper = contactsDatabaseHelper; - mPhotoPriorityResolver = photoPriorityResolver; - mNameSplitter = nameSplitter; - mCommonNicknameCache = commonNicknameCache; - - SQLiteDatabase db = mDbHelper.getReadableDatabase(); - - // Since we have no way of determining which custom status was set last, - // we'll just pick one randomly. We are using MAX as an approximation of randomness - final String replaceAggregatePresenceSql = - "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "(" - + AggregatedPresenceColumns.CONTACT_ID + ", " - + StatusUpdates.PRESENCE + ", " - + StatusUpdates.CHAT_CAPABILITY + ")" - + " SELECT " + PresenceColumns.CONTACT_ID + "," - + StatusUpdates.PRESENCE + "," - + StatusUpdates.CHAT_CAPABILITY - + " FROM " + Tables.PRESENCE - + " WHERE " - + " (" + StatusUpdates.PRESENCE - + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" - + " = (SELECT " - + "MAX (" + StatusUpdates.PRESENCE - + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" - + " FROM " + Tables.PRESENCE - + " WHERE " + PresenceColumns.CONTACT_ID - + "=?)" - + " AND " + PresenceColumns.CONTACT_ID - + "=?;"; - mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql); - - mRawContactCountQuery = db.compileStatement( - "SELECT COUNT(" + RawContacts._ID + ")" + - " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContacts.CONTACT_ID + "=?" - + " AND " + RawContacts._ID + "<>?"); - - mAggregatedPresenceDelete = db.compileStatement( - "DELETE FROM " + Tables.AGGREGATED_PRESENCE + - " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?"); - - mMarkForAggregation = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" + - " WHERE " + RawContacts._ID + "=?" - + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"); - - mPhotoIdUpdate = db.compileStatement( - "UPDATE " + Tables.CONTACTS + - " SET " + Contacts.PHOTO_ID + "=?," + Contacts.PHOTO_FILE_ID + "=? " + - " WHERE " + Contacts._ID + "=?"); - - mDisplayNameUpdate = db.compileStatement( - "UPDATE " + Tables.CONTACTS + - " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " + - " WHERE " + Contacts._ID + "=?"); - - mLookupKeyUpdate = db.compileStatement( - "UPDATE " + Tables.CONTACTS + - " SET " + Contacts.LOOKUP_KEY + "=? " + - " WHERE " + Contacts._ID + "=?"); - - mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET " - + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED - + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE " - + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND " - + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?"); - - mPinnedUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET " - + Contacts.PINNED + " = IFNULL((SELECT MIN(" + RawContacts.PINNED + ") FROM " - + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=" - + ContactsColumns.CONCRETE_ID + " AND " + RawContacts.PINNED + ">" - + PinnedPositions.UNPINNED + ")," + PinnedPositions.UNPINNED + ") " - + "WHERE " + Contacts._ID + "=?"); - - mContactIdAndMarkAggregatedUpdate = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContacts.CONTACT_ID + "=?, " - + RawContactsColumns.AGGREGATION_NEEDED + "=0" + - " WHERE " + RawContacts._ID + "=?"); - - mContactIdUpdate = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContacts.CONTACT_ID + "=?" + - " WHERE " + RawContacts._ID + "=?"); - - mMarkAggregatedUpdate = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" + - " WHERE " + RawContacts._ID + "=?"); - - mPresenceContactIdUpdate = db.compileStatement( - "UPDATE " + Tables.PRESENCE + - " SET " + PresenceColumns.CONTACT_ID + "=?" + - " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?"); - - mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL); - mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL); - - mResetPinnedForRawContact = db.compileStatement( - "UPDATE " + Tables.RAW_CONTACTS + - " SET " + RawContacts.PINNED + "=" + PinnedPositions.UNPINNED + - " WHERE " + RawContacts._ID + "=?"); - - mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); - mMimeTypeIdIdentity = mDbHelper.getMimeTypeId(Identity.CONTENT_ITEM_TYPE); - mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE); - mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); - - // Query used to retrieve data from raw contacts to populate the corresponding aggregate - mRawContactsQueryByRawContactId = String.format(Locale.US, - RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID, - mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone); - - mRawContactsQueryByContactId = String.format(Locale.US, - RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID, - mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone); - } - - public void setEnabled(boolean enabled) { - mEnabled = enabled; - } - - public boolean isEnabled() { - return mEnabled; - } - - private interface AggregationQuery { - String SQL = - "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID + - ", " + RawContactsColumns.ACCOUNT_ID + - " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContacts._ID + " IN("; - - int _ID = 0; - int CONTACT_ID = 1; - int ACCOUNT_ID = 2; - } - - /** - * Aggregate all raw contacts that were marked for aggregation in the current transaction. - * Call just before committing the transaction. - */ - public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) { - final int markedCount = mRawContactsMarkedForAggregation.size(); - if (markedCount == 0) { - return; - } - - final long start = System.currentTimeMillis(); - if (DEBUG_LOGGING) { - Log.d(TAG, "aggregateInTransaction for " + markedCount + " contacts"); - } - - EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -markedCount); - - int index = 0; - - // We don't use the cached string builder (namely mSb) here, as this string can be very - // long when upgrading (where we re-aggregate all visible contacts) and StringBuilder won't - // shrink the internal storage. - // Note: don't use selection args here. We just include all IDs directly in the selection, - // because there's a limit for the number of parameters in a query. - final StringBuilder sbQuery = new StringBuilder(); - sbQuery.append(AggregationQuery.SQL); - for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) { - if (index > 0) { - sbQuery.append(','); - } - sbQuery.append(rawContactId); - index++; - } - - sbQuery.append(')'); - - final long[] rawContactIds; - final long[] contactIds; - final long[] accountIds; - final int actualCount; - final Cursor c = db.rawQuery(sbQuery.toString(), null); - try { - actualCount = c.getCount(); - rawContactIds = new long[actualCount]; - contactIds = new long[actualCount]; - accountIds = new long[actualCount]; - - index = 0; - while (c.moveToNext()) { - rawContactIds[index] = c.getLong(AggregationQuery._ID); - contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID); - accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID); - index++; - } - } finally { - c.close(); - } - - if (DEBUG_LOGGING) { - Log.d(TAG, "aggregateInTransaction: initial query done."); - } - - for (int i = 0; i < actualCount; i++) { - aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i], - mCandidates, mMatcher); - } - - long elapsedTime = System.currentTimeMillis() - start; - EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, actualCount); - - if (DEBUG_LOGGING) { - Log.d(TAG, "Contact aggregation complete: " + actualCount + - (actualCount == 0 ? "" : ", " + (elapsedTime / actualCount) - + " ms per raw contact")); - } + super(contactsProvider, contactsDatabaseHelper, photoPriorityResolver, nameSplitter, + commonNicknameCache); } @SuppressWarnings("deprecation") @@ -540,35 +114,6 @@ public class ContactAggregator2 { } } - public void clearPendingAggregations() { - // HashMap woulnd't shrink the internal table once expands it, so let's just re-create - // a new one instead of clear()ing it. - mRawContactsMarkedForAggregation = Maps.newHashMap(); - } - - public void markNewForAggregation(long rawContactId, int aggregationMode) { - mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode); - } - - public void markForAggregation(long rawContactId, int aggregationMode, boolean force) { - final int effectiveAggregationMode; - if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) { - // As per ContactsContract documentation, default aggregation mode - // does not override a previously set mode - if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { - effectiveAggregationMode = mRawContactsMarkedForAggregation.get(rawContactId); - } else { - effectiveAggregationMode = aggregationMode; - } - } else { - mMarkForAggregation.bindLong(1, rawContactId); - mMarkForAggregation.execute(); - effectiveAggregationMode = aggregationMode; - } - - mRawContactsMarkedForAggregation.put(rawContactId, effectiveAggregationMode); - } - private static class RawContactIdAndAggregationModeQuery { public static final String TABLE = Tables.RAW_CONTACTS; @@ -581,168 +126,14 @@ public class ContactAggregator2 { } /** - * Marks all constituent raw contacts of an aggregated contact for re-aggregation. - */ - private void markContactForAggregation(SQLiteDatabase db, long contactId) { - mSelectionArgs1[0] = String.valueOf(contactId); - Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE, - RawContactIdAndAggregationModeQuery.COLUMNS, - RawContactIdAndAggregationModeQuery.SELECTION, mSelectionArgs1, null, null, null); - try { - if (cursor.moveToFirst()) { - long rawContactId = cursor.getLong(RawContactIdAndAggregationModeQuery._ID); - int aggregationMode = cursor.getInt( - RawContactIdAndAggregationModeQuery.AGGREGATION_MODE); - // Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED. - // (Also just ignore deprecated AGGREGATION_MODE_IMMEDIATE) - if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { - markForAggregation(rawContactId, aggregationMode, true); - } - } - } finally { - cursor.close(); - } - } - - /** - * Mark all visible contacts for re-aggregation. - * - * - Set {@link RawContactsColumns#AGGREGATION_NEEDED} For all visible raw_contacts with - * {@link RawContacts#AGGREGATION_MODE_DEFAULT}. - * - Also put them into {@link #mRawContactsMarkedForAggregation}. - */ - public int markAllVisibleForAggregation(SQLiteDatabase db) { - final long start = System.currentTimeMillis(); - - // Set AGGREGATION_NEEDED for all visible raw_cotnacts with AGGREGATION_MODE_DEFAULT. - // (Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED) - db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " + - RawContactsColumns.AGGREGATION_NEEDED + "=1" + - " WHERE " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + - " AND " + RawContacts.AGGREGATION_MODE + "=" + RawContacts.AGGREGATION_MODE_DEFAULT - ); - - final int count; - final Cursor cursor = db.rawQuery("SELECT " + RawContacts._ID + - " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1", null); - try { - count = cursor.getCount(); - cursor.moveToPosition(-1); - while (cursor.moveToNext()) { - final long rawContactId = cursor.getLong(0); - mRawContactsMarkedForAggregation.put(rawContactId, - RawContacts.AGGREGATION_MODE_DEFAULT); - } - } finally { - cursor.close(); - } - - final long end = System.currentTimeMillis(); - Log.i(TAG, "Marked all visible contacts for aggregation: " + count + " raw contacts, " + - (end - start) + " ms"); - return count; - } - - /** - * Creates a new contact based on the given raw contact. Does not perform aggregation. Returns - * the ID of the contact that was created. - */ - public long onRawContactInsert( - TransactionContext txContext, SQLiteDatabase db, long rawContactId) { - long contactId = insertContact(db, rawContactId); - setContactId(rawContactId, contactId); - mDbHelper.updateContactVisible(txContext, contactId); - return contactId; - } - - protected long insertContact(SQLiteDatabase db, long rawContactId) { - mSelectionArgs1[0] = String.valueOf(rawContactId); - computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert); - return mContactInsert.executeInsert(); - } - - private static final class RawContactIdAndAccountQuery { - public static final String TABLE = Tables.RAW_CONTACTS; - - public static final String[] COLUMNS = { - RawContacts.CONTACT_ID, - RawContactsColumns.ACCOUNT_ID - }; - - public static final String SELECTION = RawContacts._ID + "=?"; - - public static final int CONTACT_ID = 0; - public static final int ACCOUNT_ID = 1; - } - - public void aggregateContact( - TransactionContext txContext, SQLiteDatabase db, long rawContactId) { - if (!mEnabled) { - return; - } - - MatchCandidateList candidates = new MatchCandidateList(); - RawContactMatcher matcher = new RawContactMatcher(); - - long contactId = 0; - long accountId = 0; - mSelectionArgs1[0] = String.valueOf(rawContactId); - Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE, - RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION, - mSelectionArgs1, null, null, null); - try { - if (cursor.moveToFirst()) { - contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID); - accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID); - } - } finally { - cursor.close(); - } - - aggregateContact(txContext, db, rawContactId, accountId, contactId, - candidates, matcher); - } - - public void updateAggregateData(TransactionContext txContext, long contactId) { - if (!mEnabled) { - return; - } - - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - computeAggregateData(db, contactId, mContactUpdate); - mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId); - mContactUpdate.execute(); - - mDbHelper.updateContactVisible(txContext, contactId); - updateAggregatedStatusUpdate(contactId); - } - - private void updateAggregatedStatusUpdate(long contactId) { - mAggregatedPresenceReplace.bindLong(1, contactId); - mAggregatedPresenceReplace.bindLong(2, contactId); - mAggregatedPresenceReplace.execute(); - updateLastStatusUpdateId(contactId); - } - - /** - * Adjusts the reference to the latest status update for the specified contact. - */ - public void updateLastStatusUpdateId(long contactId) { - String contactIdString = String.valueOf(contactId); - mDbHelper.getWritableDatabase().execSQL(UPDATE_LAST_STATUS_UPDATE_ID_SQL, - new String[]{contactIdString, contactIdString}); - } - - /** * Given a specific raw contact, finds all matching raw contacts and re-aggregate them * based on the matching connectivity. */ - private synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db, - long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates, - RawContactMatcher matcher) { + synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db, + long rawContactId, long accountId, long currentContactId, + MatchCandidateList candidates) { - if (VERBOSE_LOGGING) { + if (VERBOSE_LOGGING) { Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId); } @@ -753,6 +144,7 @@ public class ContactAggregator2 { aggregationMode = aggModeObject; } + RawContactMatcher matcher = new RawContactMatcher(); RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates(); if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { // Find the set of matching candidates @@ -864,7 +256,7 @@ public class ContactAggregator2 { * Clear the is_super_primary settings for these mime-types. * {@code rawContactIds} should be a comma separated ID list. */ - private void clearSuperPrimarySetting(SQLiteDatabase db, String rawContactIds) { + private void clearSuperPrimarySetting(SQLiteDatabase db, String rawContactIds) { final String sql = "SELECT d." + DataColumns.MIMETYPE_ID + ", count(DISTINCT r." + RawContacts.CONTACT_ID + ") c FROM " + Tables.DATA + " d JOIN " + @@ -909,72 +301,6 @@ public class ContactAggregator2 { db.execSQL(superPrimaryUpdateSql, null); } - private interface RawContactMatchingSelectionStatement { - String SELECT_COUNT = "SELECT count(*) " ; - String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2." + Data.RAW_CONTACT_ID ; - } - - /** - * Build sql to check if there is any identity match/mis-match between two sets of raw contact - * ids on the same namespace. - */ - private String buildIdentityMatchingSql(String rawContactIdSet1, String rawContactIdSet2, - boolean isIdentityMatching, boolean countOnly) { - final String identityType = String.valueOf(mMimeTypeIdIdentity); - final String matchingOperator = (isIdentityMatching) ? "=" : "!="; - final String sql = - " FROM " + Tables.DATA + " AS d1" + - " JOIN " + Tables.DATA + " AS d2" + - " ON (d1." + Identity.IDENTITY + matchingOperator + - " d2." + Identity.IDENTITY + " AND" + - " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" + - " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + identityType + - " AND d2." + DataColumns.MIMETYPE_ID + " = " + identityType + - " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" + - " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")"; - return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql : - RawContactMatchingSelectionStatement.SELECT_ID + sql; - } - - private String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2, - boolean countOnly) { - final String emailType = String.valueOf(mMimeTypeIdEmail); - final String sql = - " FROM " + Tables.DATA + " AS d1" + - " JOIN " + Tables.DATA + " AS d2" + - " ON lower(d1." + Email.ADDRESS + ")= lower(d2." + Email.ADDRESS + ")" + - " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + emailType + - " AND d2." + DataColumns.MIMETYPE_ID + " = " + emailType + - " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" + - " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")"; - return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql : - RawContactMatchingSelectionStatement.SELECT_ID + sql; - } - - private String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2, - boolean countOnly) { - // It's a bit tricker because it has to be consistent with - // updateMatchScoresBasedOnPhoneMatches(). - final String phoneType = String.valueOf(mMimeTypeIdPhone); - final String sql = - " FROM " + Tables.PHONE_LOOKUP + " AS p1" + - " JOIN " + Tables.DATA + " AS d1 ON " + - "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" + - " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH + - "=p2." + PhoneLookupColumns.MIN_MATCH + ")" + - " JOIN " + Tables.DATA + " AS d2 ON " + - "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" + - " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + phoneType + - " AND d2." + DataColumns.MIMETYPE_ID + " = " + phoneType + - " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" + - " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")" + - " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + "," + - String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter()) + - ")"; - return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql : - RawContactMatchingSelectionStatement.SELECT_ID + sql; - } - private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2, int aggregationType, boolean countOnly) { final String idPairSelection = "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " + @@ -989,10 +315,6 @@ public class ContactAggregator2 { idPairSelection + sql; } - private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) { - return DatabaseUtils.longForQuery(db, query, null) > 0; - } - /** * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of * {@code matchingCandidates} into connected components. This only happens when a given @@ -1121,84 +443,6 @@ public class ContactAggregator2 { } /** - * Partition the given raw contact Ids to connected component based on aggregation exception, - * identity matching, email matching or phone matching. - */ - private Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long> rawContactIdSet) { - // Connections between two raw contacts - final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create(); - String rawContactIds = TextUtils.join(",", rawContactIdSet); - findIdPairs(db, buildExceptionMatchingSql(rawContactIds, rawContactIds, - AggregationExceptions.TYPE_KEEP_TOGETHER, /* countOnly =*/false), - matchingRawIdPairs); - findIdPairs(db, buildIdentityMatchingSql(rawContactIds, rawContactIds, - /* isIdentityMatching =*/ true, /* countOnly =*/false), matchingRawIdPairs); - findIdPairs(db, buildEmailMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false), - matchingRawIdPairs); - findIdPairs(db, buildPhoneMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false), - matchingRawIdPairs); - - return ContactAggregatorHelper.findConnectedComponents(rawContactIdSet, matchingRawIdPairs); - } - - /** - * Given a query which will return two non-null IDs in the first two columns as results, this - * method will put two entries into the given result map for each pair of different IDs, one - * keyed by each ID. - */ - private void findIdPairs(SQLiteDatabase db, String query, Multimap<Long, Long> results) { - Cursor cursor = db.rawQuery(query, null); - try { - cursor.moveToPosition(-1); - while (cursor.moveToNext()) { - long idA = cursor.getLong(0); - long idB = cursor.getLong(1); - if (idA != idB) { - results.put(idA, idB); - results.put(idB, idA); - } - } - } finally { - cursor.close(); - } - } - - /** - * Creates a new Contact for a given set of the raw contacts of {@code rawContactIds} if the - * given contactId is null. Otherwise, regroup them into contact with {@code contactId}. - */ - private void createContactForRawContacts(SQLiteDatabase db, TransactionContext txContext, - Set<Long> rawContactIds, Long contactId) { - if (rawContactIds.isEmpty()) { - // No raw contact id is provided. - return; - } - - // If contactId is not provided, generates a new one. - if (contactId == null) { - mSelectionArgs1[0]= String.valueOf(rawContactIds.iterator().next()); - computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, - mContactInsert); - contactId = mContactInsert.executeInsert(); - } - for (Long rawContactId : rawContactIds) { - // Regrouped contacts should automatically be unpinned. - unpinRawContact(rawContactId); - setContactIdAndMarkAggregated(rawContactId, contactId); - setPresenceContactId(rawContactId, contactId); - } - updateAggregateData(txContext, contactId); - } - - private static class RawContactIdQuery { - public static final String TABLE = Tables.RAW_CONTACTS; - public static final String[] COLUMNS = {RawContacts._ID, RawContactsColumns.ACCOUNT_ID }; - public static final String SELECTION = RawContacts.CONTACT_ID + "=?"; - public static final int RAW_CONTACT_ID = 0; - public static final int ACCOUNT_ID = 1; - } - - /** * Ensures that automatic aggregation rules are followed after a contact * becomes visible or invisible. Specifically, consider this case: there are * three contacts named Foo. Two of them come from account A1 and one comes @@ -1254,119 +498,6 @@ public class ContactAggregator2 { } /** - * Updates the contact ID for the specified contact. - */ - protected void setContactId(long rawContactId, long contactId) { - mContactIdUpdate.bindLong(1, contactId); - mContactIdUpdate.bindLong(2, rawContactId); - mContactIdUpdate.execute(); - } - - /** - * Marks the specified raw contact ID as aggregated - */ - private void markAggregated(long rawContactId) { - mMarkAggregatedUpdate.bindLong(1, rawContactId); - mMarkAggregatedUpdate.execute(); - } - - /** - * Updates the contact ID for the specified contact and marks the raw contact as aggregated. - */ - private void setContactIdAndMarkAggregated(long rawContactId, long contactId) { - mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId); - mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId); - mContactIdAndMarkAggregatedUpdate.execute(); - } - - private void setPresenceContactId(long rawContactId, long contactId) { - mPresenceContactIdUpdate.bindLong(1, contactId); - mPresenceContactIdUpdate.bindLong(2, rawContactId); - mPresenceContactIdUpdate.execute(); - } - - private void unpinRawContact(long rawContactId) { - mResetPinnedForRawContact.bindLong(1, rawContactId); - mResetPinnedForRawContact.execute(); - } - - interface AggregateExceptionPrefetchQuery { - String TABLE = Tables.AGGREGATION_EXCEPTIONS; - - String[] COLUMNS = { - AggregationExceptions.RAW_CONTACT_ID1, - AggregationExceptions.RAW_CONTACT_ID2, - }; - - int RAW_CONTACT_ID1 = 0; - int RAW_CONTACT_ID2 = 1; - } - - // A set of raw contact IDs for which there are aggregation exceptions - private final HashSet<Long> mAggregationExceptionIds = new HashSet<>(); - private boolean mAggregationExceptionIdsValid; - - public void invalidateAggregationExceptionCache() { - mAggregationExceptionIdsValid = false; - } - - /** - * Finds all raw contact IDs for which there are aggregation exceptions. The list of - * ids is used as an optimization in aggregation: there is no point to run a query against - * the agg_exceptions table if it is known that there are no records there for a given - * raw contact ID. - */ - private void prefetchAggregationExceptionIds(SQLiteDatabase db) { - mAggregationExceptionIds.clear(); - final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE, - AggregateExceptionPrefetchQuery.COLUMNS, - null, null, null, null, null); - - try { - while (c.moveToNext()) { - long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1); - long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2); - mAggregationExceptionIds.add(rawContactId1); - mAggregationExceptionIds.add(rawContactId2); - } - } finally { - c.close(); - } - - mAggregationExceptionIdsValid = true; - } - - interface AggregateExceptionQuery { - String TABLE = Tables.AGGREGATION_EXCEPTIONS - + " JOIN raw_contacts raw_contacts1 " - + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) " - + " JOIN raw_contacts raw_contacts2 " - + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) "; - - String[] COLUMNS = { - AggregationExceptions.TYPE, - AggregationExceptions.RAW_CONTACT_ID1, - "raw_contacts1." + RawContacts.CONTACT_ID, - "raw_contacts1." + RawContactsColumns.ACCOUNT_ID, - "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED, - AggregationExceptions.RAW_CONTACT_ID2, - "raw_contacts2." + RawContacts.CONTACT_ID, - "raw_contacts2." + RawContactsColumns.ACCOUNT_ID, - "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED, - }; - - int TYPE = 0; - int RAW_CONTACT_ID1 = 1; - int CONTACT_ID1 = 2; - int ACCOUNT_ID1 = 3; - int AGGREGATION_NEEDED_1 = 4; - int RAW_CONTACT_ID2 = 5; - int CONTACT_ID2 = 6; - int ACCOUNT_ID2 = 7; - int AGGREGATION_NEEDED_2 = 8; - } - - /** * Computes match scores based on exceptions entered by the user: always match and never match. */ private void updateMatchScoresBasedOnExceptions(SQLiteDatabase db, long rawContactId, @@ -1422,69 +553,6 @@ public class ContactAggregator2 { } } - - private interface NameLookupQuery { - String TABLE = Tables.NAME_LOOKUP; - - String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?"; - String SELECTION_STRUCTURED_NAME_BASED = - SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL; - - String[] COLUMNS = new String[] { - NameLookupColumns.NORMALIZED_NAME, - NameLookupColumns.NAME_TYPE - }; - - int NORMALIZED_NAME = 0; - int NAME_TYPE = 1; - } - - private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId, - MatchCandidateList candidates, boolean structuredNameBased) { - candidates.clear(); - mSelectionArgs1[0] = String.valueOf(rawContactId); - Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS, - structuredNameBased - ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED - : NameLookupQuery.SELECTION, - mSelectionArgs1, null, null, null); - try { - while (c.moveToNext()) { - String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME); - int type = c.getInt(NameLookupQuery.NAME_TYPE); - candidates.add(normalizedName, type); - } - } finally { - c.close(); - } - } - - private interface IdentityLookupMatchQuery { - final String TABLE = Tables.DATA + " dataA" - + " JOIN " + Tables.DATA + " dataB" + - " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE + - " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")" - + " JOIN " + Tables.RAW_CONTACTS + - " ON (dataB." + Data.RAW_CONTACT_ID + " = " - + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - - final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1" - + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2" - + " AND dataA." + Identity.NAMESPACE + " NOT NULL" - + " AND dataA." + Identity.IDENTITY + " NOT NULL" - + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2" - + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" - + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; - - final String[] COLUMNS = new String[] { - RawContacts._ID, RawContacts.CONTACT_ID, RawContactsColumns.ACCOUNT_ID - }; - - int RAW_CONTACT_ID = 0; - int CONTACT_ID = 1; - int ACCOUNT_ID = 2; - } - /** * Finds contacts with exact identity matches to the the specified raw contact. */ @@ -1505,37 +573,6 @@ public class ContactAggregator2 { } finally { c.close(); } - - } - - private interface NameLookupMatchQuery { - String TABLE = Tables.NAME_LOOKUP + " nameA" - + " JOIN " + Tables.NAME_LOOKUP + " nameB" + - " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "=" - + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")" - + " JOIN " + Tables.RAW_CONTACTS + - " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = " - + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - - String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?" - + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" - + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; - - String[] COLUMNS = new String[] { - RawContacts._ID, - RawContacts.CONTACT_ID, - RawContactsColumns.ACCOUNT_ID, - "nameA." + NameLookupColumns.NORMALIZED_NAME, - "nameA." + NameLookupColumns.NAME_TYPE, - "nameB." + NameLookupColumns.NAME_TYPE, - }; - - int RAW_CONTACT_ID = 0; - int CONTACT_ID = 1; - int ACCOUNT_ID = 2; - int NAME = 3; - int NAME_TYPE_A = 4; - int NAME_TYPE_B = 5; } /** @@ -1567,70 +604,22 @@ public class ContactAggregator2 { } } - private interface NameLookupMatchQueryWithParameter { - String TABLE = Tables.NAME_LOOKUP - + " JOIN " + Tables.RAW_CONTACTS + - " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = " - + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - - String[] COLUMNS = new String[] { - RawContacts._ID, - RawContacts.CONTACT_ID, - RawContactsColumns.ACCOUNT_ID, - NameLookupColumns.NORMALIZED_NAME, - NameLookupColumns.NAME_TYPE, - }; - - int RAW_CONTACT_ID = 0; - int CONTACT_ID = 1; - int ACCOUNT_ID = 2; - int NAME = 3; - int NAME_TYPE = 4; - } - - private final class NameLookupSelectionBuilder extends NameLookupBuilder { - - private final MatchCandidateList mNameLookupCandidates; - - private StringBuilder mSelection = new StringBuilder( - NameLookupColumns.NORMALIZED_NAME + " IN("); - - - public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) { - super(splitter); - this.mNameLookupCandidates = candidates; - } - - @Override - protected String[] getCommonNicknameClusters(String normalizedName) { - return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); - } - - @Override - protected void insertNameLookup( - long rawContactId, long dataId, int lookupType, String string) { - mNameLookupCandidates.add(string, lookupType); - DatabaseUtils.appendEscapedSQLString(mSelection, string); - mSelection.append(','); - } - - public boolean isEmpty() { - return mNameLookupCandidates.isEmpty(); - } - - public String getSelection() { - mSelection.setLength(mSelection.length() - 1); // Strip last comma - mSelection.append(')'); - return mSelection.toString(); - } - - public int getLookupType(String name) { - for (int i = 0; i < mNameLookupCandidates.mCount; i++) { - if (mNameLookupCandidates.mList.get(i).mName.equals(name)) { - return mNameLookupCandidates.mList.get(i).mLookupType; - } + private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, + RawContactMatcher matcher) { + mSelectionArgs2[0] = String.valueOf(rawContactId); + mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail); + Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS, + EmailLookupQuery.SELECTION, + mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING); + try { + while (c.moveToNext()) { + long rId = c.getLong(EmailLookupQuery.RAW_CONTACT_ID); + long contactId = c.getLong(EmailLookupQuery.CONTACT_ID); + long accountId = c.getLong(EmailLookupQuery.ACCOUNT_ID); + matcher.updateScoreWithEmailMatch(rId, contactId, accountId); } - throw new IllegalStateException(); + } finally { + c.close(); } } @@ -1669,81 +658,6 @@ public class ContactAggregator2 { } } - private interface EmailLookupQuery { - String TABLE = Tables.DATA + " dataA" - + " JOIN " + Tables.DATA + " dataB" + - " ON lower(" + "dataA." + Email.DATA + ")=lower(dataB." + Email.DATA + ")" - + " JOIN " + Tables.RAW_CONTACTS + - " ON (dataB." + Data.RAW_CONTACT_ID + " = " - + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - - String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1" - + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2" - + " AND dataA." + Email.DATA + " NOT NULL" - + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2" - + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" - + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; - - String[] COLUMNS = new String[] { - RawContacts._ID, - RawContacts.CONTACT_ID, - RawContactsColumns.ACCOUNT_ID - }; - - int RAW_CONTACT_ID = 0; - int CONTACT_ID = 1; - int ACCOUNT_ID = 2; - } - - private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, - RawContactMatcher matcher) { - mSelectionArgs2[0] = String.valueOf(rawContactId); - mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail); - Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS, - EmailLookupQuery.SELECTION, - mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING); - try { - while (c.moveToNext()) { - long rId = c.getLong(EmailLookupQuery.RAW_CONTACT_ID); - long contactId = c.getLong(EmailLookupQuery.CONTACT_ID); - long accountId = c.getLong(EmailLookupQuery.ACCOUNT_ID); - matcher.updateScoreWithEmailMatch(rId, contactId, accountId); - } - } finally { - c.close(); - } - } - - private interface PhoneLookupQuery { - String TABLE = Tables.PHONE_LOOKUP + " phoneA" - + " JOIN " + Tables.DATA + " dataA" - + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")" - + " JOIN " + Tables.PHONE_LOOKUP + " phoneB" - + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "=" - + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")" - + " JOIN " + Tables.DATA + " dataB" - + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")" - + " JOIN " + Tables.RAW_CONTACTS - + " ON (dataB." + Data.RAW_CONTACT_ID + " = " - + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - - String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" - + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", " - + "dataB." + Phone.NUMBER + ",?)" - + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" - + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; - - String[] COLUMNS = new String[] { - RawContacts._ID, - RawContacts.CONTACT_ID, - RawContactsColumns.ACCOUNT_ID - }; - - int RAW_CONTACT_ID = 0; - int CONTACT_ID = 1; - int ACCOUNT_ID = 2; - } - private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher) { mSelectionArgs2[0] = String.valueOf(rawContactId); @@ -1840,435 +754,6 @@ public class ContactAggregator2 { } } - private interface RawContactsQuery { - String SQL_FORMAT_HAS_SUPER_PRIMARY_NAME = - " EXISTS(SELECT 1 " + - " FROM " + Tables.DATA + " d " + - " WHERE d." + DataColumns.MIMETYPE_ID + "=%d " + - " AND d." + Data.RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + - " AND d." + Data.IS_SUPER_PRIMARY + "=1)"; - - String SQL_FORMAT = - "SELECT " - + RawContactsColumns.CONCRETE_ID + "," - + RawContactsColumns.DISPLAY_NAME + "," - + RawContactsColumns.DISPLAY_NAME_SOURCE + "," - + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "," - + AccountsColumns.CONCRETE_ACCOUNT_NAME + "," - + AccountsColumns.CONCRETE_DATA_SET + "," - + RawContacts.SOURCE_ID + "," - + RawContacts.CUSTOM_RINGTONE + "," - + RawContacts.SEND_TO_VOICEMAIL + "," - + RawContacts.LAST_TIME_CONTACTED + "," - + RawContacts.TIMES_CONTACTED + "," - + RawContacts.STARRED + "," - + RawContacts.PINNED + "," - + DataColumns.CONCRETE_ID + "," - + DataColumns.CONCRETE_MIMETYPE_ID + "," - + Data.IS_SUPER_PRIMARY + "," - + Photo.PHOTO_FILE_ID + "," - + SQL_FORMAT_HAS_SUPER_PRIMARY_NAME + - " FROM " + Tables.RAW_CONTACTS + - " JOIN " + Tables.ACCOUNTS + " ON (" - + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID - + ")" + - " LEFT OUTER JOIN " + Tables.DATA + - " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID - + " AND ((" + DataColumns.MIMETYPE_ID + "=%d" - + " AND " + Photo.PHOTO + " NOT NULL)" - + " OR (" + DataColumns.MIMETYPE_ID + "=%d" - + " AND " + Phone.NUMBER + " NOT NULL)))"; - - String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT + - " WHERE " + RawContactsColumns.CONCRETE_ID + "=?"; - - String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT + - " WHERE " + RawContacts.CONTACT_ID + "=?" - + " AND " + RawContacts.DELETED + "=0"; - - int RAW_CONTACT_ID = 0; - int DISPLAY_NAME = 1; - int DISPLAY_NAME_SOURCE = 2; - int ACCOUNT_TYPE = 3; - int ACCOUNT_NAME = 4; - int DATA_SET = 5; - int SOURCE_ID = 6; - int CUSTOM_RINGTONE = 7; - int SEND_TO_VOICEMAIL = 8; - int LAST_TIME_CONTACTED = 9; - int TIMES_CONTACTED = 10; - int STARRED = 11; - int PINNED = 12; - int DATA_ID = 13; - int MIMETYPE_ID = 14; - int IS_SUPER_PRIMARY = 15; - int PHOTO_FILE_ID = 16; - int HAS_SUPER_PRIMARY_NAME = 17; - } - - private interface ContactReplaceSqlStatement { - String UPDATE_SQL = - "UPDATE " + Tables.CONTACTS + - " SET " - + Contacts.NAME_RAW_CONTACT_ID + "=?, " - + Contacts.PHOTO_ID + "=?, " - + Contacts.PHOTO_FILE_ID + "=?, " - + Contacts.SEND_TO_VOICEMAIL + "=?, " - + Contacts.CUSTOM_RINGTONE + "=?, " - + Contacts.LAST_TIME_CONTACTED + "=?, " - + Contacts.TIMES_CONTACTED + "=?, " - + Contacts.STARRED + "=?, " - + Contacts.PINNED + "=?, " - + Contacts.HAS_PHONE_NUMBER + "=?, " - + Contacts.LOOKUP_KEY + "=?, " - + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=? " + - " WHERE " + Contacts._ID + "=?"; - - String INSERT_SQL = - "INSERT INTO " + Tables.CONTACTS + " (" - + Contacts.NAME_RAW_CONTACT_ID + ", " - + Contacts.PHOTO_ID + ", " - + Contacts.PHOTO_FILE_ID + ", " - + Contacts.SEND_TO_VOICEMAIL + ", " - + Contacts.CUSTOM_RINGTONE + ", " - + Contacts.LAST_TIME_CONTACTED + ", " - + Contacts.TIMES_CONTACTED + ", " - + Contacts.STARRED + ", " - + Contacts.PINNED + ", " - + Contacts.HAS_PHONE_NUMBER + ", " - + Contacts.LOOKUP_KEY + ", " - + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP - + ") " + - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"; - - int NAME_RAW_CONTACT_ID = 1; - int PHOTO_ID = 2; - int PHOTO_FILE_ID = 3; - int SEND_TO_VOICEMAIL = 4; - int CUSTOM_RINGTONE = 5; - int LAST_TIME_CONTACTED = 6; - int TIMES_CONTACTED = 7; - int STARRED = 8; - int PINNED = 9; - int HAS_PHONE_NUMBER = 10; - int LOOKUP_KEY = 11; - int CONTACT_LAST_UPDATED_TIMESTAMP = 12; - int CONTACT_ID = 13; - } - - /** - * Computes aggregate-level data for the specified aggregate contact ID. - */ - private void computeAggregateData(SQLiteDatabase db, long contactId, - SQLiteStatement statement) { - mSelectionArgs1[0] = String.valueOf(contactId); - computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement); - } - - /** - * Indicates whether the given photo entry and priority gives this photo a higher overall - * priority than the current best photo entry and priority. - */ - private boolean hasHigherPhotoPriority(PhotoEntry photoEntry, int priority, - PhotoEntry bestPhotoEntry, int bestPriority) { - int photoComparison = photoEntry.compareTo(bestPhotoEntry); - return photoComparison < 0 || photoComparison == 0 && priority > bestPriority; - } - - /** - * Computes aggregate-level data from constituent raw contacts. - */ - private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs, - SQLiteStatement statement) { - long currentRawContactId = -1; - long bestPhotoId = -1; - long bestPhotoFileId = 0; - PhotoEntry bestPhotoEntry = null; - boolean foundSuperPrimaryPhoto = false; - int photoPriority = -1; - int totalRowCount = 0; - int contactSendToVoicemail = 0; - String contactCustomRingtone = null; - long contactLastTimeContacted = 0; - int contactTimesContacted = 0; - int contactStarred = 0; - int contactPinned = Integer.MAX_VALUE; - int hasPhoneNumber = 0; - StringBuilder lookupKey = new StringBuilder(); - - mDisplayNameCandidate.clear(); - - Cursor c = db.rawQuery(sql, sqlArgs); - try { - while (c.moveToNext()) { - long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID); - if (rawContactId != currentRawContactId) { - currentRawContactId = rawContactId; - totalRowCount++; - - // Assemble sub-account. - String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); - String dataSet = c.getString(RawContactsQuery.DATA_SET); - String accountWithDataSet = (!TextUtils.isEmpty(dataSet)) - ? accountType + "/" + dataSet - : accountType; - - // Display name - String displayName = c.getString(RawContactsQuery.DISPLAY_NAME); - int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE); - int isNameSuperPrimary = c.getInt(RawContactsQuery.HAS_SUPER_PRIMARY_NAME); - processDisplayNameCandidate(rawContactId, displayName, displayNameSource, - mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet), - isNameSuperPrimary != 0); - - // Contact options - if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) { - boolean sendToVoicemail = - (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0); - if (sendToVoicemail) { - contactSendToVoicemail++; - } - } - - if (contactCustomRingtone == null - && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) { - contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE); - } - - long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED); - if (lastTimeContacted > contactLastTimeContacted) { - contactLastTimeContacted = lastTimeContacted; - } - - int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED); - if (timesContacted > contactTimesContacted) { - contactTimesContacted = timesContacted; - } - - if (c.getInt(RawContactsQuery.STARRED) != 0) { - contactStarred = 1; - } - - // contactPinned should be the lowest value of its constituent raw contacts, - // excluding negative integers - final int rawContactPinned = c.getInt(RawContactsQuery.PINNED); - if (rawContactPinned > PinnedPositions.UNPINNED) { - contactPinned = Math.min(contactPinned, rawContactPinned); - } - - appendLookupKey( - lookupKey, - accountWithDataSet, - c.getString(RawContactsQuery.ACCOUNT_NAME), - rawContactId, - c.getString(RawContactsQuery.SOURCE_ID), - displayName); - } - - if (!c.isNull(RawContactsQuery.DATA_ID)) { - long dataId = c.getLong(RawContactsQuery.DATA_ID); - long photoFileId = c.getLong(RawContactsQuery.PHOTO_FILE_ID); - int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID); - boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0; - if (mimetypeId == mMimeTypeIdPhoto) { - if (!foundSuperPrimaryPhoto) { - // Lookup the metadata for the photo, if available. Note that data set - // does not come into play here, since accounts are looked up in the - // account manager in the priority resolver. - PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId); - String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); - int priority = mPhotoPriorityResolver.getPhotoPriority(accountType); - if (superPrimary || hasHigherPhotoPriority( - photoEntry, priority, bestPhotoEntry, photoPriority)) { - bestPhotoEntry = photoEntry; - photoPriority = priority; - bestPhotoId = dataId; - bestPhotoFileId = photoFileId; - foundSuperPrimaryPhoto |= superPrimary; - } - } - } else if (mimetypeId == mMimeTypeIdPhone) { - hasPhoneNumber = 1; - } - } - } - } finally { - c.close(); - } - - if (contactPinned == Integer.MAX_VALUE) { - contactPinned = PinnedPositions.UNPINNED; - } - - statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID, - mDisplayNameCandidate.rawContactId); - - if (bestPhotoId != -1) { - statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId); - } else { - statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID); - } - - if (bestPhotoFileId != 0) { - statement.bindLong(ContactReplaceSqlStatement.PHOTO_FILE_ID, bestPhotoFileId); - } else { - statement.bindNull(ContactReplaceSqlStatement.PHOTO_FILE_ID); - } - - statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL, - totalRowCount == contactSendToVoicemail ? 1 : 0); - DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE, - contactCustomRingtone); - statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED, - contactLastTimeContacted); - statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED, - contactTimesContacted); - statement.bindLong(ContactReplaceSqlStatement.STARRED, - contactStarred); - statement.bindLong(ContactReplaceSqlStatement.PINNED, - contactPinned); - statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER, - hasPhoneNumber); - statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY, - Uri.encode(lookupKey.toString())); - statement.bindLong(ContactReplaceSqlStatement.CONTACT_LAST_UPDATED_TIMESTAMP, - Clock.getInstance().currentTimeMillis()); - } - - /** - * Builds a lookup key using the given data. - */ - protected void appendLookupKey(StringBuilder sb, String accountTypeWithDataSet, - String accountName, long rawContactId, String sourceId, String displayName) { - ContactLookupKey.appendToLookupKey(sb, accountTypeWithDataSet, accountName, rawContactId, - sourceId, displayName); - } - - /** - * Uses the supplied values to determine if they represent a "better" display name - * for the aggregate contact currently evaluated. If so, it updates - * {@link #mDisplayNameCandidate} with the new values. - */ - private void processDisplayNameCandidate(long rawContactId, String displayName, - int displayNameSource, boolean writableAccount, boolean isNameSuperPrimary) { - - boolean replace = false; - if (mDisplayNameCandidate.rawContactId == -1) { - // No previous values available - replace = true; - } else if (!TextUtils.isEmpty(displayName)) { - if (isNameSuperPrimary) { - // A super primary name is better than any other name - replace = true; - } else if (mDisplayNameCandidate.isNameSuperPrimary == isNameSuperPrimary) { - if (mDisplayNameCandidate.displayNameSource < displayNameSource) { - // New values come from an superior source, e.g. structured name vs phone number - replace = true; - } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) { - if (!mDisplayNameCandidate.writableAccount && writableAccount) { - replace = true; - } else if (mDisplayNameCandidate.writableAccount == writableAccount) { - if (NameNormalizer.compareComplexity(displayName, - mDisplayNameCandidate.displayName) > 0) { - // New name is more complex than the previously found one - replace = true; - } - } - } - } - } - - if (replace) { - mDisplayNameCandidate.rawContactId = rawContactId; - mDisplayNameCandidate.displayName = displayName; - mDisplayNameCandidate.displayNameSource = displayNameSource; - mDisplayNameCandidate.isNameSuperPrimary = isNameSuperPrimary; - mDisplayNameCandidate.writableAccount = writableAccount; - } - } - - private interface PhotoIdQuery { - final String[] COLUMNS = new String[] { - AccountsColumns.CONCRETE_ACCOUNT_TYPE, - DataColumns.CONCRETE_ID, - Data.IS_SUPER_PRIMARY, - Photo.PHOTO_FILE_ID, - }; - - int ACCOUNT_TYPE = 0; - int DATA_ID = 1; - int IS_SUPER_PRIMARY = 2; - int PHOTO_FILE_ID = 3; - } - - public void updatePhotoId(SQLiteDatabase db, long rawContactId) { - - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - long bestPhotoId = -1; - long bestPhotoFileId = 0; - int photoPriority = -1; - - long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE); - - String tables = Tables.RAW_CONTACTS - + " JOIN " + Tables.ACCOUNTS + " ON (" - + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID - + ")" - + " JOIN " + Tables.DATA + " ON(" - + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID - + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND " - + Photo.PHOTO + " NOT NULL))"; - - mSelectionArgs1[0] = String.valueOf(contactId); - final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS, - RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null); - try { - PhotoEntry bestPhotoEntry = null; - while (c.moveToNext()) { - long dataId = c.getLong(PhotoIdQuery.DATA_ID); - long photoFileId = c.getLong(PhotoIdQuery.PHOTO_FILE_ID); - boolean superPrimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0; - PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId); - - // Note that data set does not come into play here, since accounts are looked up in - // the account manager in the priority resolver. - String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE); - int priority = mPhotoPriorityResolver.getPhotoPriority(accountType); - if (superPrimary || hasHigherPhotoPriority( - photoEntry, priority, bestPhotoEntry, photoPriority)) { - bestPhotoEntry = photoEntry; - photoPriority = priority; - bestPhotoId = dataId; - bestPhotoFileId = photoFileId; - if (superPrimary) { - break; - } - } - } - } finally { - c.close(); - } - - if (bestPhotoId == -1) { - mPhotoIdUpdate.bindNull(1); - } else { - mPhotoIdUpdate.bindLong(1, bestPhotoId); - } - - if (bestPhotoFileId == 0) { - mPhotoIdUpdate.bindNull(2); - } else { - mPhotoIdUpdate.bindLong(2, bestPhotoFileId); - } - - mPhotoIdUpdate.bindLong(3, contactId); - mPhotoIdUpdate.execute(); - } - private interface PhotoFileQuery { final String[] COLUMNS = new String[] { PhotoFiles.HEIGHT, @@ -2328,316 +813,12 @@ public class ContactAggregator2 { } return new PhotoEntry(0, 0); } - - private interface DisplayNameQuery { - String SQL_HAS_SUPER_PRIMARY_NAME = - " EXISTS(SELECT 1 " + - " FROM " + Tables.DATA + " d " + - " WHERE d." + DataColumns.MIMETYPE_ID + "=? " + - " AND d." + Data.RAW_CONTACT_ID + "=" + Views.RAW_CONTACTS - + "." + RawContacts._ID + - " AND d." + Data.IS_SUPER_PRIMARY + "=1)"; - - String SQL = - "SELECT " - + RawContacts._ID + "," - + RawContactsColumns.DISPLAY_NAME + "," - + RawContactsColumns.DISPLAY_NAME_SOURCE + "," - + SQL_HAS_SUPER_PRIMARY_NAME + "," - + RawContacts.SOURCE_ID + "," - + RawContacts.ACCOUNT_TYPE_AND_DATA_SET + - " FROM " + Views.RAW_CONTACTS + - " WHERE " + RawContacts.CONTACT_ID + "=? "; - - int _ID = 0; - int DISPLAY_NAME = 1; - int DISPLAY_NAME_SOURCE = 2; - int HAS_SUPER_PRIMARY_NAME = 3; - int SOURCE_ID = 4; - int ACCOUNT_TYPE_AND_DATA_SET = 5; - } - - public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) { - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - updateDisplayNameForContact(db, contactId); - } - - public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) { - boolean lookupKeyUpdateNeeded = false; - - mDisplayNameCandidate.clear(); - - mSelectionArgs2[0] = String.valueOf(mDbHelper.getMimeTypeIdForStructuredName()); - mSelectionArgs2[1] = String.valueOf(contactId); - final Cursor c = db.rawQuery(DisplayNameQuery.SQL, mSelectionArgs2); - try { - while (c.moveToNext()) { - long rawContactId = c.getLong(DisplayNameQuery._ID); - String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME); - int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE); - int isNameSuperPrimary = c.getInt(DisplayNameQuery.HAS_SUPER_PRIMARY_NAME); - String accountTypeAndDataSet = c.getString( - DisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET); - processDisplayNameCandidate(rawContactId, displayName, displayNameSource, - mContactsProvider.isWritableAccountWithDataSet(accountTypeAndDataSet), - isNameSuperPrimary != 0); - - // If the raw contact has no source id, the lookup key is based on the display - // name, so the lookup key needs to be updated. - lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID); - } - } finally { - c.close(); - } - - if (mDisplayNameCandidate.rawContactId != -1) { - mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId); - mDisplayNameUpdate.bindLong(2, contactId); - mDisplayNameUpdate.execute(); - } - - if (lookupKeyUpdateNeeded) { - updateLookupKeyForContact(db, contactId); - } - } - - - /** - * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the - * specified raw contact. - */ - public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) { - - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - final SQLiteStatement hasPhoneNumberUpdate = db.compileStatement( - "UPDATE " + Tables.CONTACTS + - " SET " + Contacts.HAS_PHONE_NUMBER + "=" - + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)" - + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS - + " WHERE " + DataColumns.MIMETYPE_ID + "=?" - + " AND " + Phone.NUMBER + " NOT NULL" - + " AND " + RawContacts.CONTACT_ID + "=?)" + - " WHERE " + Contacts._ID + "=?"); - try { - hasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE)); - hasPhoneNumberUpdate.bindLong(2, contactId); - hasPhoneNumberUpdate.bindLong(3, contactId); - hasPhoneNumberUpdate.execute(); - } finally { - hasPhoneNumberUpdate.close(); - } - } - - private interface LookupKeyQuery { - String TABLE = Views.RAW_CONTACTS; - String[] COLUMNS = new String[] { - RawContacts._ID, - RawContactsColumns.DISPLAY_NAME, - RawContacts.ACCOUNT_TYPE_AND_DATA_SET, - RawContacts.ACCOUNT_NAME, - RawContacts.SOURCE_ID, - }; - - int ID = 0; - int DISPLAY_NAME = 1; - int ACCOUNT_TYPE_AND_DATA_SET = 2; - int ACCOUNT_NAME = 3; - int SOURCE_ID = 4; - } - - public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) { - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - updateLookupKeyForContact(db, contactId); - } - - private void updateLookupKeyForContact(SQLiteDatabase db, long contactId) { - String lookupKey = computeLookupKeyForContact(db, contactId); - - if (lookupKey == null) { - mLookupKeyUpdate.bindNull(1); - } else { - mLookupKeyUpdate.bindString(1, Uri.encode(lookupKey)); - } - mLookupKeyUpdate.bindLong(2, contactId); - - mLookupKeyUpdate.execute(); - } - - protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) { - StringBuilder sb = new StringBuilder(); - mSelectionArgs1[0] = String.valueOf(contactId); - final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS, - RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID); - try { - while (c.moveToNext()) { - ContactLookupKey.appendToLookupKey(sb, - c.getString(LookupKeyQuery.ACCOUNT_TYPE_AND_DATA_SET), - c.getString(LookupKeyQuery.ACCOUNT_NAME), - c.getLong(LookupKeyQuery.ID), - c.getString(LookupKeyQuery.SOURCE_ID), - c.getString(LookupKeyQuery.DISPLAY_NAME)); - } - } finally { - c.close(); - } - return sb.length() == 0 ? null : sb.toString(); - } - - /** - * Execute {@link SQLiteStatement} that will update the - * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}. - */ - public void updateStarred(long rawContactId) { - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - - mStarredUpdate.bindLong(1, contactId); - mStarredUpdate.execute(); - } - - /** - * Execute {@link SQLiteStatement} that will update the - * {@link Contacts#PINNED} flag for the given {@link RawContacts#_ID}. - */ - public void updatePinned(long rawContactId) { - long contactId = mDbHelper.getContactId(rawContactId); - if (contactId == 0) { - return; - } - mPinnedUpdate.bindLong(1, contactId); - mPinnedUpdate.execute(); - } - - /** - * Finds matching contacts and returns a cursor on those. - */ - public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb, - String[] projection, long contactId, int maxSuggestions, String filter, - ArrayList<AggregationSuggestionParameter> parameters) { - final SQLiteDatabase db = mDbHelper.getReadableDatabase(); - db.beginTransaction(); - try { - List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters); - return queryMatchingContacts(qb, db, projection, bestMatches, maxSuggestions, filter); - } finally { - db.endTransaction(); - } - } - - private interface ContactIdQuery { - String[] COLUMNS = new String[] { - Contacts._ID - }; - - int _ID = 0; - } - - /** - * Loads contacts with specified IDs and returns them in the order of IDs in the - * supplied list. - */ - private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db, - String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) { - StringBuilder sb = new StringBuilder(); - sb.append(Contacts._ID); - sb.append(" IN ("); - for (int i = 0; i < bestMatches.size(); i++) { - MatchScore matchScore = bestMatches.get(i); - if (i != 0) { - sb.append(","); - } - sb.append(matchScore.getContactId()); - } - sb.append(")"); - - if (!TextUtils.isEmpty(filter)) { - sb.append(" AND " + Contacts._ID + " IN "); - mContactsProvider.appendContactFilterAsNestedQuery(sb, filter); - } - - // Run a query and find ids of best matching contacts satisfying the filter (if any) - HashSet<Long> foundIds = new HashSet<>(); - Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(), - null, null, null, null); - try { - while(cursor.moveToNext()) { - foundIds.add(cursor.getLong(ContactIdQuery._ID)); - } - } finally { - cursor.close(); - } - - // Exclude all contacts that did not match the filter - Iterator<MatchScore> iter = bestMatches.iterator(); - while (iter.hasNext()) { - long id = iter.next().getContactId(); - if (!foundIds.contains(id)) { - iter.remove(); - } - } - - // Limit the number of returned suggestions - final List<MatchScore> limitedMatches; - if (bestMatches.size() > maxSuggestions) { - limitedMatches = bestMatches.subList(0, maxSuggestions); - } else { - limitedMatches = bestMatches; - } - - // Build an in-clause with the remaining contact IDs - sb.setLength(0); - sb.append(Contacts._ID); - sb.append(" IN ("); - for (int i = 0; i < limitedMatches.size(); i++) { - MatchScore matchScore = limitedMatches.get(i); - if (i != 0) { - sb.append(","); - } - sb.append(matchScore.getContactId()); - } - sb.append(")"); - - // Run the final query with the required projection and contact IDs found by the first query - cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID); - - // Build a sorted list of discovered IDs - ArrayList<Long> sortedContactIds = new ArrayList<Long>(limitedMatches.size()); - for (MatchScore matchScore : limitedMatches) { - sortedContactIds.add(matchScore.getContactId()); - } - - Collections.sort(sortedContactIds); - - // Map cursor indexes according to the descending order of match scores - int[] positionMap = new int[limitedMatches.size()]; - for (int i = 0; i < positionMap.length; i++) { - long id = limitedMatches.get(i).getContactId(); - positionMap[i] = sortedContactIds.indexOf(id); - } - - return new ReorderingCursorWrapper(cursor, positionMap); - } - /** * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the * descending order of match score. * @param parameters */ - private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId, + protected List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId, ArrayList<AggregationSuggestionParameter> parameters) { MatchCandidateList candidates = new MatchCandidateList(); diff --git a/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java b/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java index 2e552e9..9b71651 100644 --- a/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java +++ b/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java @@ -31,9 +31,6 @@ import java.util.List; public class ContactMatcher { private static final String TAG = "ContactMatcher"; - // Best possible match score - public static final int MAX_SCORE = 100; - // Suggest to aggregate contacts if their match score is equal or greater than this threshold public static final int SCORE_THRESHOLD_SUGGEST = 50; @@ -59,9 +56,6 @@ public class ContactMatcher { // Maximum number of characters in a name to be considered by the matching algorithm. private static final int MAX_MATCHED_NAME_LENGTH = 30; - // Scores a multiplied by this number to allow room for "fractional" scores - private static final int SCORE_SCALE = 1000; - public static final int MATCHING_ALGORITHM_EXACT = 0; public static final int MATCHING_ALGORITHM_CONSERVATIVE = 1; public static final int MATCHING_ALGORITHM_APPROXIMATE = 2; @@ -159,88 +153,6 @@ public class ContactMatcher { return sMaxScore[index]; } - /** - * Captures the max score and match count for a specific contact. Used in an - * contactId - MatchScore map. - */ - public static class MatchScore implements Comparable<MatchScore> { - private long mContactId; - private boolean mKeepIn; - private boolean mKeepOut; - private int mPrimaryScore; - private int mSecondaryScore; - private int mMatchCount; - - public MatchScore(long contactId) { - this.mContactId = contactId; - } - - public void reset(long contactId) { - this.mContactId = contactId; - mKeepIn = false; - mKeepOut = false; - mPrimaryScore = 0; - mSecondaryScore = 0; - mMatchCount = 0; - } - - public long getContactId() { - return mContactId; - } - - public void updatePrimaryScore(int score) { - if (score > mPrimaryScore) { - mPrimaryScore = score; - } - mMatchCount++; - } - - public void updateSecondaryScore(int score) { - if (score > mSecondaryScore) { - mSecondaryScore = score; - } - mMatchCount++; - } - - public void keepIn() { - mKeepIn = true; - } - - public void keepOut() { - mKeepOut = true; - } - - public int getScore() { - if (mKeepOut) { - return 0; - } - - if (mKeepIn) { - return MAX_SCORE; - } - - int score = (mPrimaryScore > mSecondaryScore ? mPrimaryScore : mSecondaryScore); - - // Ensure that of two contacts with the same match score the one with more matching - // data elements wins. - return score * SCORE_SCALE + mMatchCount; - } - - /** - * Descending order of match score. - */ - @Override - public int compareTo(MatchScore another) { - return another.getScore() - getScore(); - } - - @Override - public String toString() { - return mContactId + ": " + mPrimaryScore + "/" + mSecondaryScore + "(" + mMatchCount - + ")"; - } - } - private final HashMap<Long, MatchScore> mScores = new HashMap<Long, MatchScore>(); private final ArrayList<MatchScore> mScoreList = new ArrayList<MatchScore>(); private int mScoreCount = 0; @@ -268,7 +180,7 @@ public class ContactMatcher { * Marks the contact as a full match, because we found an Identity match */ public void matchIdentity(long contactId) { - updatePrimaryScore(contactId, MAX_SCORE); + updatePrimaryScore(contactId, MatchScore.MAX_SCORE); } /** @@ -374,18 +286,18 @@ public class ContactMatcher { for (int i = 0; i < mScoreCount; i++) { MatchScore score = mScoreList.get(i); - if (score.mKeepOut) { + if (score.isKeepOut()) { continue; } - int s = score.mSecondaryScore; + int s = score.getSecondaryScore(); if (s >= threshold) { if (contactIds == null) { contactIds = new ArrayList<Long>(); } - contactIds.add(score.mContactId); + contactIds.add(score.getContactId()); } - score.mPrimaryScore = NO_DATA_SCORE; + score.setPrimaryScore(NO_DATA_SCORE); } return contactIds; } @@ -401,17 +313,17 @@ public class ContactMatcher { int maxScore = 0; for (int i = 0; i < mScoreCount; i++) { MatchScore score = mScoreList.get(i); - if (score.mKeepOut) { + if (score.isKeepOut()) { continue; } - if (score.mKeepIn) { - return score.mContactId; + if (score.isKeepIn()) { + return score.getContactId(); } - int s = score.mPrimaryScore; + int s = score.getPrimaryScore(); if (s == NO_DATA_SCORE) { - s = score.mSecondaryScore; + s = score.getSecondaryScore(); } if (s >= threshold) { @@ -420,8 +332,8 @@ public class ContactMatcher { } // In order to make it stable, let's jut pick the one with the lowest ID // if multiple candidates are found. - if ((s > maxScore) || ((s == maxScore) && (contactId > score.mContactId))) { - contactId = score.mContactId; + if ((s > maxScore) || ((s == maxScore) && (contactId > score.getContactId()))) { + contactId = score.getContactId(); maxScore = s; } } @@ -433,7 +345,7 @@ public class ContactMatcher { * Returns matches in the order of descending score. */ public List<MatchScore> pickBestMatches(int threshold) { - int scaledThreshold = threshold * SCORE_SCALE; + int scaledThreshold = threshold * MatchScore.SCORE_SCALE; List<MatchScore> matches = mScoreList.subList(0, mScoreCount); Collections.sort(matches); int count = 0; diff --git a/src/com/android/providers/contacts/aggregation/util/MatchScore.java b/src/com/android/providers/contacts/aggregation/util/MatchScore.java new file mode 100644 index 0000000..f87731c --- /dev/null +++ b/src/com/android/providers/contacts/aggregation/util/MatchScore.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2015 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.aggregation.util; + +/** + * Captures the max score and match count for a specific raw contact or contact. + */ +public class MatchScore implements Comparable<MatchScore> { + // Scores a multiplied by this number to allow room for "fractional" scores + public static final int SCORE_SCALE = 1000; + // Best possible match score + public static final int MAX_SCORE = 100; + + private long mRawContactId; + private long mContactId; + private long mAccountId; + + private boolean mKeepIn; + private boolean mKeepOut; + + private int mPrimaryScore; + private int mSecondaryScore; + private int mMatchCount; + + public MatchScore(long rawContactId, long contactId, long accountId) { + this.mRawContactId = rawContactId; + this.mContactId = contactId; + this.mAccountId = accountId; + } + + public MatchScore(long contactId) { + this.mRawContactId = 0; + this.mContactId = contactId; + this.mAccountId = 0; + } + + public void reset(long rawContactId, long contactId, long accountId) { + this.mRawContactId = rawContactId; + this.mContactId = contactId; + this.mAccountId = accountId; + mKeepIn = false; + mKeepOut = false; + mPrimaryScore = 0; + mSecondaryScore = 0; + mMatchCount = 0; + } + + public void reset(long contactId) { + this.reset(0l, contactId, 0l); + } + + + public long getRawContactId() { + return mRawContactId; + } + + public long getContactId() { + return mContactId; + } + + public long getAccountId() { + return mAccountId; + } + + public void updatePrimaryScore(int score) { + if (score > mPrimaryScore) { + mPrimaryScore = score; + } + mMatchCount++; + } + + public void updateSecondaryScore(int score) { + if (score > mSecondaryScore) { + mSecondaryScore = score; + } + mMatchCount++; + } + + public void keepIn() { + mKeepIn = true; + } + + public void keepOut() { + mKeepOut = true; + } + + public int getScore() { + if (mKeepOut) { + return 0; + } + + if (mKeepIn) { + return MAX_SCORE; + } + + int score = (mPrimaryScore > mSecondaryScore ? mPrimaryScore : mSecondaryScore); + + // Ensure that of two contacts with the same match score the one with more matching + // data elements wins. + return score * SCORE_SCALE + mMatchCount; + } + + public boolean isKeepIn() { + return mKeepIn; + } + + public boolean isKeepOut() { + return mKeepOut; + } + + public int getPrimaryScore() { + return mPrimaryScore; + } + + public int getSecondaryScore() { + return mSecondaryScore; + } + + public void setPrimaryScore(int mPrimaryScore) { + this.mPrimaryScore = mPrimaryScore; + } + + /** + * Descending order of match score. + */ + @Override + public int compareTo(MatchScore another) { + return another.getScore() - getScore(); + } + + @Override + public String toString() { + return mRawContactId + "/" + mContactId + "/" + mAccountId + ": " + mPrimaryScore + + "/" + mSecondaryScore + "(" + mMatchCount + ")"; + } +} diff --git a/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java b/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java index 3b0150c..1abcfa1 100644 --- a/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java +++ b/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java @@ -15,11 +15,10 @@ */ package com.android.providers.contacts.aggregation.util; +import android.util.Log; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; import com.android.providers.contacts.util.Hex; -import android.util.Log; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -160,102 +159,6 @@ public class RawContactMatcher { return sMaxScore[index]; } - /** - * Captures the max score and match count for a specific raw contact. Used in an - * rawContactId - MatchScore map. - */ - public static class MatchScore implements Comparable<MatchScore> { - private long mRawContactId; - private long mContactId; - private long mAccountId; - private boolean mKeepIn; - private boolean mKeepOut; - private int mPrimaryScore; - private int mSecondaryScore; - private int mMatchCount; - - public MatchScore(long rawContactId, long contactId, long accountId) { - this.mRawContactId = rawContactId; - this.mContactId = contactId; - this.mAccountId = accountId; - } - - public void reset(long rawContactId, long contactId, long accountId) { - this.mRawContactId = rawContactId; - this.mContactId = contactId; - this.mAccountId = accountId; - mKeepIn = false; - mKeepOut = false; - mPrimaryScore = 0; - mSecondaryScore = 0; - mMatchCount = 0; - } - - public long getRawContactId() { - return mRawContactId; - } - - public long getContactId() { - return mContactId; - } - - public long getAccountId() { - return mAccountId; - } - - public void updatePrimaryScore(int score) { - if (score > mPrimaryScore) { - mPrimaryScore = score; - } - mMatchCount++; - } - - public void updateSecondaryScore(int score) { - if (score > mSecondaryScore) { - mSecondaryScore = score; - } - mMatchCount++; - } - - public void keepIn() { - mKeepIn = true; - } - - public void keepOut() { - mKeepOut = true; - } - - public int getScore() { - if (mKeepOut) { - return 0; - } - - if (mKeepIn) { - return MAX_SCORE; - } - - int score = (mPrimaryScore > mSecondaryScore ? mPrimaryScore : mSecondaryScore); - - // Ensure that of two contacts with the same match score the one with more matching - // data elements wins. - return score * SCORE_SCALE + mMatchCount; - } - - /** - * Descending order of match score. - */ - @Override - public int compareTo(MatchScore another) { - return another.getScore() - getScore(); - } - - @Override - public String toString() { - return mRawContactId + "/" + mContactId + "/" + mAccountId + ": " + mPrimaryScore + - "/" + mSecondaryScore + "(" + mMatchCount + ")"; - } - } - private final HashMap<Long, MatchScore> mScores = new HashMap<Long, MatchScore>(); private final ArrayList<MatchScore> mScoreList = new ArrayList<MatchScore>(); private int mScoreCount = 0; diff --git a/src/com/android/providers/contacts/aggregation/util/RawContactMatchingCandidates.java b/src/com/android/providers/contacts/aggregation/util/RawContactMatchingCandidates.java index 39125b4..917c810 100644 --- a/src/com/android/providers/contacts/aggregation/util/RawContactMatchingCandidates.java +++ b/src/com/android/providers/contacts/aggregation/util/RawContactMatchingCandidates.java @@ -29,25 +29,25 @@ import static com.android.internal.util.Preconditions.checkNotNull; * Matching candidates for a raw contact, used in the contact aggregator. */ public class RawContactMatchingCandidates { - private List<RawContactMatcher.MatchScore> mBestMatches; + private List<MatchScore> mBestMatches; private Set<Long> mRawContactIds = null; private Map<Long, Long> mRawContactToContact = null; private Map<Long, Long> mRawContactToAccount = null; - public RawContactMatchingCandidates(List<RawContactMatcher.MatchScore> mBestMatches) { + public RawContactMatchingCandidates(List<MatchScore> mBestMatches) { checkNotNull(mBestMatches); this.mBestMatches = mBestMatches; } public RawContactMatchingCandidates() { - mBestMatches = new ArrayList<RawContactMatcher.MatchScore>(); + mBestMatches = new ArrayList<MatchScore>(); } public int getCount() { return mBestMatches.size(); } - public void add(RawContactMatcher.MatchScore score) { + public void add(MatchScore score) { mBestMatches.add(score); if (mRawContactIds != null) { mRawContactIds.add(score.getRawContactId()); |