summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/com/android/providers/contacts/ContactAggregator.java183
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java28
-rw-r--r--tests/src/com/android/providers/contacts/ContactAggregatorTest.java49
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,