Merge "Fix bugs in aggregator2." into mnc-dev
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 8fce6a6..c4da2e8 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -1534,6 +1534,18 @@
         return true;
     }
 
+    @VisibleForTesting
+    public void setNewAggregatorForTest(boolean enabled) {
+        mContactAggregator = (enabled)
+                ? new ContactAggregator2(this, mContactsHelper,
+                createPhotoPriorityResolver(getContext()), mNameSplitter, mCommonNicknameCache)
+                : new ContactAggregator(this, mContactsHelper,
+                createPhotoPriorityResolver(getContext()), mNameSplitter, mCommonNicknameCache);
+        mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
+        initDataRowHandlers(mDataRowHandlers, mContactsHelper, mContactAggregator,
+                mContactsPhotoStore);
+    }
+
     // Updates the locale set to reflect a new system locale.
     private static LocaleSet updateLocaleSet(LocaleSet oldLocales, Locale newLocale) {
         final Locale prevLocale = oldLocales.getPrimaryLocale();
diff --git a/src/com/android/providers/contacts/aggregation/ContactAggregator2.java b/src/com/android/providers/contacts/aggregation/ContactAggregator2.java
index d048609..f696950 100644
--- a/src/com/android/providers/contacts/aggregation/ContactAggregator2.java
+++ b/src/com/android/providers/contacts/aggregation/ContactAggregator2.java
@@ -147,9 +147,14 @@
         RawContactMatcher matcher = new RawContactMatcher();
         RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates();
         if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
-            // Find the set of matching candidates
-            matchingCandidates = findRawContactMatchingCandidates(db, rawContactId, candidates,
-                    matcher);
+            // If this is a newly inserted contact or a visible contact, look for
+            // data matches.
+            if (currentContactId == 0
+                    || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) {
+                // Find the set of matching candidates
+                matchingCandidates = findRawContactMatchingCandidates(db, rawContactId, candidates,
+                        matcher);
+            }
         } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
             return;
         }
@@ -223,11 +228,10 @@
      */
     private RawContactMatchingCandidates findRawContactMatchingCandidates(SQLiteDatabase db, long
             rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
-        updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
+        updateMatchScores(db, rawContactId, candidates,
                 matcher);
         final RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates(
-                matcher.pickBestMatches(SCORE_THRESHOLD_SUGGEST));
-
+                matcher.pickBestMatches());
         Set<Long> newIds = new HashSet<>();
         newIds.addAll(matchingCandidates.getRawContactIdSet());
         // Keep doing the following until no new raw contact candidate is found.
@@ -236,9 +240,9 @@
             final Set<Long> tmpIdSet = new HashSet<>();
             for (long rId : newIds) {
                 final RawContactMatcher rMatcher = new RawContactMatcher();
-                updateMatchScoresForSuggestionsBasedOnDataMatches(db, rId, new MatchCandidateList(),
+                updateMatchScores(db, rId, new MatchCandidateList(),
                         rMatcher);
-                List<MatchScore> newMatches = rMatcher.pickBestMatches(SCORE_THRESHOLD_SUGGEST);
+                List<MatchScore> newMatches = rMatcher.pickBestMatches();
                 for (MatchScore newMatch : newMatches) {
                     final long newRawContactId = newMatch.getRawContactId();
                     if (!matchingCandidates.getRawContactIdSet().contains(newRawContactId)) {
@@ -260,12 +264,10 @@
      */
      private void clearSuperPrimarySetting(SQLiteDatabase db, String rawContactIds) {
         final String sql =
-                "SELECT d." + DataColumns.MIMETYPE_ID + ", count(DISTINCT r." +
-                        RawContacts.CONTACT_ID + ") c  FROM " + Tables.DATA + " d JOIN " +
-                        Tables.RAW_CONTACTS + " r on d." + Data.RAW_CONTACT_ID + " = r." +
-                        RawContacts._ID +" WHERE d." + Data.IS_SUPER_PRIMARY + " = 1 AND d." +
-                        Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ") group by d." +
-                        DataColumns.MIMETYPE_ID + " having c > 1";
+                "SELECT " + DataColumns.MIMETYPE_ID + ", count(1) c  FROM " +
+                        Tables.DATA +" WHERE " + Data.IS_SUPER_PRIMARY + " = 1 AND " +
+                        Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ") group by " +
+                        DataColumns.MIMETYPE_ID + " HAVING c > 1";
 
         // Find out which mime-types exist with is_super_primary=true on more then one contacts.
         int index = 0;
@@ -294,13 +296,12 @@
         // in both raw contact of rawContactId and raw contacts of contactId
         String superPrimaryUpdateSql = "UPDATE " + Tables.DATA +
                 " SET " + Data.IS_SUPER_PRIMARY + "=0" +
-                " WHERE (" +  Data.RAW_CONTACT_ID +
-                " IN (SELECT " + RawContacts._ID +  " FROM " + Tables.RAW_CONTACTS +
-                " WHERE " + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")";
+                " WHERE " +  Data.RAW_CONTACT_ID +
+                " IN (" + rawContactIds + ")";
 
         mimeTypeCondition.append(')');
         superPrimaryUpdateSql += mimeTypeCondition.toString();
-        db.execSQL(superPrimaryUpdateSql, null);
+        db.execSQL(superPrimaryUpdateSql);
     }
 
     private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
@@ -357,6 +358,12 @@
                         (currentContactContentsCount == 0) ||
                         canBeReused(db, currentCidForRawContact, connectedRawContactIds)) {
                     contactId = currentCidForRawContact;
+                    for (Long connectedRawContactId : connectedRawContactIds) {
+                        Long cid = matchingCandidates.getContactId(connectedRawContactId);
+                        if (cid != null && cid != contactId) {
+                            cidsNeedToBeUpdated.add(cid);
+                        }
+                    }
                 } else if (currentCidForRawContact != 0){
                     cidsNeedToBeUpdated.add(currentCidForRawContact);
                 }
@@ -373,8 +380,9 @@
                     }
                 }
             }
-            createContactForRawContacts(db, txContext, connectedRawContactIds, contactId);
             clearSuperPrimarySetting(db, TextUtils.join(",", connectedRawContactIds));
+            createContactForRawContacts(db, txContext, connectedRawContactIds, contactId);
+
             for (Long cid : cidsNeedToBeUpdated) {
                 long currentRcCount = 0;
                 if (cid != 0) {
@@ -850,12 +858,13 @@
     }
 
     /**
-     * Computes scores for contacts that have matching data rows.
+     * Computes suggestion scores for contacts that have matching data rows.
+     * Aggregation suggestion doesn't consider aggregation exceptions, but is purely based on the
+     * raw contacts information.
      */
     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
             long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
 
-        updateMatchScoresBasedOnExceptions(db, rawContactId, matcher);
         updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
         updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
@@ -864,6 +873,19 @@
         lookupApproximateNameMatches(db, candidates, matcher);
     }
 
+    /**
+     * Computes scores for contacts that have matching data rows.
+     */
+    private void updateMatchScores(SQLiteDatabase db, long rawContactId,
+            MatchCandidateList candidates, RawContactMatcher matcher) {
+        updateMatchScoresBasedOnExceptions(db, rawContactId, matcher);
+        updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
+        updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+        updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+        updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+        updateMatchScoresBasedOnSecondaryData(db, rawContactId, candidates, matcher);
+    }
+
     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
             MatchCandidateList candidates, RawContactMatcher matcher,
             ArrayList<AggregationSuggestionParameter> parameters) {
@@ -875,4 +897,35 @@
             // TODO: add support for other parameter kinds
         }
     }
