diff options
Diffstat (limited to 'src')
3 files changed, 239 insertions, 31 deletions
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java index 7404dca6..fb7995fe 100644 --- a/src/com/android/providers/contacts/ContactAggregator.java +++ b/src/com/android/providers/contacts/ContactAggregator.java @@ -331,11 +331,14 @@ public class ContactAggregator { private interface AggregationQuery { String SQL = "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID + + ", " + RawContacts.ACCOUNT_TYPE + "," + RawContacts.ACCOUNT_NAME + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + " IN("; int _ID = 0; int CONTACT_ID = 1; + int ACCOUNT_TYPE = 2; + int ACCOUNT_NAME = 3; } /** @@ -372,6 +375,8 @@ public class ContactAggregator { long rawContactIds[] = new long[count]; long contactIds[] = new long[count]; + String accountTypes[] = new String[count]; + String accountNames[] = new String[count]; Cursor c = db.rawQuery(mSb.toString(), selectionArgs); try { count = c.getCount(); @@ -379,6 +384,8 @@ public class ContactAggregator { while (c.moveToNext()) { rawContactIds[index] = c.getLong(AggregationQuery._ID); contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID); + accountTypes[index] = c.getString(AggregationQuery.ACCOUNT_TYPE); + accountNames[index] = c.getString(AggregationQuery.ACCOUNT_NAME); index++; } } finally { @@ -386,7 +393,8 @@ public class ContactAggregator { } for (int i = 0; i < count; i++) { - aggregateContact(db, rawContactIds[i], contactIds[i], mCandidates, mMatcher, mValues); + aggregateContact(db, rawContactIds[i], accountTypes[i], accountNames[i], contactIds[i], + mCandidates, mMatcher, mValues); } long elapsedTime = System.currentTimeMillis() - start; @@ -432,10 +440,44 @@ public class ContactAggregator { mDbHelper.updateContactVisible(contactId); } + private static final class RawContactIdAndAccountQuery { + public static final String TABLE = Tables.RAW_CONTACTS; + + public static final String[] COLUMNS = { + RawContacts.CONTACT_ID, RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME }; + + public static final String SELECTION = RawContacts._ID + "=?"; + + public static final int CONTACT_ID = 0; + public static final int ACCOUNT_TYPE = 1; + public static final int ACCOUNT_NAME = 2; + } + + public void aggregateContact(SQLiteDatabase db, long rawContactId) { + long contactId = 0; + String accountName = null; + String accountType = null; + 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); + accountType = cursor.getString(RawContactIdAndAccountQuery.ACCOUNT_TYPE); + accountName = cursor.getString(RawContactIdAndAccountQuery.ACCOUNT_NAME); + } + } finally { + cursor.close(); + } + aggregateContact(db, rawContactId, accountType, accountName, contactId); + } + /** * Synchronously aggregate the specified contact assuming an open transaction. */ - public void aggregateContact(SQLiteDatabase db, long rawContactId, long currentContactId) { + public void aggregateContact(SQLiteDatabase db, long rawContactId, String accountType, + String accountName, long currentContactId) { if (!mEnabled) { return; } @@ -444,7 +486,8 @@ public class ContactAggregator { ContactMatcher matcher = new ContactMatcher(); ContentValues values = new ContentValues(); - aggregateContact(db, rawContactId, currentContactId, candidates, matcher, values); + aggregateContact(db, rawContactId, accountType, accountName, currentContactId, candidates, + matcher, values); } public void updateAggregateData(long contactId) { @@ -472,8 +515,8 @@ public class ContactAggregator { * with the highest match score. If no such contact is found, creates a new contact. */ private synchronized void aggregateContact(SQLiteDatabase db, long rawContactId, - long currentContactId, MatchCandidateList candidates, ContactMatcher matcher, - ContentValues values) { + String accountType, String accountName, long currentContactId, + MatchCandidateList candidates, ContactMatcher matcher, ContentValues values) { int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; @@ -513,22 +556,21 @@ public class ContactAggregator { contactId = currentContactId; } + long contactIdToSplit = -1; + + if (contactId != currentContactId && contactId != -1) { + if (containsRawContactsFromAccount(db, contactId, accountType, accountName)) { + contactIdToSplit = contactId; + contactId = -1; + } + } + if (contactId == currentContactId) { // Aggregation unchanged markAggregated(rawContactId); } else if (contactId == -1) { // Splitting an aggregate - mSelectionArgs1[0] = String.valueOf(rawContactId); - computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, - mContactInsert); - contactId = mContactInsert.executeInsert(); - setContactIdAndMarkAggregated(rawContactId, contactId); - mDbHelper.updateContactVisible(contactId); - - setPresenceContactId(rawContactId, contactId); - - updateAggregatedPresence(contactId); - + createNewContactForRawContact(db, rawContactId); if (currentContactContentsCount > 0) { updateAggregateData(currentContactId); } @@ -550,6 +592,106 @@ public class ContactAggregator { mDbHelper.updateContactVisible(contactId); updateAggregatedPresence(contactId); } + + if (contactIdToSplit != -1) { + splitAutomaticallyAggregatedRawContacts(db, contactIdToSplit); + } + } + + /** + * Returns true if the aggregate contains has any raw contacts from the specified account. + */ + private boolean containsRawContactsFromAccount( + SQLiteDatabase db, long contactId, String accountType, String accountName) { + String query; + String[] args; + if (accountType == null) { + query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS + + " WHERE " + RawContacts.CONTACT_ID + "=?" + + " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL " + + " AND " + RawContacts.ACCOUNT_NAME + " IS NULL "; + args = mSelectionArgs1; + args[0] = String.valueOf(contactId); + } else { + query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS + + " WHERE " + RawContacts.CONTACT_ID + "=?" + + " AND " + RawContacts.ACCOUNT_TYPE + "=?" + + " AND " + RawContacts.ACCOUNT_NAME + "=?"; + args = mSelectionArgs3; + args[0] = String.valueOf(contactId); + args[1] = accountType; + args[2] = accountName; + } + Cursor cursor = db.rawQuery(query, args); + try { + cursor.moveToFirst(); + return cursor.getInt(0) != 0; + } finally { + cursor.close(); + } + } + + /** + * Breaks up an existing aggregate when a new raw contact is inserted that has + * comes from the same account as one of the raw contacts in this aggregate. + */ + private void splitAutomaticallyAggregatedRawContacts(SQLiteDatabase db, long contactId) { + mSelectionArgs1[0] = String.valueOf(contactId); + int count = (int) DatabaseUtils.longForQuery(db, + "SELECT COUNT(" + RawContacts._ID + ")" + + " FROM " + Tables.RAW_CONTACTS + + " WHERE " + RawContacts.CONTACT_ID + "=?", mSelectionArgs1); + if (count < 2) { + // A single-raw-contact aggregate does not need to be split up + return; + } + + // Find all constituent raw contacts that are not held together by + // an explicit aggregation exception + String query = + "SELECT " + RawContacts._ID + + " FROM " + Tables.RAW_CONTACTS + + " WHERE " + RawContacts.CONTACT_ID + "=?" + + " AND " + RawContacts._ID + " NOT IN " + + "(SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + + " FROM " + Tables.AGGREGATION_EXCEPTIONS + + " WHERE " + AggregationExceptions.TYPE + "=" + + AggregationExceptions.TYPE_KEEP_TOGETHER + + " UNION SELECT " + AggregationExceptions.RAW_CONTACT_ID2 + + " FROM " + Tables.AGGREGATION_EXCEPTIONS + + " WHERE " + AggregationExceptions.TYPE + "=" + + AggregationExceptions.TYPE_KEEP_TOGETHER + + ")"; + Cursor cursor = db.rawQuery(query, mSelectionArgs1); + try { + // Process up to count-1 raw contact, leaving the last one alone. + for (int i = 0; i < count - 1; i++) { + if (!cursor.moveToNext()) { + break; + } + long rawContactId = cursor.getLong(0); + createNewContactForRawContact(db, rawContactId); + } + } finally { + cursor.close(); + } + if (contactId > 0) { + updateAggregateData(contactId); + } + } + + /** + * Creates a stand-alone Contact for the given raw contact ID. + */ + private void createNewContactForRawContact(SQLiteDatabase db, long rawContactId) { + mSelectionArgs1[0] = String.valueOf(rawContactId); + computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, + mContactInsert); + long contactId = mContactInsert.executeInsert(); + setContactIdAndMarkAggregated(rawContactId, contactId); + mDbHelper.updateContactVisible(contactId); + setPresenceContactId(rawContactId, contactId); + updateAggregatedPresence(contactId); } /** @@ -704,7 +846,7 @@ public class ContactAggregator { c.close(); } - return matcher.pickBestMatch(ContactMatcher.MAX_SCORE); + return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true); } /** @@ -726,9 +868,15 @@ public class ContactAggregator { // Find good matches based on name alone long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, candidates, matcher); - if (bestMatch == -1) { + 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; @@ -766,7 +914,7 @@ public class ContactAggregator { matchAllCandidates(db, mSb.toString(), candidates, matcher, ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null); - return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY); + return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false); } private interface NameLookupQuery { @@ -812,7 +960,7 @@ public class ContactAggregator { MatchCandidateList candidates, ContactMatcher matcher) { updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher); - long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY); + long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY, false); if (bestMatch != -1) { return bestMatch; } diff --git a/src/com/android/providers/contacts/ContactMatcher.java b/src/com/android/providers/contacts/ContactMatcher.java index a38f2760..7f26d903 100644 --- a/src/com/android/providers/contacts/ContactMatcher.java +++ b/src/com/android/providers/contacts/ContactMatcher.java @@ -68,6 +68,9 @@ public class ContactMatcher { // Minimum edit distance between two email ids to be considered an approximate match public static final float APPROXIMATE_MATCH_THRESHOLD_FOR_EMAIL = 0.95f; + // Returned value when we found multiple matches and that was not allowed + public static final long MULTIPLE_MATCHES = -2; + /** * Name matching scores: a matrix by name type vs. candidate lookup type. * For example, if the name type is "full name" while we are looking for a @@ -371,7 +374,7 @@ public class ContactMatcher { * Returns the contactId with the best match score over the specified threshold or -1 * if no such contact is found. */ - public long pickBestMatch(int threshold) { + public long pickBestMatch(int threshold, boolean allowMultipleMatches) { long contactId = -1; int maxScore = 0; for (int i = 0; i < mScoreCount; i++) { @@ -389,9 +392,14 @@ public class ContactMatcher { s = score.mSecondaryScore; } - if (s >= threshold && s > maxScore) { - contactId = score.mContactId; - maxScore = s; + if (s >= threshold) { + if (contactId != -1 && !allowMultipleMatches) { + return MULTIPLE_MATCHES; + } + if (s > maxScore) { + contactId = score.mContactId; + maxScore = s; + } } } return contactId; diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index a16d3cda..61a67d4e 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -76,6 +76,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.MemoryFile; import android.os.RemoteException; +import android.os.SystemClock; import android.os.SystemProperties; import android.pim.vcard.VCardComposer; import android.pim.vcard.VCardConfig; @@ -156,6 +157,9 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1; private static final String PREF_LOCALE = "locale"; + private static final String PROPERTY_AGGREGATION_ALGORITHM = "aggregation_v2"; + private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 2; + private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate"; private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); @@ -1995,6 +1999,10 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun verifyLocale(); } + if (isAggregationUpgradeNeeded()) { + upgradeAggregationAlgorithm(); + } + return (mDb != null); } @@ -2634,8 +2642,7 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun } case RawContacts.AGGREGATION_MODE_IMMEDIATE: { - long contactId = mDbHelper.getContactId(rawContactId); - mContactAggregator.aggregateContact(mDb, rawContactId, contactId); + mContactAggregator.aggregateContact(mDb, rawContactId); break; } } @@ -3963,11 +3970,8 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun mContactAggregator.markForAggregation(rawContactId2, RawContacts.AGGREGATION_MODE_DEFAULT, true); - long contactId1 = mDbHelper.getContactId(rawContactId1); - mContactAggregator.aggregateContact(db, rawContactId1, contactId1); - - long contactId2 = mDbHelper.getContactId(rawContactId2); - mContactAggregator.aggregateContact(db, rawContactId2, contactId2); + mContactAggregator.aggregateContact(db, rawContactId1); + mContactAggregator.aggregateContact(db, rawContactId2); // The return value is fake - we just confirm that we made a change, not count actual // rows changed. @@ -6014,4 +6018,52 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun stmt.bindLong(index, value.longValue()); } } + + protected boolean isAggregationUpgradeNeeded() { + if (!mContactAggregator.isEnabled()) { + return false; + } + + int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_AGGREGATION_ALGORITHM, "1")); + return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION; + } + + protected void upgradeAggregationAlgorithm() { + // This upgrade will affect very few contacts, so it can be performed on the + // main thread during the initial boot after an OTA + + Log.i(TAG, "Upgrading aggregation algorithm"); + int count = 0; + long start = SystemClock.currentThreadTimeMillis(); + try { + mDb.beginTransaction(); + Cursor cursor = mDb.query(true, + Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2", + new String[]{"r1." + RawContacts._ID}, + "r1." + RawContacts._ID + "!=r2." + RawContacts._ID + + " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID + + " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME + + " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE, + null, null, null, null, null); + try { + while (cursor.moveToNext()) { + long rawContactId = cursor.getLong(0); + mContactAggregator.markForAggregation(rawContactId, + RawContacts.AGGREGATION_MODE_DEFAULT, true); + count++; + } + } finally { + cursor.close(); + } + mContactAggregator.aggregateInTransaction(mDb); + mDb.setTransactionSuccessful(); + mDbHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM, + String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION)); + } finally { + mDb.endTransaction(); + long end = SystemClock.currentThreadTimeMillis(); + Log.i(TAG, "Aggregation algorithm upgraded for " + count + + " contacts, in " + (end - start) + "ms"); + } + } } |