diff options
author | Dmitri Plotnikov <dplotnikov@google.com> | 2010-08-11 16:57:00 -0700 |
---|---|---|
committer | Dmitri Plotnikov <dplotnikov@google.com> | 2010-08-11 16:57:00 -0700 |
commit | 5b3634b24d3c21618f96860e969fd5c9ba7d9ca8 (patch) | |
tree | 7f990e816d0d25282c4db802f4d018c172585416 | |
parent | b0c9a8a175ca1d3fea593062081b838b6e758339 (diff) | |
download | packages_providers_ContactsProvider-5b3634b24d3c21618f96860e969fd5c9ba7d9ca8.zip packages_providers_ContactsProvider-5b3634b24d3c21618f96860e969fd5c9ba7d9ca8.tar.gz packages_providers_ContactsProvider-5b3634b24d3c21618f96860e969fd5c9ba7d9ca8.tar.bz2 |
Adding support for query-based aggregation suggestions.
For now exact name match is required.
Will add other types of search later.
Change-Id: Ibc7bca3a7f418da349b318e0e31861268af5f827
3 files changed, 235 insertions, 25 deletions
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java index 44a9c71..46fb78d 100644 --- a/src/com/android/providers/contacts/ContactAggregator.java +++ b/src/com/android/providers/contacts/ContactAggregator.java @@ -35,14 +35,16 @@ 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.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.RawContacts; import android.provider.ContactsContract.StatusUpdates; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.Photo; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; @@ -93,6 +95,9 @@ public class ContactAggregator { 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 */ @@ -130,6 +135,19 @@ public class ContactAggregator { 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. @@ -169,6 +187,10 @@ public class ContactAggregator { public void clear() { mCount = 0; } + + public boolean isEmpty() { + return mCount == 0; + } } /** @@ -200,10 +222,13 @@ public class ContactAggregator { */ public ContactAggregator(ContactsProvider2 contactsProvider, ContactsDatabaseHelper contactsDatabaseHelper, - PhotoPriorityResolver photoPriorityResolver) { + PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter, + CommonNicknameCache commonNicknameCache) { mContactsProvider = contactsProvider; mDbHelper = contactsDatabaseHelper; mPhotoPriorityResolver = photoPriorityResolver; + mNameSplitter = nameSplitter; + mCommonNicknameCache = commonNicknameCache; SQLiteDatabase db = mDbHelper.getReadableDatabase(); @@ -212,17 +237,17 @@ public class ContactAggregator { final String replaceAggregatePresenceSql = "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "(" + AggregatedPresenceColumns.CONTACT_ID + ", " - + StatusUpdates.PRESENCE_STATUS + ", " + + StatusUpdates.STATUS + ", " + StatusUpdates.CHAT_CAPABILITY + ")" + " SELECT " + PresenceColumns.CONTACT_ID + "," - + StatusUpdates.PRESENCE_STATUS + "," + + StatusUpdates.STATUS + "," + StatusUpdates.CHAT_CAPABILITY + " FROM " + Tables.PRESENCE + " WHERE " - + " (" + StatusUpdates.PRESENCE_STATUS + + " (" + StatusUpdates.STATUS + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" + " = (SELECT " - + "MAX (" + StatusUpdates.PRESENCE_STATUS + + "MAX (" + StatusUpdates.STATUS + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")" + " FROM " + Tables.PRESENCE + " WHERE " + PresenceColumns.CONTACT_ID @@ -848,6 +873,9 @@ public class ContactAggregator { 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); @@ -872,6 +900,101 @@ 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 mCandidates; + + private StringBuilder mSelection = new StringBuilder( + NameLookupColumns.NORMALIZED_NAME + " IN("); + + + public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) { + super(splitter); + this.mCandidates = candidates; + } + + @Override + protected String[] getCommonNicknameClusters(String normalizedName) { + return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); + } + + @Override + protected void insertNameLookup( + long rawContactId, long dataId, int lookupType, String string) { + mCandidates.add(string, lookupType); + DatabaseUtils.appendEscapedSQLString(mSelection, string); + mSelection.append(','); + } + + public boolean isEmpty() { + return mCandidates.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 < mCandidates.mCount; i++) { + if (mCandidates.mList.get(i).mName.equals(name)) { + return mCandidates.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) { + 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" + @@ -1521,10 +1644,11 @@ public class ContactAggregator { * Finds matching contacts and returns a cursor on those. */ public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb, String[] projection, - long contactId, int maxSuggestions, String filter) { + long contactId, int maxSuggestions, String filter, + ArrayList<AggregationSuggestionParameter> parameters) { final SQLiteDatabase db = mDbHelper.getReadableDatabase(); - List<MatchScore> bestMatches = findMatchingContacts(db, contactId); + List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters); return queryMatchingContacts(qb, db, contactId, projection, bestMatches, maxSuggestions, filter); } @@ -1634,8 +1758,10 @@ public class ContactAggregator { /** * 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) { + private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId, + ArrayList<AggregationSuggestionParameter> parameters) { MatchCandidateList candidates = new MatchCandidateList(); ContactMatcher matcher = new ContactMatcher(); @@ -1643,16 +1769,21 @@ public class ContactAggregator { // Don't aggregate a contact with itself matcher.keepOut(contactId); - 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._ID); - updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates, - matcher); + if (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._ID); + updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates, + matcher); + } + } finally { + c.close(); } - } finally { - c.close(); + } else { + updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates, + matcher, parameters); } return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST); @@ -1670,4 +1801,16 @@ public class ContactAggregator { 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.MATCH_NAME.equals(parameter.kind)) { + updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher); + } + + // TODO: add support for other + } + } } diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index d28ec88..d3514aa 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -17,6 +17,7 @@ package com.android.providers.contacts; import com.android.internal.content.SyncStateContentProviderHelper; +import com.android.providers.contacts.ContactAggregator.AggregationSuggestionParameter; import com.android.providers.contacts.ContactLookupKey.LookupKeySegment; import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns; @@ -96,6 +97,7 @@ import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.ContactCounts; import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Contacts.AggregationSuggestions; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.Directory; import android.provider.ContactsContract.DisplayNameSources; @@ -1852,10 +1854,6 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun mContactDirectoryManager = new ContactDirectoryManager(this); mGlobalSearchSupport = new GlobalSearchSupport(this); mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport); - mContactAggregator = new ContactAggregator(this, mDbHelper, - createPhotoPriorityResolver(context)); - mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); - mDb = mDbHelper.getWritableDatabase(); initForDefaultLocale(); @@ -2018,6 +2016,10 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun mPostalSplitter = new PostalSplitter(mCurrentLocale); mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase()); ContactLocaleUtils.getIntance().setLocale(mCurrentLocale); + mContactAggregator = new ContactAggregator(this, mDbHelper, + createPhotoPriorityResolver(getContext()), mNameSplitter, mCommonNicknameCache); + mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); + initDataRowHandlers(); } @@ -4840,10 +4842,26 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun maxSuggestions = DEFAULT_MAX_SUGGESTIONS; } + ArrayList<AggregationSuggestionParameter> parameters = null; + List<String> query = uri.getQueryParameters("query"); + if (query != null && !query.isEmpty()) { + parameters = new ArrayList<AggregationSuggestionParameter>(query.size()); + for (String parameter : query) { + int offset = parameter.indexOf(':'); + parameters.add(offset == -1 + ? new AggregationSuggestionParameter( + AggregationSuggestions.MATCH_NAME, + parameter) + : new AggregationSuggestionParameter( + parameter.substring(0, offset), + parameter.substring(offset + 1))); + } + } + setTablesAndProjectionMapForContacts(qb, uri, projection); return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId, - maxSuggestions, filter); + maxSuggestions, filter, parameters); } case SETTINGS: { diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java index 6113856..38cd4bb 100644 --- a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java +++ b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java @@ -28,6 +28,7 @@ import android.net.Uri; import android.provider.ContactsContract; import android.provider.ContactsContract.AggregationExceptions; import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Contacts.AggregationSuggestions; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.CommonDataKinds.Organization; @@ -893,6 +894,54 @@ public class ContactAggregatorTest extends BaseContactsProvider2Test { assertAggregated(rawContactId1, rawContactId2); } + public void testAggregationSuggestionsQueryBuilderWithContactId() throws Exception { + Uri uri = AggregationSuggestions.builder().setContactId(12).setLimit(7).build(); + assertEquals("content://com.android.contacts/contacts/12/suggestions?limit=7", + uri.toString()); + } + + public void testAggregationSuggestionsQueryBuilderWithValues() throws Exception { + Uri uri = AggregationSuggestions.builder() + .addParameter(AggregationSuggestions.MATCH_NAME, "name1") + .addParameter(AggregationSuggestions.MATCH_NAME, "name2") + .addParameter(AggregationSuggestions.MATCH_EMAIL, "email1") + .addParameter(AggregationSuggestions.MATCH_EMAIL, "email2") + .addParameter(AggregationSuggestions.MATCH_PHONE, "phone1") + .addParameter(AggregationSuggestions.MATCH_NICKNAME, "nickname1") + .setLimit(7) + .build(); + assertEquals("content://com.android.contacts/contacts/-1/suggestions?" + + "limit=7" + + "&query=name%3Aname1" + + "&query=name%3Aname2" + + "&query=email%3Aemail1" + + "&query=email%3Aemail2" + + "&query=phone%3Aphone1" + + "&query=nickname%3Anickname1", uri.toString()); + } + + public void testAggregationSuggestionsByName() throws Exception { + long rawContactId1 = createRawContactWithName("first1", "last1"); + long rawContactId2 = createRawContactWithName("first2", "last2"); + + Uri uri = AggregationSuggestions.builder() + .addParameter(AggregationSuggestions.MATCH_NAME, "last1 first1") + .build(); + + Cursor cursor = mResolver.query( + uri, new String[] { Contacts._ID, Contacts.DISPLAY_NAME }, null, null, null); + + assertEquals(1, cursor.getCount()); + + cursor.moveToFirst(); + + ContentValues values = new ContentValues(); + values.put(Contacts._ID, queryContactId(rawContactId1)); + values.put(Contacts.DISPLAY_NAME, "first1 last1"); + assertCursorValues(cursor, values); + cursor.close(); + } + private void assertSuggestions(long contactId, long... suggestions) { final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); Uri uri = Uri.withAppendedPath(aggregateUri, |