diff options
Diffstat (limited to 'src/com/android/providers')
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; } |