diff options
author | Makoto Onuki <omakoto@google.com> | 2012-04-11 11:07:26 -0700 |
---|---|---|
committer | Makoto Onuki <omakoto@google.com> | 2012-04-20 14:45:07 -0700 |
commit | 0992b9d4969ed0eee6e879db94292b635229e2b7 (patch) | |
tree | 6759f884a9f84441a695235988a715e7cdf19e9a /src/com/android | |
parent | 18cb61aa592aa2c02c7f583f608ad0f8832f5742 (diff) | |
download | packages_providers_ContactsProvider-0992b9d4969ed0eee6e879db94292b635229e2b7.zip packages_providers_ContactsProvider-0992b9d4969ed0eee6e879db94292b635229e2b7.tar.gz packages_providers_ContactsProvider-0992b9d4969ed0eee6e879db94292b635229e2b7.tar.bz2 |
New aggregation logic
Now a raw contact can be merged into a contact containing raw contacts from
the same account if there's at least one raw contact in those raw contacts
that shares at least one email address, phone number, or identity.
Now the aggregation logic version is 3.
Also:
- Make sure changes to the identity triggers aggregation.
- Stop re-using some HashMaps/HashSets, as they don't shirink the internal
table when clear()ed. During the aggregation update we may put a bunch of
stuff into those, and we want to make sure that we don't keep unnecessarily
bit internal tables after the upgrade. This should be okay with the modern
dalvik GC.
Change-Id: I855085d334679363cf9bffb918ca2ceb0cfe77f5
Diffstat (limited to 'src/com/android')
7 files changed, 418 insertions, 132 deletions
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index 1cba421..fb00a94 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -114,6 +114,7 @@ import android.provider.ContactsContract.AggregationExceptions; import android.provider.ContactsContract.Authorization; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.CommonDataKinds.Identity; import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Nickname; import android.provider.ContactsContract.CommonDataKinds.Note; @@ -213,7 +214,7 @@ public class ContactsProvider2 extends AbstractContactsProvider private static final String PREF_LOCALE = "locale"; - private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 2; + private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 3; private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate"; @@ -1508,6 +1509,8 @@ public class ContactsProvider2 extends AbstractContactsProvider new DataRowHandlerForPhoto(context, dbHelper, contactAggregator, photoStore)); handlerMap.put(Note.CONTENT_ITEM_TYPE, new DataRowHandlerForNote(context, dbHelper, contactAggregator)); + handlerMap.put(Identity.CONTENT_ITEM_TYPE, + new DataRowHandlerForIdentity(context, dbHelper, contactAggregator)); } @VisibleForTesting @@ -1821,14 +1824,6 @@ public class ContactsProvider2 extends AbstractContactsProvider return mMaxThumbnailPhotoDim; } - /* package */ NameSplitter getNameSplitter() { - return mNameSplitter; - } - - /* package */ NameLookupBuilder getNameLookupBuilder() { - return mNameLookupBuilder; - } - @VisibleForTesting public ContactDirectoryManager getContactDirectoryManagerForTest() { return mContactDirectoryManager; @@ -2119,10 +2114,10 @@ public class ContactsProvider2 extends AbstractContactsProvider } if (inProfileMode()) { mProfileAggregator.clearPendingAggregations(); - mProfileTransactionContext.clear(); + mProfileTransactionContext.clearExceptSearchIndexUpdates(); } else { mContactAggregator.clearPendingAggregations(); - mContactTransactionContext.clear(); + mContactTransactionContext.clearExceptSearchIndexUpdates(); } } @@ -2207,7 +2202,7 @@ public class ContactsProvider2 extends AbstractContactsProvider } } - mTransactionContext.get().clear(); + mTransactionContext.get().clearExceptSearchIndexUpdates(); } /** @@ -7905,44 +7900,46 @@ public class ContactsProvider2 extends AbstractContactsProvider protected void upgradeAggregationAlgorithmInBackground() { Log.i(TAG, "Upgrading aggregation algorithm"); - int count = 0; + final long start = SystemClock.elapsedRealtime(); - SQLiteDatabase db = null; + setProviderStatus(ProviderStatus.STATUS_UPGRADING); + + // Re-aggregate all visible raw contacts. try { - switchToContactMode(); - db = mContactsHelper.getWritableDatabase(); - mActiveDb.set(db); - db.beginTransaction(); - Cursor cursor = db.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." + RawContactsColumns.ACCOUNT_ID + - "=r2." + RawContactsColumns.ACCOUNT_ID, - null, null, null, null, null); + int count = 0; + SQLiteDatabase db = null; + boolean success = false; try { - while (cursor.moveToNext()) { - long rawContactId = cursor.getLong(0); - mContactAggregator.markForAggregation(rawContactId, - RawContacts.AGGREGATION_MODE_DEFAULT, true); - count++; - } + // Re-aggregation os only for the contacts DB. + switchToContactMode(); + db = mContactsHelper.getWritableDatabase(); + mActiveDb.set(db); + + // Start the actual process. + db.beginTransaction(); + + count = mContactAggregator.markAllVisibleForAggregation(db); + mContactAggregator.aggregateInTransaction(mTransactionContext.get(), db); + + updateSearchIndexInTransaction(); + + mContactsHelper.setProperty(DbProperties.AGGREGATION_ALGORITHM, + String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION)); + + db.setTransactionSuccessful(); + + success = true; } finally { - cursor.close(); - } - mContactAggregator.aggregateInTransaction(mTransactionContext.get(), db); - updateSearchIndexInTransaction(); - db.setTransactionSuccessful(); - mContactsHelper.setProperty(DbProperties.AGGREGATION_ALGORITHM, - String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION)); - } finally { - if (db != null) { - db.endTransaction(); + mTransactionContext.get().clearAll(); + if (db != null) { + db.endTransaction(); + } + final long end = SystemClock.elapsedRealtime(); + Log.i(TAG, "Aggregation algorithm upgraded for " + count + " raw contacts" + + (success ? (" in " + (end - start) + "ms") : " failed")); } - final long end = SystemClock.elapsedRealtime(); - Log.i(TAG, "Aggregation algorithm upgraded for " + count - + " contacts, in " + (end - start) + "ms"); + } finally { // Need one more finally because endTransaction() may fail. + setProviderStatus(ProviderStatus.STATUS_NORMAL); } } diff --git a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java new file mode 100644 index 0000000..440e430 --- /dev/null +++ b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License + */ +package com.android.providers.contacts; + +import com.android.providers.contacts.aggregation.ContactAggregator; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.provider.ContactsContract.CommonDataKinds.Identity; + +/** + * Handler for Identity data rows. + */ +public class DataRowHandlerForIdentity extends DataRowHandler { + public DataRowHandlerForIdentity( + Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) { + super(context, dbHelper, aggregator, Identity.CONTENT_ITEM_TYPE); + } + + @Override + public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, + ContentValues values) { + final long dataId = super.insert(db, txContext, rawContactId, values); + + // Identity affects aggregation. + if (values.containsKey(Identity.IDENTITY) || values.containsKey(Identity.NAMESPACE)) { + triggerAggregation(txContext, rawContactId); + } + + return dataId; + } + + @Override + public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, + Cursor c, boolean callerIsSyncAdapter) { + + super.update(db, txContext, values, c, callerIsSyncAdapter); + + // Identity affects aggregation. + final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); + if (values.containsKey(Identity.IDENTITY) || values.containsKey(Identity.NAMESPACE)) { + triggerAggregation(txContext, rawContactId); + } + + return true; + } + + @Override + public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) { + final int count = super.delete(db, txContext, c); + + // Identity affects aggregation. + final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); + triggerAggregation(txContext, rawContactId); + + return count; + } +} diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java index 2ee61e4..99313e9 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java +++ b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java @@ -50,9 +50,8 @@ public class DataRowHandlerForPhoneNumber extends DataRowHandlerForCommonDataKin updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); mContactAggregator.updateHasPhoneNumber(db, rawContactId); fixRawContactDisplayName(db, txContext, rawContactId); - if (normalizedNumber != null) { - triggerAggregation(txContext, rawContactId); - } + + triggerAggregation(txContext, rawContactId); } return dataId; } @@ -74,8 +73,10 @@ public class DataRowHandlerForPhoneNumber extends DataRowHandlerForCommonDataKin values.getAsString(Phone.NORMALIZED_NUMBER)); mContactAggregator.updateHasPhoneNumber(db, rawContactId); fixRawContactDisplayName(db, txContext, rawContactId); + triggerAggregation(txContext, rawContactId); } + return true; } diff --git a/src/com/android/providers/contacts/SearchIndexManager.java b/src/com/android/providers/contacts/SearchIndexManager.java index f1b7338..5ca9859 100644 --- a/src/com/android/providers/contacts/SearchIndexManager.java +++ b/src/com/android/providers/contacts/SearchIndexManager.java @@ -45,6 +45,8 @@ import java.util.regex.Pattern; public class SearchIndexManager { private static final String TAG = "ContactsFTS"; + private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); + public static final String PROPERTY_SEARCH_INDEX_VERSION = "search_index"; private static final int SEARCH_INDEX_VERSION = 1; @@ -253,33 +255,37 @@ public class SearchIndexManager { } public void updateIndexForRawContacts(Set<Long> contactIds, Set<Long> rawContactIds) { - mSb.setLength(0); - mSb.append("("); + if (VERBOSE_LOGGING) { + Log.v(TAG, "Updating search index for " + contactIds.size() + + " contacts / " + rawContactIds.size() + " raw contacts"); + } + StringBuilder sb = new StringBuilder(); + sb.append("("); if (!contactIds.isEmpty()) { - mSb.append(RawContacts.CONTACT_ID + " IN ("); + sb.append(RawContacts.CONTACT_ID + " IN ("); for (Long contactId : contactIds) { - mSb.append(contactId).append(","); + sb.append(contactId).append(","); } - mSb.setLength(mSb.length() - 1); - mSb.append(')'); + sb.setLength(sb.length() - 1); + sb.append(')'); } if (!rawContactIds.isEmpty()) { if (!contactIds.isEmpty()) { - mSb.append(" OR "); + sb.append(" OR "); } - mSb.append(RawContactsColumns.CONCRETE_ID + " IN ("); + sb.append(RawContactsColumns.CONCRETE_ID + " IN ("); for (Long rawContactId : rawContactIds) { - mSb.append(rawContactId).append(","); + sb.append(rawContactId).append(","); } - mSb.setLength(mSb.length() - 1); - mSb.append(')'); + sb.setLength(sb.length() - 1); + sb.append(')'); } - mSb.append(")"); + sb.append(")"); // The selection to select raw_contacts. - final String rawContactsSelection = mSb.toString(); + final String rawContactsSelection = sb.toString(); // Remove affected search_index rows. final SQLiteDatabase db = mDbHelper.getWritableDatabase(); @@ -292,7 +298,10 @@ public class SearchIndexManager { , null); // Then rebuild index for them. - buildAndInsertIndex(db, rawContactsSelection); + final int count = buildAndInsertIndex(db, rawContactsSelection); + if (VERBOSE_LOGGING) { + Log.v(TAG, "Updated search index for " + count + " contacts"); + } } private int buildAndInsertIndex(SQLiteDatabase db, String selection) { diff --git a/src/com/android/providers/contacts/TransactionContext.java b/src/com/android/providers/contacts/TransactionContext.java index 4f880f7..2bbacf0 100644 --- a/src/com/android/providers/contacts/TransactionContext.java +++ b/src/com/android/providers/contacts/TransactionContext.java @@ -21,7 +21,6 @@ import com.google.android.collect.Sets; import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -33,12 +32,12 @@ public class TransactionContext { private final boolean mForProfile; /** Map from raw contact id to account Id */ - private HashMap<Long, Long> mInsertedRawContactsAccounts = Maps.newHashMap(); - private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet(); - private HashSet<Long> mDirtyRawContacts = Sets.newHashSet(); - private HashSet<Long> mStaleSearchIndexRawContacts = Sets.newHashSet(); - private HashSet<Long> mStaleSearchIndexContacts = Sets.newHashSet(); - private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap(); + private HashMap<Long, Long> mInsertedRawContactsAccounts; + private HashSet<Long> mUpdatedRawContacts; + private HashSet<Long> mDirtyRawContacts; + private HashSet<Long> mStaleSearchIndexRawContacts; + private HashSet<Long> mStaleSearchIndexContacts; + private HashMap<Long, Object> mUpdatedSyncStates; public TransactionContext(boolean forProfile) { mForProfile = forProfile; @@ -49,70 +48,89 @@ public class TransactionContext { } public void rawContactInserted(long rawContactId, long accountId) { + if (mInsertedRawContactsAccounts == null) mInsertedRawContactsAccounts = Maps.newHashMap(); mInsertedRawContactsAccounts.put(rawContactId, accountId); } public void rawContactUpdated(long rawContactId) { + if (mUpdatedRawContacts == null) mUpdatedRawContacts = Sets.newHashSet(); mUpdatedRawContacts.add(rawContactId); } public void markRawContactDirty(long rawContactId) { + if (mDirtyRawContacts == null) mDirtyRawContacts = Sets.newHashSet(); mDirtyRawContacts.add(rawContactId); } public void syncStateUpdated(long rowId, Object data) { + if (mUpdatedSyncStates == null) mUpdatedSyncStates = Maps.newHashMap(); mUpdatedSyncStates.put(rowId, data); } public void invalidateSearchIndexForRawContact(long rawContactId) { + if (mStaleSearchIndexRawContacts == null) mStaleSearchIndexRawContacts = Sets.newHashSet(); mStaleSearchIndexRawContacts.add(rawContactId); } public void invalidateSearchIndexForContact(long contactId) { + if (mStaleSearchIndexContacts == null) mStaleSearchIndexContacts = Sets.newHashSet(); mStaleSearchIndexContacts.add(contactId); } public Set<Long> getInsertedRawContactIds() { + if (mInsertedRawContactsAccounts == null) mInsertedRawContactsAccounts = Maps.newHashMap(); return mInsertedRawContactsAccounts.keySet(); } public Set<Long> getUpdatedRawContactIds() { + if (mUpdatedRawContacts == null) mUpdatedRawContacts = Sets.newHashSet(); return mUpdatedRawContacts; } public Set<Long> getDirtyRawContactIds() { + if (mDirtyRawContacts == null) mDirtyRawContacts = Sets.newHashSet(); return mDirtyRawContacts; } public Set<Long> getStaleSearchIndexRawContactIds() { + if (mStaleSearchIndexRawContacts == null) mStaleSearchIndexRawContacts = Sets.newHashSet(); return mStaleSearchIndexRawContacts; } public Set<Long> getStaleSearchIndexContactIds() { + if (mStaleSearchIndexContacts == null) mStaleSearchIndexContacts = Sets.newHashSet(); return mStaleSearchIndexContacts; } public Set<Entry<Long, Object>> getUpdatedSyncStates() { + if (mUpdatedSyncStates == null) mUpdatedSyncStates = Maps.newHashMap(); return mUpdatedSyncStates.entrySet(); } public Long getAccountIdOrNullForRawContact(long rawContactId) { + if (mInsertedRawContactsAccounts == null) mInsertedRawContactsAccounts = Maps.newHashMap(); return mInsertedRawContactsAccounts.get(rawContactId); } public boolean isNewRawContact(long rawContactId) { + if (mInsertedRawContactsAccounts == null) mInsertedRawContactsAccounts = Maps.newHashMap(); return mInsertedRawContactsAccounts.containsKey(rawContactId); } - public void clear() { - mInsertedRawContactsAccounts.clear(); - mUpdatedRawContacts.clear(); - mUpdatedSyncStates.clear(); - mDirtyRawContacts.clear(); + public void clearExceptSearchIndexUpdates() { + mInsertedRawContactsAccounts = null; + mUpdatedRawContacts = null; + mUpdatedSyncStates = null; + mDirtyRawContacts = null; } public void clearSearchIndexUpdates() { - mStaleSearchIndexRawContacts.clear(); - mStaleSearchIndexContacts.clear(); + mStaleSearchIndexRawContacts = null; + mStaleSearchIndexContacts = null; + } + + public void clearAll() { + clearExceptSearchIndexUpdates(); + clearSearchIndexUpdates(); } } diff --git a/src/com/android/providers/contacts/aggregation/ContactAggregator.java b/src/com/android/providers/contacts/aggregation/ContactAggregator.java index 60eba95..1d24b92 100644 --- a/src/com/android/providers/contacts/aggregation/ContactAggregator.java +++ b/src/com/android/providers/contacts/aggregation/ContactAggregator.java @@ -30,9 +30,6 @@ 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.aggregation.util.CommonNicknameCache; -import com.android.providers.contacts.aggregation.util.ContactMatcher; -import com.android.providers.contacts.aggregation.util.ContactMatcher.MatchScore; import com.android.providers.contacts.ContactsProvider2; import com.android.providers.contacts.NameLookupBuilder; import com.android.providers.contacts.NameNormalizer; @@ -40,6 +37,10 @@ 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.ContactMatcher; +import com.android.providers.contacts.aggregation.util.ContactMatcher.MatchScore; +import com.google.android.collect.Maps; import android.database.Cursor; import android.database.DatabaseUtils; @@ -81,6 +82,7 @@ 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 = @@ -154,12 +156,11 @@ public class ContactAggregator { private SQLiteStatement mContactUpdate; private SQLiteStatement mContactInsert; - private HashMap<Long, Integer> mRawContactsMarkedForAggregation = new HashMap<Long, Integer>(); + private HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap(); private String[] mSelectionArgs1 = new String[1]; private String[] mSelectionArgs2 = new String[2]; private String[] mSelectionArgs3 = new String[3]; - private String[] mSelectionArgs4 = new String[4]; private long mMimeTypeIdIdentity; private long mMimeTypeIdEmail; private long mMimeTypeIdPhoto; @@ -398,39 +399,48 @@ public class ContactAggregator { * Call just before committing the transaction. */ public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) { - int count = mRawContactsMarkedForAggregation.size(); - if (count == 0) { + final int markedCount = mRawContactsMarkedForAggregation.size(); + if (markedCount == 0) { return; } - long start = System.currentTimeMillis(); - if (VERBOSE_LOGGING) { - Log.v(TAG, "Contact aggregation: " + count); + final long start = System.currentTimeMillis(); + if (DEBUG_LOGGING) { + Log.d(TAG, "aggregateInTransaction for " + markedCount + " contacts"); } - EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -count); - - String selectionArgs[] = new String[count]; + EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -markedCount); int index = 0; - mSb.setLength(0); - mSb.append(AggregationQuery.SQL); + + // 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) { - mSb.append(','); + sbQuery.append(','); } - mSb.append('?'); - selectionArgs[index++] = String.valueOf(rawContactId); + sbQuery.append(rawContactId); + index++; } - mSb.append(')'); + sbQuery.append(')'); - long rawContactIds[] = new long[count]; - long contactIds[] = new long[count]; - long accountIds[] = new long[count]; - Cursor c = db.rawQuery(mSb.toString(), selectionArgs); + final long[] rawContactIds; + final long[] contactIds; + final long[] accountIds; + final int actualCount; + final Cursor c = db.rawQuery(sbQuery.toString(), null); try { - count = c.getCount(); + 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); @@ -442,17 +452,22 @@ public class ContactAggregator { c.close(); } - for (int i = 0; i < count; i++) { + 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, count); + EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, actualCount); - if (VERBOSE_LOGGING) { - String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact"; - Log.i(TAG, "Contact aggregation complete: " + count + performance); + if (DEBUG_LOGGING) { + Log.d(TAG, "Contact aggregation complete: " + actualCount + + (actualCount == 0 ? "" : ", " + (elapsedTime / actualCount) + + " ms per raw contact")); } } @@ -489,7 +504,9 @@ public class ContactAggregator { } public void clearPendingAggregations() { - mRawContactsMarkedForAggregation.clear(); + // 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) { @@ -539,6 +556,8 @@ public class ContactAggregator { 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); } @@ -549,6 +568,46 @@ public class ContactAggregator { } /** + * 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. */ @@ -646,6 +705,10 @@ public class ContactAggregator { 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); @@ -653,7 +716,7 @@ public class ContactAggregator { aggregationMode = aggModeObject; } - long contactId = -1; + long contactId = -1; // Best matching contact ID. long contactIdToSplit = -1; if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) { @@ -674,7 +737,7 @@ public class ContactAggregator { // the same account, not only will we not join it, but also we will split // that other aggregate if (contactId != -1 && contactId != currentContactId && - containsRawContactsFromAccount(db, contactId, accountId)) { + !canJoinIntoContact(db, contactId, rawContactId, accountId)) { contactIdToSplit = contactId; contactId = -1; } @@ -683,6 +746,8 @@ public class ContactAggregator { return; } + // # of raw_contacts in the [currentContactId] contact excluding the [rawContactId] + // raw_contact. long currentContactContentsCount = 0; if (currentContactId != 0) { @@ -734,19 +799,138 @@ public class ContactAggregator { } /** - * Returns true if the aggregate contains has any raw contacts from the specified account. + * @return true if the raw contact of {@code rawContactId} can be joined into the existing + * contact of {@code of contactId}. + * + * Now a raw contact can be merged into a contact containing raw contacts from + * the same account if there's at least one raw contact in those raw contacts + * that shares at least one email address, phone number, or identity. */ - private boolean containsRawContactsFromAccount( - SQLiteDatabase db, long contactId, long accountId) { - final String query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS + + private boolean canJoinIntoContact(SQLiteDatabase db, long contactId, + long rawContactId, long rawContactAccountId) { + // First, list all raw contact IDs in contact [contactId] on account [rawContactAccountId], + // excluding raw_contact [rawContactId]. + + // Append all found raw contact IDs into this SB to create a comma separated list of + // the IDs. + // We don't always need it, so lazily initialize it. + StringBuilder rawContactIdsBuilder; + + mSelectionArgs3[0] = String.valueOf(contactId); + mSelectionArgs3[1] = String.valueOf(rawContactId); + mSelectionArgs3[2] = String.valueOf(rawContactAccountId); + final Cursor duplicatesCursor = db.rawQuery( + "SELECT " + RawContacts._ID + + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=?" + - " AND " + RawContactsColumns.ACCOUNT_ID + "=?"; - Cursor cursor = db.rawQuery(query, new String[] { - Long.toString(contactId), Long.toString(accountId) - }); + " AND " + RawContacts._ID + "!=?" + + " AND " + RawContactsColumns.ACCOUNT_ID +"=?", + mSelectionArgs3); try { - cursor.moveToFirst(); - return cursor.getInt(0) != 0; + final int duplicateCount = duplicatesCursor.getCount(); + if (duplicateCount == 0) { + return true; // No duplicates -- common case -- bail early. + } + if (VERBOSE_LOGGING) { + Log.v(TAG, "canJoinIntoContact: " + duplicateCount + " duplicate(s) found"); + } + + rawContactIdsBuilder = new StringBuilder(); + + duplicatesCursor.moveToPosition(-1); + while (duplicatesCursor.moveToNext()) { + if (rawContactIdsBuilder.length() > 0) { + rawContactIdsBuilder.append(','); + } + rawContactIdsBuilder.append(duplicatesCursor.getLong(0)); + } + } finally { + duplicatesCursor.close(); + } + + // Comma separated raw_contacts IDs. + final String rawContactIds = rawContactIdsBuilder.toString(); + + // See if there's any raw_contacts that share an email address, a phone number, or + // an identity with raw_contact [rawContactId]. + + // First, check for the email address. + mSelectionArgs2[0] = String.valueOf(mMimeTypeIdEmail); + mSelectionArgs2[1] = String.valueOf(rawContactId); + if (isFirstColumnGreaterThanZero(db, + "SELECT count(*)" + + " FROM " + Tables.DATA + " AS d1" + + " JOIN " + Tables.DATA + " AS d2" + + " ON (d1." + Email.ADDRESS + " = d2." + Email.ADDRESS + ")" + + " WHERE d1." + DataColumns.MIMETYPE_ID + " = ?1" + + " AND d2." + DataColumns.MIMETYPE_ID + " = ?1" + + " AND d1." + Data.RAW_CONTACT_ID + " = ?2" + + " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")", + mSelectionArgs2)) { + if (VERBOSE_LOGGING) { + Log.v(TAG, "Relaxing rule SA: email match found for rid=" + rawContactId); + } + return true; + } + + // Next, check for the identity. + mSelectionArgs2[0] = String.valueOf(mMimeTypeIdIdentity); + mSelectionArgs2[1] = String.valueOf(rawContactId); + if (isFirstColumnGreaterThanZero(db, + "SELECT count(*)" + + " FROM " + Tables.DATA + " AS d1" + + " JOIN " + Tables.DATA + " AS d2" + + " ON (d1." + Identity.IDENTITY + " = d2." + Identity.IDENTITY + " AND" + + " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" + + " WHERE d1." + DataColumns.MIMETYPE_ID + " = ?1" + + " AND d2." + DataColumns.MIMETYPE_ID + " = ?1" + + " AND d1." + Data.RAW_CONTACT_ID + " = ?2" + + " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")", + mSelectionArgs2)) { + if (VERBOSE_LOGGING) { + Log.v(TAG, "Relaxing rule SA: identity match found for rid=" + rawContactId); + } + return true; + } + + // Lastly, the phone number. + // It's a bit tricker because it has to be consistent with + // updateMatchScoresBasedOnPhoneMatches(). + mSelectionArgs3[0] = String.valueOf(mMimeTypeIdPhone); + mSelectionArgs3[1] = String.valueOf(rawContactId); + mSelectionArgs3[2] = String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter()); + + if (isFirstColumnGreaterThanZero(db, + "SELECT count(*)" + + " 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 + " = ?1" + + " AND d2." + DataColumns.MIMETYPE_ID + " = ?1" + + " AND d1." + Data.RAW_CONTACT_ID + " = ?2" + + " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")" + + " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + ",?3)", + mSelectionArgs3)) { + if (VERBOSE_LOGGING) { + Log.v(TAG, "Relaxing rule SA: phone match found for rid=" + rawContactId); + } + return true; + } + if (VERBOSE_LOGGING) { + Log.v(TAG, "Rule SA splitting up cid=" + contactId + " for rid=" + rawContactId); + } + return false; + } + + private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query, + String[] selectionArgs) { + final Cursor cursor = db.rawQuery(query, selectionArgs); + try { + return cursor.moveToFirst() && (cursor.getInt(0) > 0); } finally { cursor.close(); } @@ -1167,11 +1351,11 @@ public class ContactAggregator { " ON (dataB." + Data.RAW_CONTACT_ID + " = " + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" - + " AND dataA." + DataColumns.MIMETYPE_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 + "=?" + + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2" + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; @@ -1187,11 +1371,11 @@ public class ContactAggregator { */ private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId, ContactMatcher matcher) { - mSelectionArgs3[0] = String.valueOf(rawContactId); - mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdIdentity); + mSelectionArgs2[0] = String.valueOf(rawContactId); + mSelectionArgs2[1] = String.valueOf(mMimeTypeIdIdentity); Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS, IdentityLookupMatchQuery.SELECTION, - mSelectionArgs3, RawContacts.CONTACT_ID, null, null); + mSelectionArgs2, RawContacts.CONTACT_ID, null, null); try { while (c.moveToNext()) { final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID); @@ -1360,10 +1544,10 @@ public class ContactAggregator { " ON (dataB." + Data.RAW_CONTACT_ID + " = " + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")"; - String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?" - + " AND dataA." + DataColumns.MIMETYPE_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 + "=?" + + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2" + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0" + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; @@ -1376,11 +1560,11 @@ public class ContactAggregator { private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, ContactMatcher matcher) { - mSelectionArgs3[0] = String.valueOf(rawContactId); - mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdEmail); + mSelectionArgs2[0] = String.valueOf(rawContactId); + mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail); Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS, EmailLookupQuery.SELECTION, - mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING); + mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING); try { while (c.moveToNext()) { long contactId = c.getLong(EmailLookupQuery.CONTACT_ID); diff --git a/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java b/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java index 60e41ab..a29735d 100644 --- a/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java +++ b/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java @@ -381,7 +381,9 @@ public class ContactMatcher { /** * Returns the contactId with the best match score over the specified threshold or -1 - * if no such contact is found. + * if no such contact is found. If multiple contacts are found, and + * {@code allowMultipleMatches} is {@code true}, it returns the first one found, but if + * {@code allowMultipleMatches} is {@code false} it'll return {@link #MULTIPLE_MATCHES}. */ public long pickBestMatch(int threshold, boolean allowMultipleMatches) { long contactId = -1; @@ -405,7 +407,9 @@ public class ContactMatcher { if (contactId != -1 && !allowMultipleMatches) { return MULTIPLE_MATCHES; } - if (s > maxScore) { + // 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; maxScore = s; } |