summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java87
-rw-r--r--src/com/android/providers/contacts/DataRowHandlerForIdentity.java73
-rw-r--r--src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java7
-rw-r--r--src/com/android/providers/contacts/SearchIndexManager.java37
-rw-r--r--src/com/android/providers/contacts/TransactionContext.java46
-rw-r--r--src/com/android/providers/contacts/aggregation/ContactAggregator.java292
-rw-r--r--src/com/android/providers/contacts/aggregation/util/ContactMatcher.java8
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;
}