+
+    /**
+     * Update scores for matches with secondary data matching but insufficient primary scores.
+     * This method loads structured names for all candidate contacts and recomputes match scores
+     * using approximate matching.
+     */
+    private void updateMatchScoresBasedOnSecondaryData(SQLiteDatabase db,
+            long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
+        final List<Long> secondaryRawContactIds = matcher.prepareSecondaryMatchCandidates();
+        if (secondaryRawContactIds == null || secondaryRawContactIds.size() > SECONDARY_HIT_LIMIT) {
+            return;
+        }
+
+        loadNameMatchCandidates(db, rawContactId, candidates, true);
+
+        mSb.setLength(0);
+        mSb.append(RawContacts._ID).append(" IN (");
+        for (int i = 0; i < secondaryRawContactIds.size(); i++) {
+            if (i != 0) {
+                mSb.append(',');
+            }
+            mSb.append(secondaryRawContactIds.get(i));
+        }
+
+        // We only want to compare structured names to structured names
+        // at this stage, we need to ignore all other sources of name lookup data.
+        mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL);
+
+        matchAllCandidates(db, mSb.toString(), candidates, matcher,
+                RawContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
+    }
 }
diff --git a/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java b/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java
index 1abcfa1..88aa226 100644
--- a/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java
+++ b/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java
@@ -43,9 +43,6 @@
     // and there is a secondary match (phone number, email etc).
     public static final int SCORE_THRESHOLD_SECONDARY = 50;
 
-    // Score for missing data (as opposed to present data but a bad match)
-    private static final int NO_DATA_SCORE = -1;
-
     // Score for matching phone numbers
     private static final int PHONE_MATCH_SCORE = 71;
 
@@ -280,6 +277,55 @@
         mScores.clear();
         mScoreCount = 0;
     }
+    /**
+     * Returns a list of IDs for raw contacts that are matched on secondary data elements
+     * (phone number, email address, nickname). We still need to obtain the approximate
+     * primary score for those contacts to determine if any of them should be aggregated.
+     * <p>
+     * May return null.
+     */
+    public List<Long> prepareSecondaryMatchCandidates() {
+        ArrayList<Long> rawContactIds = null;
+
+        for (int i = 0; i < mScoreCount; i++) {
+            MatchScore score = mScoreList.get(i);
+            if (score.isKeepOut() ||  score.getPrimaryScore() > SCORE_THRESHOLD_PRIMARY){
+                continue;
+            }
+
+            if (score.getSecondaryScore() >= SCORE_THRESHOLD_PRIMARY) {
+                if (rawContactIds == null) {
+                    rawContactIds = new ArrayList<Long>();
+                }
+                rawContactIds.add(score.getRawContactId());
+            }
+            score.setPrimaryScore(0);
+        }
+        return rawContactIds;
+    }
+
+    /**
+     * Returns the list of raw contact Ids with the match score over threshold.
+     */
+    public List<MatchScore> pickBestMatches() {
+        final List<MatchScore> matches = new ArrayList<>();
+        for (int i = 0; i < mScoreCount; i++) {
+            MatchScore score = mScoreList.get(i);
+            if (score.isKeepOut()) {
+                continue;
+            }
+
+            if (score.isKeepIn()) {
+                matches.add(score);
+                continue;
+            }
+
+            if (score.getPrimaryScore() >= SCORE_THRESHOLD_SECONDARY) {
+                matches.add(score);
+            }
+        }
+        return matches;
+    }
 
     /**
      * Returns matches in the order of descending score.
diff --git a/tests/src/com/android/providers/contacts/aggregation/ContactAggregator2Test.java b/tests/src/com/android/providers/contacts/aggregation/ContactAggregator2Test.java
new file mode 100644
index 0000000..834b8f7
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/aggregation/ContactAggregator2Test.java
@@ -0,0 +1,1865 @@
+/*
+ * Copyright (C) 2015 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.aggregation;
+
+import android.accounts.Account;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.provider.ContactsContract.Contacts.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.providers.contacts.BaseContactsProvider2Test;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
+import com.android.providers.contacts.ContactsProvider2;
+import com.android.providers.contacts.TestUtils;
+import com.android.providers.contacts.tests.R;
+import com.android.providers.contacts.testutil.DataUtil;
+import com.android.providers.contacts.testutil.RawContactUtil;
+
+import com.google.android.collect.Lists;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link ContactAggregator2}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -e \
+ *         class com.android.providers.contacts.aggregation.ContactAggregator2Test -w \
+ *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@MediumTest
+public class ContactAggregator2Test extends BaseContactsProvider2Test {
+
+    private static final Account ACCOUNT_1 = new Account("account_name_1", "account_type_1");
+    private static final Account ACCOUNT_2 = new Account("account_name_2", "account_type_2");
+    private static final Account ACCOUNT_3 = new Account("account_name_3", "account_type_3");
+
+    private static final String[] AGGREGATION_EXCEPTION_PROJECTION = new String[] {
+            AggregationExceptions.TYPE,
+            AggregationExceptions.RAW_CONTACT_ID1,
+            AggregationExceptions.RAW_CONTACT_ID2
+    };
+
+    protected void setUp() throws Exception {
+        super.setUp();
+        // Enable new aggregator.
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        cp.setNewAggregatorForTest(true);
+    }
+
+    public void testCrudAggregationExceptions() throws Exception {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "zz", "top");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "aa", "bottom");
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+
+        String selection = "(" + AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1
+                + " AND " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2
+                + ") OR (" + AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId2
+                + " AND " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId1 + ")";
+
+        // Refetch the row we have just inserted
+        Cursor c = mResolver.query(AggregationExceptions.CONTENT_URI,
+                AGGREGATION_EXCEPTION_PROJECTION, selection, null, null);
+
+        assertTrue(c.moveToFirst());
+        assertEquals(AggregationExceptions.TYPE_KEEP_TOGETHER, c.getInt(0));
+        assertTrue((rawContactId1 == c.getLong(1) && rawContactId2 == c.getLong(2))
+                || (rawContactId2 == c.getLong(1) && rawContactId1 == c.getLong(2)));
+        assertFalse(c.moveToNext());
+        c.close();
+
+        // Change from TYPE_KEEP_IN to TYPE_KEEP_OUT
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId2);
+
+        c = mResolver.query(AggregationExceptions.CONTENT_URI, AGGREGATION_EXCEPTION_PROJECTION,
+                selection, null, null);
+
+        assertTrue(c.moveToFirst());
+        assertEquals(AggregationExceptions.TYPE_KEEP_SEPARATE, c.getInt(0));
+        assertTrue((rawContactId1 == c.getLong(1) && rawContactId2 == c.getLong(2))
+                || (rawContactId2 == c.getLong(1) && rawContactId1 == c.getLong(2)));
+        assertFalse(c.moveToNext());
+        c.close();
+
+        // Delete the rule
+        setAggregationException(AggregationExceptions.TYPE_AUTOMATIC,
+                rawContactId1, rawContactId2);
+
+        // Verify that the row is gone
+        c = mResolver.query(AggregationExceptions.CONTENT_URI, AGGREGATION_EXCEPTION_PROJECTION,
+                selection, null, null);
+        assertFalse(c.moveToFirst());
+        c.close();
+    }
+
+    public void testAggregationCreatesNewAggregate() {
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
+
+        Uri resultUri = DataUtil.insertStructuredName(mResolver, rawContactId, "Johna", "Smitha");
+
+        // Parse the URI and confirm that it contains an ID
+        assertTrue(ContentUris.parseId(resultUri) != 0);
+
+        long contactId = queryContactId(rawContactId);
+        assertTrue(contactId != 0);
+
+        String displayName = queryDisplayName(contactId);
+        assertEquals("Johna Smitha", displayName);
+    }
+
+    public void testAggregationOfExactFullNameMatch() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnb", "Smithb");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnb", "Smithb");
+
+        assertAggregated(rawContactId1, rawContactId2, "Johnb Smithb");
+    }
+
+    public void testAggregationIgnoresInvisibleContact() {
+        Account account = new Account("accountName", "accountType");
+        createAutoAddGroup(account);
+
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
+
+        // Hide by removing from all groups
+        removeGroupMemberships(rawContactId1);
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
+
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertAggregated(rawContactId2, rawContactId3, "Flynn Ryder");
+    }
+
+    public void testAggregationOfCaseInsensitiveFullNameMatch() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnc", "Smithc");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnc", "smithc");
+
+        assertAggregated(rawContactId1, rawContactId2, "Johnc Smithc");
+    }
+
+    public void testAggregationOfLastNameMatch() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, null, "Johnd");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, null, "johnd");
+
+        assertAggregated(rawContactId1, rawContactId2, "Johnd");
+    }
+
+    public void testNonAggregationOfFirstNameMatch() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johne", "Smithe");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johne", null);
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testNonAggregationOfLastNameMatch() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnf", "Smithf");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, null, "Smithf");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationOfConcatenatedFullNameMatch() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johng", "Smithg");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "johngsmithg", null);
+
+        assertAggregated(rawContactId1, rawContactId2, "Johng Smithg");
+    }
+
+    public void testAggregationOfNormalizedFullNameMatch() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "helene bjorn", null);
+
+        assertAggregated(rawContactId1, rawContactId2, "H\u00e9l\u00e8ne Bj\u00f8rn");
+    }
+
+    public void testAggregationOfNormalizedFullNameMatchWithReadOnlyAccount() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("acct",
+                READ_ONLY_ACCOUNT_TYPE));
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "helene bjorn", null);
+
+        assertAggregated(rawContactId1, rawContactId2, "helene bjorn");
+    }
+
+    public void testAggregationOfNumericNames() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "123", null);
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "1-2-3", null);
+
+        assertAggregated(rawContactId1, rawContactId2, "1-2-3");
+    }
+
+    public void testAggregationOfInconsistentlyParsedNames() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+
+        ContentValues values = new ContentValues();
+        values.put(StructuredName.DISPLAY_NAME, "604 Arizona Ave");
+        values.put(StructuredName.GIVEN_NAME, "604");
+        values.put(StructuredName.MIDDLE_NAME, "Arizona");
+        values.put(StructuredName.FAMILY_NAME, "Ave");
+        DataUtil.insertStructuredName(mResolver, rawContactId1, values);
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        values.clear();
+        values.put(StructuredName.DISPLAY_NAME, "604 Arizona Ave");
+        values.put(StructuredName.GIVEN_NAME, "604");
+        values.put(StructuredName.FAMILY_NAME, "Arizona Ave");
+        DataUtil.insertStructuredName(mResolver, rawContactId2, values);
+
+        assertAggregated(rawContactId1, rawContactId2, "604 Arizona Ave");
+    }
+
+    public void testAggregationBasedOnMiddleName() {
+        ContentValues values = new ContentValues();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        values.put(StructuredName.GIVEN_NAME, "John");
+        values.put(StructuredName.GIVEN_NAME, "Abigale");
+        values.put(StructuredName.FAMILY_NAME, "James");
+
+        DataUtil.insertStructuredName(mResolver, rawContactId1, values);
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        values.clear();
+        values.put(StructuredName.GIVEN_NAME, "John");
+        values.put(StructuredName.GIVEN_NAME, "Marie");
+        values.put(StructuredName.FAMILY_NAME, "James");
+        DataUtil.insertStructuredName(mResolver, rawContactId2, values);
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationBasedOnPhoneNumberNoNameData() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        insertPhoneNumber(rawContactId1, "(888)555-1231");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        insertPhoneNumber(rawContactId2, "1(888)555-1231");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationBasedOnPhoneNumberWhenTargetAggregateHasNoName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        insertPhoneNumber(rawContactId1, "(888)555-1232");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnl", "Smithl");
+        insertPhoneNumber(rawContactId2, "1(888)555-1232");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationBasedOnPhoneNumberWhenNewContactHasNoName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnm", "Smithm");
+        insertPhoneNumber(rawContactId1, "(888)555-1233");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        insertPhoneNumber(rawContactId2, "1(888)555-1233");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationBasedOnPhoneNumberWithDifferentNames() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Baby", "Bear");
+        insertPhoneNumber(rawContactId1, "(888)555-1235");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Blind", "Mouse");
+        insertPhoneNumber(rawContactId2, "1(888)555-1235");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationBasedOnPhoneNumberWithJustFirstName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Chick", "Notnull");
+        insertPhoneNumber(rawContactId1, "(888)555-1236");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Chick", null);
+        insertPhoneNumber(rawContactId2, "1(888)555-1236");
+
+        assertAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationBasedOnEmailNoNameData() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        insertEmail(rawContactId1, "lightning@android.com");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        insertEmail(rawContactId2, "lightning@android.com");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationBasedOnEmailWhenTargetAggregateHasNoName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        insertEmail(rawContactId1, "mcqueen@android.com");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Lightning", "McQueen");
+        insertEmail(rawContactId2, "mcqueen@android.com");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationBasedOnEmailWhenNewContactHasNoName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Doc", "Hudson");
+        insertEmail(rawContactId1, "doc@android.com");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        insertEmail(rawContactId2, "doc@android.com");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationBasedOnEmailWithDifferentNames() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Chick", "Hicks");
+        insertEmail(rawContactId1, "hicky@android.com");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Luigi", "Guido");
+        insertEmail(rawContactId2, "hicky@android.com");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationByCommonNicknameWithLastName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Bill", "Gore");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "William", "Gore");
+
+        assertAggregated(rawContactId1, rawContactId2, "William Gore");
+    }
+
+    public void testAggregationByCommonNicknameOnly() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Lawrence", null);
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Larry", null);
+
+        assertAggregated(rawContactId1, rawContactId2, "Lawrence");
+    }
+
+    public void testAggregationByNicknameNoStructuredName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        insertNickname(rawContactId1, "Frozone");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        insertNickname(rawContactId2, "Frozone");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationByNicknameWithDifferentNames() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Helen", "Parr");
+        insertNickname(rawContactId1, "Elastigirl");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Shawn", "Johnson");
+        insertNickname(rawContactId2, "Elastigirl");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testNonAggregationOnOrganization() {
+        ContentValues values = new ContentValues();
+        values.put(Organization.TITLE, "Monsters, Inc");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        insertOrganization(rawContactId1, values);
+        insertNickname(rawContactId1, "Boo");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        insertOrganization(rawContactId2, values);
+        insertNickname(rawContactId2, "Rendall");   // To force reaggregation
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationByIdentity() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        insertIdentity(rawContactId1, "iden1", "namespace1");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        insertIdentity(rawContactId2, "iden1", "namespace1");
+
+        assertAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationExceptionKeepIn() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnk", "Smithk");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnkx", "Smithkx");
+
+        long contactId1 = queryContactId(rawContactId1);
+        long contactId2 = queryContactId(rawContactId2);
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+
+        assertAggregated(rawContactId1, rawContactId2, "Johnkx Smithkx");
+
+        // Assert that the empty aggregate got removed
+        long newContactId1 = queryContactId(rawContactId1);
+        if (contactId1 != newContactId1) {
+            Cursor cursor = queryContact(contactId1);
+            assertFalse(cursor.moveToFirst());
+            cursor.close();
+        } else {
+            Cursor cursor = queryContact(contactId2);
+            assertFalse(cursor.moveToFirst());
+            cursor.close();
+        }
+    }
+
+    public void testAggregationExceptionKeepOut() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnh", "Smithh");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnh", "Smithh");
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId2);
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationExceptionKeepOutCheckUpdatesDisplayName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johni", "Smithi");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnj", "Smithj");
+
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, ACCOUNT_3);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Johnm", "Smithm");
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId3);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId2, rawContactId3);
+
+        assertAggregated(rawContactId1, rawContactId2, "Johnm Smithm");
+        assertAggregated(rawContactId1, rawContactId3);
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId2);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId3);
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+        assertNotAggregated(rawContactId1, rawContactId3);
+
+        String displayName1 = queryDisplayName(queryContactId(rawContactId1));
+        assertEquals("Johni Smithi", displayName1);
+
+        assertAggregated(rawContactId2, rawContactId3, "Johnm Smithm");
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId2, rawContactId3);
+        assertNotAggregated(rawContactId1, rawContactId2);
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+
+        String displayName2 = queryDisplayName(queryContactId(rawContactId1));
+        assertEquals("Johni Smithi", displayName2);
+
+        String displayName3 = queryDisplayName(queryContactId(rawContactId2));
+        assertEquals("Johnj Smithj", displayName3);
+
+        String displayName4 = queryDisplayName(queryContactId(rawContactId3));
+        assertEquals("Johnm Smithm", displayName4);
+    }
+
+    public void testAggregationExceptionKeepOutCheckResultDisplayNames() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "c", "c", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "b", "b", ACCOUNT_2);
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "a", "a", ACCOUNT_3);
+
+        // Join all contacts
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId3);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId2, rawContactId3);
+
+        // Separate all contacts. The order (2-3 , 1-2, 1-3) is important
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId2, rawContactId3);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId2);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId3);
+
+        // Verify that we have three different contacts
+        long contactId1 = queryContactId(rawContactId1);
+        long contactId2 = queryContactId(rawContactId2);
+        long contactId3 = queryContactId(rawContactId3);
+
+        assertTrue(contactId1 != contactId2);
+        assertTrue(contactId1 != contactId3);
+        assertTrue(contactId2 != contactId3);
+
+        // Verify that each raw contact contribute to the contact display name
+        assertDisplayNameEquals(contactId1, rawContactId1);
+        assertDisplayNameEquals(contactId2, rawContactId2);
+        assertDisplayNameEquals(contactId3, rawContactId3);
+    }
+
+    public void testNonAggregationWithMultipleAffinities() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        assertNotAggregated(rawContactId1, rawContactId2);
+
+        // There are two aggregates this raw contact could join, so it should join neither
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_2);
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+
+        // Just in case - let's make sure the original two did not get aggregated in the process
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testReaggregateBecauseOfMultipleAffinities() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_2);
+        assertAggregated(rawContactId1, rawContactId2);
+
+        // The aggregate this raw contact could join has a raw contact from the same account,
+        // The ambiguity will trigger re-aggregation. And since no data matching exists, all
+        // three raw contacts are broken-up.
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregation_notAggregateByPhoneticName() {
+        // Different names, but have the same phonetic name.  Shouldn't be aggregated.
+
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Sergey", null, "Yamada");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Lawrence", null, "Yamada");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregation_notAggregateByPhoneticName_2() {
+        // Have the same phonetic name.  One has a regular name too.  Shouldn't be aggregated.
+
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, null, null, "Yamada");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Lawrence", null, "Yamada");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregation_PhoneticNameOnly() {
+        // If a contact has no name but a phonetic name, then its display will be set from the
+        // phonetic name.  In this case, we still aggregate by the display name.
+
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, null, null, "Yamada");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, null, null, "Yamada");
+
+        assertAggregated(rawContactId1, rawContactId2, "Yamada");
+    }
+
+    public void testReaggregationWhenBecomesInvisible() {
+        Account account = new Account("accountName", "accountType");
+        createAutoAddGroup(account);
+
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
+
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
+
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+        assertNotAggregated(rawContactId1, rawContactId2);
+
+        // Hide by removing from all groups
+        removeGroupMemberships(rawContactId3);
+
+        assertAggregated(rawContactId1, rawContactId2, "Flynn Ryder");
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+    }
+
+    public void testReaggregationWhenBecomesInvisibleSecondaryDataMatch() {
+        Account account = new Account("accountName", "accountType");
+        createAutoAddGroup(account);
+
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
+
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
+
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+        assertNotAggregated(rawContactId1, rawContactId2);
+
+        // Hide by removing from all groups
+        removeGroupMemberships(rawContactId3);
+
+        assertAggregated(rawContactId1, rawContactId2, "Flynn Ryder");
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+    }
+
+    public void testReaggregationWhenBecomesVisible() {
+        Account account = new Account("accountName", "accountType");
+        long groupId = createAutoAddGroup(account);
+
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
+
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
+        removeGroupMemberships(rawContactId3);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
+
+        assertAggregated(rawContactId1, rawContactId2, "Flynn Ryder");
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+
+        insertGroupMembership(rawContactId3, groupId);
+
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testNonSplitBecauseOfMultipleAffinitiesWhenOverridden() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_2);
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_3);
+        assertAggregated(rawContactId1, rawContactId2);
+        assertAggregated(rawContactId1, rawContactId3);
+        setAggregationException(
+                AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
+        assertAggregated(rawContactId1, rawContactId2);
+        assertAggregated(rawContactId1, rawContactId3);
+
+        // The aggregate this raw contact could join has a raw contact from the same account,
+        // Let's re-aggregate the existing aggregate because of the ambiguity
+        long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        assertAggregated(rawContactId1, rawContactId2);     // Aggregation exception
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId1, rawContactId4);
+        assertNotAggregated(rawContactId3, rawContactId4);
+    }
+
+    public void testNonSplitWhenIdentityMatch() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertIdentity(rawContactId1, "iden", "namespace");
+        insertIdentity(rawContactId1, "iden2", "namespace");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_2);
+        insertIdentity(rawContactId2, "iden", "namespace");
+        assertAggregated(rawContactId1, rawContactId2);
+
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        assertAggregated(rawContactId1, rawContactId2);
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId3);
+    }
+
+    public void testReAggregateToConnectedComponent() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertPhoneNumber(rawContactId1, "111");
+        setRawContactCustomization(rawContactId1, 1, 1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_2);
+        insertPhoneNumber(rawContactId2, "111");
+        setRawContactCustomization(rawContactId2, 1, 1);
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_3);
+        insertIdentity(rawContactId3, "iden", "namespace");
+        long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                new Account("account_name_4", "account_type_4"));
+        insertIdentity(rawContactId4, "iden", "namespace");
+
+        assertAggregated(rawContactId1, rawContactId2);
+        assertAggregated(rawContactId1, rawContactId3);
+        assertAggregated(rawContactId1, rawContactId4);
+        assertStoredValue(getContactUriForRawContact(rawContactId1),
+                Contacts.STARRED, 1);
+        assertStoredValue(getContactUriForRawContact(rawContactId4),
+                Contacts.SEND_TO_VOICEMAIL, 0);
+
+        long rawContactId5 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+
+        assertAggregated(rawContactId1, rawContactId2);
+        assertAggregated(rawContactId3, rawContactId4);
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId1, rawContactId5);
+        assertNotAggregated(rawContactId3, rawContactId5);
+        assertStoredValue(getContactUriForRawContact(rawContactId1),
+                Contacts.STARRED, 1);
+        assertStoredValue(getContactUriForRawContact(rawContactId1),
+                Contacts.SEND_TO_VOICEMAIL, 1);
+
+        assertStoredValue(getContactUriForRawContact(rawContactId3),
+                Contacts.STARRED, 0);
+        assertStoredValue(getContactUriForRawContact(rawContactId3),
+                Contacts.SEND_TO_VOICEMAIL, 0);
+
+        assertStoredValue(getContactUriForRawContact(rawContactId5),
+                Contacts.STARRED, 0);
+        assertStoredValue(getContactUriForRawContact(rawContactId5),
+                Contacts.SEND_TO_VOICEMAIL, 0);
+    }
+
+    public void testNonAggregationFromSameAccountWithoutAnyDataMatching() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testNonAggregationFromSameAccountNoCommonData() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertEmail(rawContactId1, "lightning1@android.com");
+        insertPhoneNumber(rawContactId1, "111-222-3333");
+        insertIdentity(rawContactId1, "iden1", "namespace");
+
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertEmail(rawContactId2, "lightning2@android.com");
+        insertPhoneNumber(rawContactId2, "555-666-7777");
+        insertIdentity(rawContactId1, "iden2", "namespace");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationFromSameAccountEmailSame_IgnoreCase() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertEmail(rawContactId1, "lightning@android.com");
+
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertEmail(rawContactId2, "lightning@android.com");
+
+        assertAggregated(rawContactId1, rawContactId2);
+
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "Jane", "Doe",
+                ACCOUNT_1);
+        insertEmail(rawContactId3, "jane@android.com");
+
+        long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "Jane", "Doe",
+                ACCOUNT_1);
+        insertEmail(rawContactId4, "JANE@ANDROID.COM");
+
+        assertAggregated(rawContactId3, rawContactId4);
+    }
+
+    public void testNonAggregationFromSameAccountEmailDifferent() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertEmail(rawContactId1, "lightning1@android.com");
+
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertEmail(rawContactId2, "lightning2@android.com");
+        insertEmail(rawContactId2, "lightning3@android.com");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationFromSameAccountIdentitySame() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertIdentity(rawContactId1, "iden", "namespace");
+
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertIdentity(rawContactId2, "iden", "namespace");
+
+        assertAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testNonAggregationFromSameAccountIdentityDifferent() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertIdentity(rawContactId1, "iden1", "namespace1");
+        insertIdentity(rawContactId1, "iden2", "namespace2");
+
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertIdentity(rawContactId2, "iden2", "namespace1");
+        insertIdentity(rawContactId2, "iden1", "namespace2");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationFromSameAccountPhoneNumberSame() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertPhoneNumber(rawContactId1, "111-222-3333");
+
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertPhoneNumber(rawContactId2, "111-222-3333");
+
+        assertAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationFromSameAccountPhoneNumberNormalizedSame() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertPhoneNumber(rawContactId1, "111-222-3333");
+
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertPhoneNumber(rawContactId2, "+1-111-222-3333");
+
+        assertAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testNonAggregationFromSameAccountPhoneNumberDifferent() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertPhoneNumber(rawContactId1, "111-222-3333");
+
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        insertPhoneNumber(rawContactId2, "111-222-3334");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Duane", null);
+
+        // Exact name match
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Duane", null);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId2);
+
+        // Edit distance == 0.84
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Dwayne", null);
+
+        // Edit distance == 0.6
+        long rawContactId4 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId4, "Donny", null);
+
+        long contactId1 = queryContactId(rawContactId1);
+        long contactId2 = queryContactId(rawContactId2);
+        long contactId3 = queryContactId(rawContactId3);
+
+        assertSuggestions(contactId1, contactId2, contactId3);
+    }
+
+    public void testAggregationSuggestionsBasedOnPhoneNumber() {
+
+        // Create two contacts that would not be aggregated because of name mismatch
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Lord", "Farquaad");
+        insertPhoneNumber(rawContactId1, "(888)555-1236");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Talking", "Donkey");
+        insertPhoneNumber(rawContactId2, "1(888)555-1236");
+
+        long contactId1 = queryContactId(rawContactId1);
+        long contactId2 = queryContactId(rawContactId2);
+        assertTrue(contactId1 != contactId2);
+
+        assertSuggestions(contactId1, contactId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnEmailAddress() {
+
+        // Create two contacts that would not be aggregated because of name mismatch
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Carl", "Fredricksen");
+        insertEmail(rawContactId1, "up@android.com");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Charles", "Muntz");
+        insertEmail(rawContactId2, "Up@Android.com");
+
+        long contactId1 = queryContactId(rawContactId1);
+        long contactId2 = queryContactId(rawContactId2);
+        assertTrue(contactId1 != contactId2);
+
+        assertSuggestions(contactId1, contactId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnEmailAddressApproximateMatch() {
+
+        // Create two contacts that would not be aggregated because of name mismatch
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Bob", null);
+        insertEmail(rawContactId1, "incredible@android.com");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Lucius", "Best");
+        insertEmail(rawContactId2, "incrediball@android.com");
+
+        long contactId1 = queryContactId(rawContactId1);
+        long contactId2 = queryContactId(rawContactId2);
+        assertTrue(contactId1 != contactId2);
+
+        assertSuggestions(contactId1, contactId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnNickname() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Peter", "Parker");
+        insertNickname(rawContactId1, "Spider-Man");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Manny", "Spider");
+
+        long contactId1 = queryContactId(rawContactId1);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId2);
+
+        long contactId2 = queryContactId(rawContactId2);
+        assertSuggestions(contactId1, contactId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnNicknameMatchingName() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Clark", "Kent");
+        insertNickname(rawContactId1, "Superman");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Roy", "Williams");
+        insertNickname(rawContactId2, "superman");
+
+        long contactId1 = queryContactId(rawContactId1);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId2);
+
+        long contactId2 = queryContactId(rawContactId2);
+        assertSuggestions(contactId1, contactId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnCommonNickname() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Dick", "Cherry");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Richard", "Cherry");
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId2);
+
+        long contactId1 = queryContactId(rawContactId1);
+        long contactId2 = queryContactId(rawContactId2);
+        assertSuggestions(contactId1, contactId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnPhoneNumberWithFilter() {
+
+        // Create two contacts that would not be aggregated because of name mismatch
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Lord", "Farquaad");
+        insertPhoneNumber(rawContactId1, "(888)555-1236");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Talking", "Donkey");
+        insertPhoneNumber(rawContactId2, "1(888)555-1236");
+
+        long contactId1 = queryContactId(rawContactId1);
+        long contactId2 = queryContactId(rawContactId2);
+        assertTrue(contactId1 != contactId2);
+
+        assertSuggestions(contactId1, "talk", contactId2);
+        assertSuggestions(contactId1, "don", contactId2);
+        assertSuggestions(contactId1, "", contactId2);
+        assertSuggestions(contactId1, "eddie");
+    }
+
+    public void testAggregationSuggestionsDontSuggestInvisible() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "first", "last",
+                ACCOUNT_1);
+        insertPhoneNumber(rawContactId1, "111-222-3333");
+        insertNickname(rawContactId1, "Superman");
+        insertEmail(rawContactId1, "incredible@android.com");
+
+        // Create another with the exact same name, phone number, nickname and email.
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "first", "last",
+                ACCOUNT_2);
+        insertPhoneNumber(rawContactId2, "111-222-3333");
+        insertNickname(rawContactId2, "Superman");
+        insertEmail(rawContactId2, "incredible@android.com");
+
+        // The aggregator should have joined them.  Split them up.
+        setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+                rawContactId1, rawContactId2);
+
+        long contactId1 = queryContactId(rawContactId1);
+        long contactId2 = queryContactId(rawContactId2);
+
+        // Make sure they're different contacts.
+        MoreAsserts.assertNotEqual(contactId1, contactId2);
+
+        // Contact 2 should be suggested.
+        assertSuggestions(contactId1, contactId2);
+
+        // Make contact 2 invisible.
+        markInvisible(contactId2);
+
+        // Now contact 2 shuldn't be suggested.
+        assertSuggestions(contactId1, new long[0]);
+    }
+
+    public void testChoosePhotoSetBeforeAggregation() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId1, "donut", "donut_act");
+        insertPhoto(rawContactId1);
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+        long cupcakeId = ContentUris.parseId(insertPhoto(rawContactId2));
+
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId3, "froyo", "froyo_act");
+        insertPhoto(rawContactId3);
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId3);
+        assertEquals(cupcakeId, queryPhotoId(queryContactId(rawContactId2)));
+    }
+
+    public void testChoosePhotoSetAfterAggregation() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId1, "donut", "donut_act");
+        insertPhoto(rawContactId1);
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+        setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+        long cupcakeId = ContentUris.parseId(insertPhoto(rawContactId2));
+
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId3);
+        setContactAccount(rawContactId3, "froyo", "froyo_act");
+        insertPhoto(rawContactId3);
+
+        assertEquals(cupcakeId, queryPhotoId(queryContactId(rawContactId2)));
+    }
+
+    // Note that for the following tests of photo aggregation, the accounts are being used to
+    // set the typical photo priority that each raw contact would have, based on
+    // SynchronousContactsProvider2.createPhotoPriorityResolver.  The relative priorities
+    // specified there are:
+    // cupcake: 3
+    // donut: 2
+    // froyo: 1
+    // <other>: 0
+
+    public void testChooseLargerPhotoByDimensions() {
+        // Donut photo is 256x256.
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId1, "donut", "donut_act");
+        long normalEarthDataId = ContentUris.parseId(
+                insertPhoto(rawContactId1, R.drawable.earth_normal));
+        long normalEarthPhotoFileId = getStoredLongValue(
+                ContentUris.withAppendedId(Data.CONTENT_URI, normalEarthDataId),
+                Photo.PHOTO_FILE_ID);
+
+        // Cupcake would normally have priority, but its photo is 200x200.
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+        insertPhoto(rawContactId2, R.drawable.earth_200);
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+
+        // Larger photo (by dimensions) wins.
+        assertEquals(normalEarthPhotoFileId, queryPhotoFileId(queryContactId(rawContactId1)));
+    }
+
+    public void testChooseLargerPhotoByFileSize() {
+        // Donut photo is a 256x256 photo of a nebula.
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId1, "donut", "donut_act");
+        long nebulaDataId = ContentUris.parseId(
+                insertPhoto(rawContactId1, R.drawable.nebula));
+        long nebulaPhotoFileId = getStoredLongValue(
+                ContentUris.withAppendedId(Data.CONTENT_URI, nebulaDataId),
+                Photo.PHOTO_FILE_ID);
+
+        // Cupcake would normally have priority, but its photo (of a galaxy) has the same dimensions
+        // as Donut's, but a smaller filesize.
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+        insertPhoto(rawContactId2, R.drawable.galaxy);
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+
+        // Larger photo (by filesize) wins.
+        assertEquals(nebulaPhotoFileId, queryPhotoFileId(queryContactId(rawContactId1)));
+    }
+
+    public void testChooseFilePhotoOverThumbnail() {
+        // Donut photo is a 256x256 photo of Earth.
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId1, "donut", "donut_act");
+        long normalEarthDataId = ContentUris.parseId(
+                insertPhoto(rawContactId1, R.drawable.earth_normal));
+        long normalEarthPhotoFileId = getStoredLongValue(
+                ContentUris.withAppendedId(Data.CONTENT_URI, normalEarthDataId),
+                Photo.PHOTO_FILE_ID);
+
+        // Cupcake would normally have priority, but its photo of Earth is thumbnail-sized.
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+        insertPhoto(rawContactId2, R.drawable.earth_small);
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+
+        // Larger photo (by filesize) wins.
+        assertEquals(normalEarthPhotoFileId, queryPhotoFileId(queryContactId(rawContactId1)));
+    }
+
+    public void testFallbackToAccountPriorityForSamePhoto() {
+        // Donut photo is a 256x256 photo of Earth.
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId1, "donut", "donut_act");
+        insertPhoto(rawContactId1, R.drawable.earth_normal);
+
+        // Cupcake has the same 256x256 photo of Earth.
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+        long cupcakeEarthDataId = ContentUris.parseId(
+                insertPhoto(rawContactId2, R.drawable.earth_normal));
+        long cupcakeEarthPhotoFileId = getStoredLongValue(
+                ContentUris.withAppendedId(Data.CONTENT_URI, cupcakeEarthDataId),
+                Photo.PHOTO_FILE_ID);
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+
+        // Cupcake's version of the photo wins, falling back to account priority.
+        assertEquals(cupcakeEarthPhotoFileId, queryPhotoFileId(queryContactId(rawContactId1)));
+    }
+
+    public void testFallbackToAccountPriorityForDifferingThumbnails() {
+        // Donut photo is a 96x96 thumbnail of Earth.
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId1, "donut", "donut_act");
+        insertPhoto(rawContactId1, R.drawable.earth_small);
+
+        // Cupcake photo is the 96x96 "no contact" placeholder (smaller filesize than the Earth
+        // picture, but thumbnail filesizes are ignored in the aggregator).
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+        long cupcakeDataId = ContentUris.parseId(
+                insertPhoto(rawContactId2, R.drawable.ic_contact_picture));
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+
+        // The Cupcake thumbnail wins, by account priority..
+        assertEquals(cupcakeDataId, queryPhotoId(queryContactId(rawContactId1)));
+    }
+
+    public void testDisplayNameSources() {
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
+        long contactId = queryContactId(rawContactId);
+
+        assertNull(queryDisplayName(contactId));
+
+        insertEmail(rawContactId, "eclair@android.com");
+        assertEquals("eclair@android.com", queryDisplayName(contactId));
+
+        insertPhoneNumber(rawContactId, "800-555-5555");
+        assertEquals("800-555-5555", queryDisplayName(contactId));
+
+        ContentValues values = new ContentValues();
+        values.put(Organization.COMPANY, "Android");
+        insertOrganization(rawContactId, values);
+        assertEquals("Android", queryDisplayName(contactId));
+
+        insertNickname(rawContactId, "Dro");
+        assertEquals("Dro", queryDisplayName(contactId));
+
+        values.clear();
+        values.put(StructuredName.GIVEN_NAME, "Eclair");
+        values.put(StructuredName.FAMILY_NAME, "Android");
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
+        assertEquals("Eclair Android", queryDisplayName(contactId));
+    }
+
+    public void testMergeSuperPrimaryName_rawContact1() {
+        // Setup: raw contact #1 has a super primary name. #2 doesn't.
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "name1", null, null,
+                /* isSuperPrimary = */ true);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "name2", null, null,
+                /* isSuperPrimary = */ false);
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: the aggregate's name comes from raw contact #1
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name1", queryDisplayName(contactId));
+    }
+
+    public void testMergeSuperPrimaryName_rawContact2AndEdit() {
+        // Setup: raw contact #2 has a super primary name. #1 doesn't.
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        final Uri nameUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1",
+                null, null, /* isSuperPrimary = */ false);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        final Uri nameUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "name2", null,
+                null, /* isSuperPrimary = */ true);
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: the aggregate's name comes from raw contact #2. This is the opposite of the check
+        // inside testMergeSuperPrimaryName_rawContact1().
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name2", queryDisplayName(contactId));
+
+        // Action: edit the super primary name
+        final ContentValues values = new ContentValues();
+        values.put(StructuredName.GIVEN_NAME, "edited name");
+        mResolver.update(nameUri2, values, null, null);
+
+        // Verify: editing the super primary name affects aggregate name
+        assertEquals("edited name", queryDisplayName(contactId));
+
+        // Action: edit the non primary name
+        values.put(StructuredName.GIVEN_NAME, "edited name2");
+        mResolver.update(nameUri1, values, null, null);
+
+        // Verify: aggregate name is still based off the primary name
+        assertEquals("edited name", queryDisplayName(contactId));
+    }
+
+    public void testMergedSuperPrimaryName_changeSuperPrimary() {
+        // Setup: aggregated contact where raw contact #1 has a super primary name. #2 doesn't.
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        final Uri nameUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1",
+                null, null, /* isSuperPrimary = */ true);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        final Uri nameUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "name2", null,
+                null, /* isSuperPrimary = */ false);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Action: make raw contact 2's name super primary
+        storeValue(nameUri2, Data.IS_SUPER_PRIMARY, 1);
+
+        // Sanity check.
+        assertStoredValue(nameUri1, Data.IS_SUPER_PRIMARY, 0);
+        assertStoredValue(nameUri2, Data.IS_SUPER_PRIMARY, 1);
+
+        // Verify: aggregate name is based off of the newly super primary name
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name2", queryDisplayName(contactId));
+    }
+
+    public void testAggregationModeSuspendedSeparateTransactions() {
+
+        // Setting aggregation mode to SUSPENDED should prevent aggregation from happening
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        storeValue(RawContacts.CONTENT_URI, rawContactId1,
+                RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+        Uri name1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "THE", "SAME");
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        storeValue(RawContacts.CONTENT_URI, rawContactId2,
+                RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "THE", "SAME");
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+
+        // Changing aggregation mode to DEFAULT should change nothing
+        storeValue(RawContacts.CONTENT_URI, rawContactId1,
+                RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+        storeValue(RawContacts.CONTENT_URI, rawContactId2,
+                RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+        assertNotAggregated(rawContactId1, rawContactId2);
+
+        // Changing the name should trigger aggregation
+        storeValue(name1, StructuredName.GIVEN_NAME, "the");
+        assertAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationModeInitializedAsSuspended() throws Exception {
+
+        // Setting aggregation mode to SUSPENDED should prevent aggregation from happening
+        ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+                .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+                .build();
+        ContentProviderOperation cpo2 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+                .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+                .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+                .withValue(StructuredName.GIVEN_NAME, "John")
+                .withValue(StructuredName.FAMILY_NAME, "Doe")
+                .build();
+        ContentProviderOperation cpo3 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+                .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+                .build();
+        ContentProviderOperation cpo4 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+                .withValueBackReference(Data.RAW_CONTACT_ID, 2)
+                .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+                .withValue(StructuredName.GIVEN_NAME, "John")
+                .withValue(StructuredName.FAMILY_NAME, "Doe")
+                .build();
+        ContentProviderOperation cpo5 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+                .withSelection(RawContacts._ID + "=?", new String[1])
+                .withSelectionBackReference(0, 0)
+                .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT)
+                .build();
+        ContentProviderOperation cpo6 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+                .withSelection(RawContacts._ID + "=?", new String[1])
+                .withSelectionBackReference(0, 2)
+                .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT)
+                .build();
+
+        ContentProviderResult[] results =
+                mResolver.applyBatch(ContactsContract.AUTHORITY,
+                        Lists.newArrayList(cpo1, cpo2, cpo3, cpo4, cpo5, cpo6));
+
+        long rawContactId1 = ContentUris.parseId(results[0].uri);
+        long rawContactId2 = ContentUris.parseId(results[2].uri);
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationModeUpdatedToSuspended() throws Exception {
+
+        // Setting aggregation mode to SUSPENDED should prevent aggregation from happening
+        ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+                .withValues(new ContentValues())
+                .build();
+        ContentProviderOperation cpo2 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+                .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+                .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+                .withValue(StructuredName.GIVEN_NAME, "John")
+                .withValue(StructuredName.FAMILY_NAME, "Doe")
+                .build();
+        ContentProviderOperation cpo3 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+                .withValues(new ContentValues())
+                .build();
+        ContentProviderOperation cpo4 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+                .withValueBackReference(Data.RAW_CONTACT_ID, 2)
+                .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+                .withValue(StructuredName.GIVEN_NAME, "John")
+                .withValue(StructuredName.FAMILY_NAME, "Doe")
+                .build();
+        ContentProviderOperation cpo5 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+                .withSelection(RawContacts._ID + "=?", new String[1])
+                .withSelectionBackReference(0, 0)
+                .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+                .build();
+        ContentProviderOperation cpo6 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+                .withSelection(RawContacts._ID + "=?", new String[1])
+                .withSelectionBackReference(0, 2)
+                .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+                .build();
+
+        ContentProviderResult[] results =
+                mResolver.applyBatch(ContactsContract.AUTHORITY,
+                        Lists.newArrayList(cpo1, cpo2, cpo3, cpo4, cpo5, cpo6));
+
+        long rawContactId1 = ContentUris.parseId(results[0].uri);
+        long rawContactId2 = ContentUris.parseId(results[2].uri);
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationModeSuspendedOverriddenByAggException() throws Exception {
+        ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+                .withValue(RawContacts.ACCOUNT_NAME, "a")
+                .withValue(RawContacts.ACCOUNT_TYPE, "b")
+                .build();
+        ContentProviderOperation cpo2 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+                .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+                .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+                .withValue(StructuredName.GIVEN_NAME, "John")
+                .withValue(StructuredName.FAMILY_NAME, "Doe")
+                .build();
+        ContentProviderOperation cpo3 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+                .withValue(RawContacts.ACCOUNT_NAME, "c")
+                .withValue(RawContacts.ACCOUNT_TYPE, "d")
+                .build();
+        ContentProviderOperation cpo4 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+                .withValueBackReference(Data.RAW_CONTACT_ID, 2)
+                .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+                .withValue(StructuredName.GIVEN_NAME, "John")
+                .withValue(StructuredName.FAMILY_NAME, "Doe")
+                .build();
+        ContentProviderOperation cpo5 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+                .withSelection(RawContacts._ID + "=?", new String[1])
+                .withSelectionBackReference(0, 0)
+                .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+                .build();
+        ContentProviderOperation cpo6 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+                .withSelection(RawContacts._ID + "=?", new String[1])
+                .withSelectionBackReference(0, 2)
+                .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+                .build();
+
+        // Checking that aggregation mode SUSPENDED should be overridden by inserting
+        // an explicit aggregation exception
+        ContentProviderOperation cpo7 =
+                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI)
+                        .withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, 0)
+                        .withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, 2)
+                        .withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER)
+                        .build();
+
+        ContentProviderResult[] results =
+                mResolver.applyBatch(ContactsContract.AUTHORITY,
+                        Lists.newArrayList(cpo1, cpo2, cpo3, cpo4, cpo5, cpo6, cpo7));
+
+        long rawContactId1 = ContentUris.parseId(results[0].uri);
+        long rawContactId2 = ContentUris.parseId(results[2].uri);
+
+        assertAggregated(rawContactId1, rawContactId2);
+    }
+
+    public void testAggregationSuggestionsQueryBuilderWithContactId() throws Exception {
+        Uri uri = AggregationSuggestions.builder().setContactId(12).setLimit(7).build();
+        assertEquals("content://com.android.contacts/contacts/12/suggestions?limit=7",
+                uri.toString());
+    }
+
+    public void testAggregationSuggestionsQueryBuilderWithValues() throws Exception {
+        Uri uri = AggregationSuggestions.builder()
+                .addNameParameter("name1")
+                .addNameParameter("name2")
+                .setLimit(7)
+                .build();
+        assertEquals("content://com.android.contacts/contacts/0/suggestions?"
+                + "limit=7"
+                + "&query=name%3Aname1"
+                + "&query=name%3Aname2", uri.toString());
+    }
+
+    public void testAggregatedStatusUpdate() {
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        Uri dataUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "john", "doe");
+        insertStatusUpdate(ContentUris.parseId(dataUri1), StatusUpdates.AWAY, "Green", 100,
+                StatusUpdates.CAPABILITY_HAS_CAMERA);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        Uri dataUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "john", "doe");
+        insertStatusUpdate(ContentUris.parseId(dataUri2), StatusUpdates.AVAILABLE, "Red", 50,
+                StatusUpdates.CAPABILITY_HAS_CAMERA);
+        setAggregationException(
+                AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
+
+        assertStoredValue(getContactUriForRawContact(rawContactId1),
+                Contacts.CONTACT_STATUS, "Green");
+
+        // When we split these two raw contacts, their respective statuses should be restored
+        setAggregationException(
+                AggregationExceptions.TYPE_KEEP_SEPARATE, rawContactId1, rawContactId2);
+
+        assertStoredValue(getContactUriForRawContact(rawContactId1),
+                Contacts.CONTACT_STATUS, "Green");
+
+        assertStoredValue(getContactUriForRawContact(rawContactId2),
+                Contacts.CONTACT_STATUS, "Red");
+    }
+
+    public void testAggregationSuggestionsByName() throws Exception {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "first1", "last1");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "first2", "last2");
+
+        Uri uri = AggregationSuggestions.builder()
+                .addNameParameter("last1 first1")
+                .build();
+
+        Cursor cursor = mResolver.query(
+                uri, new String[] { Contacts._ID, Contacts.DISPLAY_NAME }, null, null, null);
+
+        assertEquals(1, cursor.getCount());
+
+        cursor.moveToFirst();
+
+        ContentValues values = new ContentValues();
+        values.put(Contacts._ID, queryContactId(rawContactId1));
+        values.put(Contacts.DISPLAY_NAME, "first1 last1");
+        assertCursorValues(cursor, values);
+        cursor.close();
+    }
+
+    public void testAggregation_phoneticNamePriority1() {
+        // Setup: one raw contact has a complex phonetic name and the other a simple given name
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name", ACCOUNT_1);
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: given name is used instead of phonetic, contrary to results of
+        // testAggregation_nameComplexity
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name", queryDisplayName(contactId));
+    }
+
+    // Same as testAggregation_phoneticNamePriority1, but with setup order reversed
+    public void testAggregation_phoneticNamePriority2() {
+        // Setup: one raw contact has a complex phonetic name and the other a simple given name
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: given name is used instead of phonetic, contrary to results of
+        // testAggregation_nameComplexity
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name", queryDisplayName(contactId));
+    }
+
+    public void testAggregation_nameComplexity1() {
+        // Setup: two names, one of which is unambiguously more complex
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name phonetic", ACCOUNT_1);
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: more complex name is used
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name phonetic", queryDisplayName(contactId));
+    }
+
+    // Same as testAggregation_nameComplexity1, but with setup order reversed
+    public void testAggregation_nameComplexity2() {
+        // Setup: two names, one of which is unambiguously more complex
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name phonetic", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name", ACCOUNT_1);
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: more complex name is used
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name phonetic", queryDisplayName(contactId));
+    }
+
+    public void testAggregation_clearSuperPrimary() {
+        // Three types of mime-type super primary merging are tested here
+        // 1. both raw contacts have super primary phone numbers
+        // 2. both raw contacts have emails, but only one has super primary email
+        // 3. only raw contact1 has organizations and it has set the super primary organization
+        ContentValues values = new ContentValues();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        Uri uri_phone1a = insertPhoneNumber(rawContactId1, "(222)222-2222", true, true);
+        Uri uri_phone1b = insertPhoneNumber(rawContactId1, "(555)555-5555", false, false);
+        Uri uri_phone3 = insertPhoneNumber(rawContactId1, "(111)111-1111", true, true);
+        Uri uri_email1 = insertEmail(rawContactId1, "one@gmail.com", true, true);
+        values.clear();
+        values.put(Organization.COMPANY, "Monsters Inc");
+        Uri uri_org1 = insertOrganization(rawContactId1, values, true, true);
+        values.clear();
+        values.put(Organization.TITLE, "CEO");
+        Uri uri_org2 = insertOrganization(rawContactId1, values, false, false);
+
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        Uri uri_phone2 = insertPhoneNumber(rawContactId2, "(333)333-3333", true, true);
+        Uri uri_phone4 = insertPhoneNumber(rawContactId2, "1(111)111-1111", true, true);
+        Uri uri_email2 = insertEmail(rawContactId2, "two@gmail.com", false, false);
+
+        // Two raw contacts with same name will trigger the aggregation
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Helen", "Parr");
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Helen", "Parr");
+
+        // After aggregation, the super primary flag should only be cleared for case 1,
+        // i.e., phone mime-type. Both case 2 and 3, i.e. organization and email mime-types,
+        // have the super primary flag unchanged.
+        assertAggregated(rawContactId1, rawContactId2);
+        assertSuperPrimary(ContentUris.parseId(uri_phone1a), false);
+        assertSuperPrimary(ContentUris.parseId(uri_phone1b), false);
+        assertSuperPrimary(ContentUris.parseId(uri_phone2), false);
+        assertSuperPrimary(ContentUris.parseId(uri_phone3), false);
+        assertSuperPrimary(ContentUris.parseId(uri_phone4), false);
+
+        assertSuperPrimary(ContentUris.parseId(uri_email1), true);
+        assertSuperPrimary(ContentUris.parseId(uri_email2), false);
+
+        assertSuperPrimary(ContentUris.parseId(uri_org1), true);
+        assertSuperPrimary(ContentUris.parseId(uri_org2), false);
+    }
+
+    public void testAggregation_clearSuperPrimarySingleMimetype() {
+        // Setup: two raw contacts, each has a single name. One of the names is super primary.
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        final Uri uri = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1",
+                null, null, /* isSuperPrimary = */ true);
+
+        // Sanity check.
+        assertStoredValue(uri, Data.IS_SUPER_PRIMARY, 1);
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: name is still super primary
+        assertStoredValue(uri, Data.IS_SUPER_PRIMARY, 1);
+    }
+
+    public void testNotAggregate_TooManyRawContactsInCandidate() {
+        long preId= 0;
+        for (int i = 0; i < ContactAggregator.AGGREGATION_CONTACT_SIZE_LIMIT; i++) {
+            long id = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
+            if (i >  0) {
+                setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, preId, id);
+            }
+            preId = id;
+        }
+        // Although the newly added raw contact matches the names with other raw contacts,
+        // but the best matching contact has already meets the size limit, so keep the new raw
+        // contact separate from other raw contacts.
+        long newId = RawContactUtil.createRawContact(mResolver,
+                new Account("account_new", "new account type"));
+        DataUtil.insertStructuredName(mResolver, newId, "John", "Doe");
+        assertNotAggregated(preId, newId);
+        assertTrue(queryContactId(newId) > 0);
+    }
+
+    public void testDoctorsWithSameWorkPhoneNumber() {
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        ContentValues values = new ContentValues();
+        values.put(Organization.TITLE, "Weight Management");
+        insertOrganization(rawContactId1, values);
+        insertPhoneNumber(rawContactId1,"8296", false, 3);
+        insertPhoneNumber(rawContactId1,"8270", false, 4);
+
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "Jane", "Smith",
+                ACCOUNT_1);
+        insertEmail(rawContactId2, "s@hospital.org");
+        values.put(Organization.TITLE, "Weight Management Program");
+        insertOrganization(rawContactId2, values);
+        insertPhoneNumber(rawContactId2,"8264", false, 3);
+        insertPhoneNumber(rawContactId2,"8270", false, 4);
+
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "Karen", "Lee",
+                ACCOUNT_1);
+        insertEmail(rawContactId3, "l@hospital.org");
+        values.put(Organization.TITLE, "Weight Management");
+        insertOrganization(rawContactId3, values);
+        insertPhoneNumber(rawContactId3,"8268", false, 3);
+        insertPhoneNumber(rawContactId3,"8270", false, 4);
+
+        long rawContactId4 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        insertEmail(rawContactId4, "orders@v.org");
+        values.put(Organization.TITLE, "Weight Management");
+        insertOrganization(rawContactId4, values);
+        insertPhoneNumber(rawContactId4,"8262", false, 3);
+        insertPhoneNumber(rawContactId4,"8260", false, 3);
+        insertPhoneNumber(rawContactId4,"8270", false, 4);
+
+        long rawContactId5 = RawContactUtil.createRawContactWithName(mResolver, "Rachel", "Fred",
+                ACCOUNT_1);
+        insertEmail(rawContactId5, "f@hospital.org");
+        values.put(Organization.TITLE, "Weight Management");
+        insertOrganization(rawContactId5, values);
+        insertPhoneNumber(rawContactId5,"8261", false, 3);
+        insertPhoneNumber(rawContactId5,"8270", false, 4);
+
+        assertNotAggregated(rawContactId1, rawContactId2);
+        assertNotAggregated(rawContactId1, rawContactId3);
+        assertNotAggregated(rawContactId1, rawContactId4);
+        assertNotAggregated(rawContactId1, rawContactId5);
+        assertNotAggregated(rawContactId2, rawContactId3);
+        assertNotAggregated(rawContactId2, rawContactId4);
+        assertNotAggregated(rawContactId2, rawContactId5);
+        assertNotAggregated(rawContactId3, rawContactId4);
+        assertNotAggregated(rawContactId3, rawContactId5);
+        assertNotAggregated(rawContactId4, rawContactId5);
+    }
+
+    private void assertSuggestions(long contactId, long... suggestions) {
+        final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        Uri uri = Uri.withAppendedPath(aggregateUri,
+                Contacts.AggregationSuggestions.CONTENT_DIRECTORY);
+        assertSuggestions(uri, suggestions);
+    }
+
+    private void assertSuggestions(long contactId, String filter, long... suggestions) {
+        final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(aggregateUri,
+                Contacts.AggregationSuggestions.CONTENT_DIRECTORY), Uri.encode(filter));
+        assertSuggestions(uri, suggestions);
+    }
+
+    private void assertSuggestions(Uri uri, long... suggestions) {
+        final Cursor cursor = mResolver.query(uri,
+                new String[] { Contacts._ID, Contacts.CONTACT_PRESENCE },
+                null, null, null);
+
+        try {
+            assertEquals(suggestions.length, cursor.getCount());
+
+            for (int i = 0; i < suggestions.length; i++) {
+                cursor.moveToNext();
+                assertEquals(suggestions[i], cursor.getLong(0));
+            }
+        } finally {
+            TestUtils.dumpCursor(cursor);
+        }
+
+        cursor.close();
+    }
+
+    private void assertDisplayNameEquals(long contactId, long rawContactId) {
+
+        String contactDisplayName = queryDisplayName(contactId);
+
+        Cursor c = queryRawContact(rawContactId);
+        assertTrue(c.moveToFirst());
+        String rawDisplayName = c.getString(c.getColumnIndex(RawContacts.DISPLAY_NAME_PRIMARY));
+        c.close();
+
+        assertTrue(contactDisplayName != null);
+        assertTrue(rawDisplayName != null);
+        assertEquals(rawDisplayName, contactDisplayName);
+    }
+}