Merge commit 'goog/eclair-dev' into merge3

Merged the new contacts content provider into goog/master. The old and
new content providers now live side by side under separate authorities.

Conflicts:
	Android.mk
	AndroidManifest.xml
	res/values/strings.xml
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..96ae80f
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,17 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := ContactsProvider2Tests
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_INSTRUMENTATION_FOR := ContactsProvider
+LOCAL_CERTIFICATE := shared
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..d662b53
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.providers.contacts.tests"
+        android:sharedUserId="android.uid.shared">
+
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <!--
+    The test delcared in this instrumentation will be run along with tests declared by
+    all other applications via the command: "adb shell itr".
+    The "itr" command will find all tests declared by all applications. If you want to run just these
+    tests on their own then use the command:
+    "adb shell am instrument -w com.android.providers.contacts.tests/android.test.InstrumentationTestRunner"
+    -->
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.providers.contacts"
+        android:label="Contacts2 provider tests">
+    </instrumentation>
+
+</manifest>
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
new file mode 100644
index 0000000..aa2b2ef
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
+
+import com.android.providers.contacts.ContactsActor;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Aggregates;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * A common superclass for {@link ContactsProvider2}-related tests.
+ */
+@LargeTest
+public abstract class BaseContactsProvider2Test extends AndroidTestCase {
+
+    protected static final String PACKAGE = "ContactsProvider2Test";
+
+    private ContactsActor mActor;
+    protected MockContentResolver mResolver;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mActor = new ContactsActor(getContext(), PACKAGE_GREY);
+        mResolver = mActor.resolver;
+    }
+
+    protected long createContact() {
+        ContentValues values = new ContentValues();
+        values.put(Contacts.PACKAGE, mActor.packageName);
+        Uri contactUri = mResolver.insert(Contacts.CONTENT_URI, values);
+        return ContentUris.parseId(contactUri);
+    }
+
+    protected Uri insertStructuredName(long contactId, String givenName, String familyName) {
+        ContentValues values = new ContentValues();
+        StringBuilder sb = new StringBuilder();
+        if (givenName != null) {
+            sb.append(givenName);
+        }
+        if (givenName != null && familyName != null) {
+            sb.append(" ");
+        }
+        if (familyName != null) {
+            sb.append(familyName);
+        }
+        values.put(StructuredName.DISPLAY_NAME, sb.toString());
+        values.put(StructuredName.GIVEN_NAME, givenName);
+        values.put(StructuredName.FAMILY_NAME, familyName);
+
+        return insertStructuredName(contactId, values);
+    }
+
+    protected Uri insertStructuredName(long contactId, ContentValues values) {
+        values.put(Data.CONTACT_ID, contactId);
+        values.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
+    protected Uri insertPhoneNumber(long contactId, String phoneNumber) {
+        ContentValues values = new ContentValues();
+        values.put(Data.CONTACT_ID, contactId);
+        values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        values.put(Phone.NUMBER, phoneNumber);
+
+        Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
+    protected Uri insertEmail(long contactId, String email) {
+        ContentValues values = new ContentValues();
+        values.put(Data.CONTACT_ID, contactId);
+        values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        values.put(Email.DATA, email);
+
+        Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
+    protected Uri insertNickname(long contactId, String nickname) {
+        ContentValues values = new ContentValues();
+        values.put(Data.CONTACT_ID, contactId);
+        values.put(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
+        values.put(Nickname.NAME, nickname);
+
+        Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
+    protected Uri insertPresence(int protocol, String handle, int presence) {
+        ContentValues values = new ContentValues();
+        values.put(Presence.IM_PROTOCOL, protocol);
+        values.put(Presence.IM_HANDLE, handle);
+        values.put(Presence.PRESENCE_STATUS, presence);
+
+        Uri resultUri = mResolver.insert(Presence.CONTENT_URI, values);
+        return resultUri;
+    }
+
+    protected Uri insertImHandle(long contactId, int protocol, String handle) {
+        ContentValues values = new ContentValues();
+        values.put(Data.CONTACT_ID, contactId);
+        values.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        values.put(Im.PROTOCOL, protocol);
+        values.put(Im.DATA, handle);
+
+        Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
+    protected void setAggregationException(int type, long aggregateId, long contactId) {
+        ContentValues values = new ContentValues();
+        values.put(AggregationExceptions.AGGREGATE_ID, aggregateId);
+        values.put(AggregationExceptions.CONTACT_ID, contactId);
+        values.put(AggregationExceptions.TYPE, type);
+        mResolver.update(AggregationExceptions.CONTENT_URI, values, null, null);
+    }
+
+    protected Cursor queryContact(long contactId) {
+        return mResolver.query(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), null,
+                null, null, null);
+    }
+
+    protected Cursor queryAggregate(long aggregateId) {
+        return mResolver.query(ContentUris.withAppendedId(Aggregates.CONTENT_URI, aggregateId),
+                null, null, null, null);
+    }
+
+    protected Cursor queryAggregateSummary(long aggregateId, String[] projection) {
+        return mResolver.query(ContentUris.withAppendedId(Aggregates.CONTENT_SUMMARY_URI,
+                aggregateId), projection, null, null, null);
+    }
+
+    protected Cursor queryAggregateSummary() {
+        return mResolver.query(Aggregates.CONTENT_SUMMARY_URI, null, null, null, null);
+    }
+
+    protected long queryAggregateId(long contactId) {
+        Cursor c = queryContact(contactId);
+        assertTrue(c.moveToFirst());
+        long aggregateId = c.getLong(c.getColumnIndex(Contacts.AGGREGATE_ID));
+        c.close();
+        return aggregateId;
+    }
+
+    protected String queryDisplayName(long aggregateId) {
+        Cursor c = queryAggregate(aggregateId);
+        assertTrue(c.moveToFirst());
+        String displayName = c.getString(c.getColumnIndex(Aggregates.DISPLAY_NAME));
+        c.close();
+        return displayName;
+    }
+
+    protected void assertAggregated(long contactId1, long contactId2) {
+        long aggregateId1 = queryAggregateId(contactId1);
+        long aggregateId2 = queryAggregateId(contactId2);
+        assertTrue(aggregateId1 == aggregateId2);
+    }
+
+    protected void assertAggregated(long contactId1, long contactId2, String expectedDisplayName) {
+        long aggregateId1 = queryAggregateId(contactId1);
+        long aggregateId2 = queryAggregateId(contactId2);
+        assertTrue(aggregateId1 == aggregateId2);
+
+        String displayName = queryDisplayName(aggregateId1);
+        assertEquals(expectedDisplayName, displayName);
+    }
+
+    protected void assertNotAggregated(long contactId1, long contactId2) {
+        long aggregateId1 = queryAggregateId(contactId1);
+        long aggregateId2 = queryAggregateId(contactId2);
+        assertTrue(aggregateId1 != aggregateId2);
+    }
+
+    protected void assertStructuredName(long contactId, String prefix, String givenName,
+            String middleName, String familyName, String suffix) {
+        Uri uri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
+                Contacts.Data.CONTENT_DIRECTORY);
+
+        final String[] projection = new String[] {
+                StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME,
+                StructuredName.FAMILY_NAME, StructuredName.SUFFIX
+        };
+
+        Cursor c = mResolver.query(uri, projection, Data.MIMETYPE + "='"
+                + StructuredName.CONTENT_ITEM_TYPE + "'", null, null);
+
+        assertTrue(c.moveToFirst());
+        assertEquals(prefix, c.getString(0));
+        assertEquals(givenName, c.getString(1));
+        assertEquals(middleName, c.getString(2));
+        assertEquals(familyName, c.getString(3));
+        assertEquals(suffix, c.getString(4));
+        c.close();
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java b/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
new file mode 100644
index 0000000..1a98d2a
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests from {@link ContactAggregationScheduler}.
+ */
+@SmallTest
+public class ContactAggregationSchedulerTest extends TestCase {
+
+    private TestContactAggregationScheduler mScheduler;
+
+    @Override
+    protected void setUp() throws Exception {
+        mScheduler = new TestContactAggregationScheduler();
+    }
+
+    public void testScheduleInitial() {
+        mScheduler.schedule();
+        assertEquals(1, mScheduler.mRunDelayed);
+    }
+
+    public void testScheduleTwiceRapidly() {
+        mScheduler.schedule();
+
+        mScheduler.mTime += ContactAggregationScheduler.AGGREGATION_DELAY / 2;
+        mScheduler.schedule();
+        assertEquals(2, mScheduler.mRunDelayed);
+    }
+
+    public void testScheduleTwiceExceedingMaxDelay() {
+        mScheduler.schedule();
+
+        mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
+        mScheduler.schedule();
+        assertEquals(1, mScheduler.mRunDelayed);
+    }
+
+    public void testScheduleWhileRunning() {
+        mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
+            boolean mInterruptCalled;
+
+            public void interrupt() {
+                mInterruptCalled = true;
+            }
+
+            public void run() {
+                mScheduler.schedule();
+                assertTrue(mInterruptCalled);
+            }
+        });
+
+        mScheduler.run();
+        assertEquals(1, mScheduler.mRunDelayed);
+    }
+
+    public void testScheduleWhileRunningExceedingMaxDelay() {
+        mScheduler.schedule();
+
+        mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
+
+        mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
+            boolean mInterruptCalled;
+
+            public void interrupt() {
+                mInterruptCalled = true;
+            }
+
+            public void run() {
+                mScheduler.schedule();
+                assertFalse(mInterruptCalled);
+            }
+        });
+
+        mScheduler.run();
+        assertEquals(2, mScheduler.mRunDelayed);
+    }
+
+    private static class TestContactAggregationScheduler extends ContactAggregationScheduler {
+
+        long mTime = 1000;
+        int mRunDelayed;
+
+        @Override
+        public void start() {
+        }
+
+        @Override
+        public void stop() {
+        }
+
+        @Override
+        long currentTime() {
+            return mTime;
+        }
+
+        @Override
+        void runDelayed() {
+            mRunDelayed++;
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
new file mode 100644
index 0000000..daff7f0
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Aggregates;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Unit tests for {@link ContactAggregator}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -w \
+ *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class ContactAggregatorTest extends BaseContactsProvider2Test {
+
+    private static final String[] AGGREGATION_EXCEPTION_PROJECTION = new String[] {
+            AggregationExceptions.TYPE,
+            AggregationExceptions.AGGREGATE_ID,
+            AggregationExceptions.CONTACT_ID
+    };
+
+    public void testCrudAggregationExceptions() throws Exception {
+        long contactId1 = createContact();
+        long aggregateId = queryAggregateId(contactId1);
+        long contactId2 = createContact();
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_IN, aggregateId, contactId2);
+
+        // Refetch the row we have just inserted
+        Cursor c = mResolver.query(AggregationExceptions.CONTENT_URI,
+                AGGREGATION_EXCEPTION_PROJECTION, AggregationExceptions.AGGREGATE_ID + "="
+                        + aggregateId, null, null);
+
+        assertTrue(c.moveToFirst());
+        assertEquals(AggregationExceptions.TYPE_KEEP_IN, c.getInt(0));
+        assertEquals(aggregateId, c.getLong(1));
+        assertEquals(contactId2, c.getLong(2));
+        assertFalse(c.moveToNext());
+        c.close();
+
+        // Change from TYPE_KEEP_IN to TYPE_KEEP_OUT
+        setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, aggregateId, contactId2);
+
+        c = mResolver.query(AggregationExceptions.CONTENT_URI,
+                AGGREGATION_EXCEPTION_PROJECTION, AggregationExceptions.AGGREGATE_ID + "="
+                        + aggregateId, null, null);
+
+        assertTrue(c.moveToFirst());
+        assertEquals(AggregationExceptions.TYPE_KEEP_OUT, c.getInt(0));
+        assertEquals(aggregateId, c.getLong(1));
+        assertEquals(contactId2, c.getLong(2));
+        assertFalse(c.moveToNext());
+        c.close();
+
+        // Delete the rule
+        setAggregationException(AggregationExceptions.TYPE_AUTOMATIC, aggregateId, contactId2);
+
+        // Verify that the row is gone
+        c = mResolver.query(AggregationExceptions.CONTENT_URI,
+                AGGREGATION_EXCEPTION_PROJECTION, AggregationExceptions.AGGREGATE_ID + "="
+                        + aggregateId, null, null);
+        assertFalse(c.moveToFirst());
+        c.close();
+    }
+
+    public void testAggregationCreatesNewAggregate() {
+        long contactId = createContact();
+
+        Uri resultUri = insertStructuredName(contactId, "Johna", "Smitha");
+
+        // Parse the URI and confirm that it contains an ID
+        assertTrue(ContentUris.parseId(resultUri) != 0);
+
+        long aggregateId = queryAggregateId(contactId);
+        assertTrue(aggregateId != 0);
+
+        String displayName = queryDisplayName(aggregateId);
+        assertEquals("Johna Smitha", displayName);
+    }
+
+    public void testAggregationOfExactFullNameMatch() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Johnb", "Smithb");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Johnb", "Smithb");
+
+        assertAggregated(contactId1, contactId2, "Johnb Smithb");
+    }
+
+    public void testAggregationOfCaseInsensitiveFullNameMatch() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Johnc", "Smithc");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Johnc", "smithc");
+
+        assertAggregated(contactId1, contactId2, "Johnc Smithc");
+    }
+
+    public void testAggregationOfLastNameMatch() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, null, "Johnd");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, null, "johnd");
+
+        assertAggregated(contactId1, contactId2, "Johnd");
+    }
+
+    public void testNonAggregationOfFirstNameMatch() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Johne", "Smithe");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Johne", null);
+
+        assertNotAggregated(contactId1, contactId2);
+    }
+
+    // TODO: should this be allowed to match?
+    public void testNonAggregationOfLastNameMatch() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Johnf", "Smithf");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, null, "Smithf");
+
+        assertNotAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationOfConcatenatedFullNameMatch() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Johng", "Smithg");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "johngsmithg", null);
+
+        assertAggregated(contactId1, contactId2, "Johng Smithg");
+    }
+
+    public void testAggregationOfNormalizedFullNameMatch() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "helene bjorn", null);
+
+        assertAggregated(contactId1, contactId2, "H\u00e9l\u00e8ne Bj\u00f8rn");
+    }
+
+    public void testAggregationBasedOnPhoneNumberNoNameData() {
+        long contactId1 = createContact();
+        insertPhoneNumber(contactId1, "(888)555-1231");
+
+        long contactId2 = createContact();
+        insertPhoneNumber(contactId2, "1(888)555-1231");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationBasedOnPhoneNumberWhenTargetAggregateHasNoName() {
+        long contactId1 = createContact();
+        insertPhoneNumber(contactId1, "(888)555-1232");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Johnl", "Smithl");
+        insertPhoneNumber(contactId2, "1(888)555-1232");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationBasedOnPhoneNumberWhenNewContactHasNoName() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Johnm", "Smithm");
+        insertPhoneNumber(contactId1, "(888)555-1233");
+
+        long contactId2 = createContact();
+        insertPhoneNumber(contactId2, "1(888)555-1233");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationBasedOnPhoneNumberWithSimilarNames() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Ogre", "Hunter");
+        insertPhoneNumber(contactId1, "(888)555-1234");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Opra", "Humper");
+        insertPhoneNumber(contactId2, "1(888)555-1234");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationBasedOnPhoneNumberWithDifferentNames() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Baby", "Bear");
+        insertPhoneNumber(contactId1, "(888)555-1235");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Blind", "Mouse");
+        insertPhoneNumber(contactId2, "1(888)555-1235");
+
+        assertNotAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationBasedOnEmailNoNameData() {
+        long contactId1 = createContact();
+        insertEmail(contactId1, "lightning@android.com");
+
+        long contactId2 = createContact();
+        insertEmail(contactId2, "lightning@android.com");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationBasedOnEmailWhenTargetAggregateHasNoName() {
+        long contactId1 = createContact();
+        insertEmail(contactId1, "mcqueen@android.com");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Lightning", "McQueen");
+        insertEmail(contactId2, "mcqueen@android.com");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationBasedOnEmailWhenNewContactHasNoName() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Doc", "Hudson");
+        insertEmail(contactId1, "doc@android.com");
+
+        long contactId2 = createContact();
+        insertEmail(contactId2, "doc@android.com");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationBasedOnEmailWithSimilarNames() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Sally", "Carrera");
+        insertEmail(contactId1, "sally@android.com");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Sallie", "Carerra");
+        insertEmail(contactId2, "sally@android.com");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationBasedOnEmailWithDifferentNames() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Chick", "Hicks");
+        insertEmail(contactId1, "hicky@android.com");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Luigi", "Guido");
+        insertEmail(contactId2, "hicky@android.com");
+
+        assertNotAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationByCommonNicknameWithLastName() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Bill", "Gore");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "William", "Gore");
+
+        assertAggregated(contactId1, contactId2, "William Gore");
+    }
+
+    public void testAggregationByCommonNicknameOnly() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Lawrence", null);
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Larry", null);
+
+        assertAggregated(contactId1, contactId2, "Lawrence");
+    }
+
+    public void testAggregationByNicknameNoStructuredName() {
+        long contactId1 = createContact();
+        insertNickname(contactId1, "Frozone");
+
+        long contactId2 = createContact();
+        insertNickname(contactId2, "Frozone");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationByNicknameWithSimilarNames() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Buddy", "Pine");
+        insertNickname(contactId1, "Syndrome");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Body", "Pane");
+        insertNickname(contactId2, "Syndrome");
+
+        assertAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationByNicknameWithDifferentNames() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Helen", "Parr");
+        insertNickname(contactId1, "Elastigirl");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Shawn", "Johnson");
+        insertNickname(contactId2, "Elastigirl");
+
+        assertNotAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationExceptionKeepIn() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Johnk", "Smithk");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Johnkx", "Smithkx");
+
+        long aggregateId1 = queryAggregateId(contactId1);
+        long aggregateId2 = queryAggregateId(contactId2);
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_IN,
+                queryAggregateId(contactId1), contactId2);
+
+        assertAggregated(contactId1, contactId2, "Johnkx Smithkx");
+
+        // Assert that the empty aggregate got removed
+        long newAggregateId1 = queryAggregateId(contactId1);
+        if (aggregateId1 != newAggregateId1) {
+            Cursor cursor = queryAggregate(aggregateId1);
+            assertFalse(cursor.moveToFirst());
+            cursor.close();
+        } else {
+            Cursor cursor = queryAggregate(aggregateId2);
+            assertFalse(cursor.moveToFirst());
+            cursor.close();
+        }
+    }
+
+    public void testAggregationExceptionKeepOut() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Johnh", "Smithh");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Johnh", "Smithh");
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_OUT,
+                queryAggregateId(contactId1), contactId2);
+
+        assertNotAggregated(contactId1, contactId2);
+    }
+
+    public void testAggregationExceptionKeepOutCheckUpdatesDisplayName() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Johni", "Smithi");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Johnj", "Smithj");
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_IN,
+                queryAggregateId(contactId1), contactId2);
+
+        assertAggregated(contactId1, contactId2, "Johnj Smithj");
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_OUT,
+                queryAggregateId(contactId1), contactId2);
+
+        assertNotAggregated(contactId1, contactId2);
+
+        String displayName1 = queryDisplayName(queryAggregateId(contactId1));
+        assertEquals("Johni Smithi", displayName1);
+
+        String displayName2 = queryDisplayName(queryAggregateId(contactId2));
+        assertEquals("Johnj Smithj", displayName2);
+    }
+
+    public void testAggregationSuggestionsBasedOnName() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Duane", null);
+
+        // Exact name match
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Duane", null);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_OUT,
+                queryAggregateId(contactId1), contactId2);
+
+        // Edit distance == 0.84
+        long contactId3 = createContact();
+        insertStructuredName(contactId3, "Dwayne", null);
+
+        // Edit distance == 0.6
+        long contactId4 = createContact();
+        insertStructuredName(contactId4, "Donny", null);
+
+        long aggregateId1 = queryAggregateId(contactId1);
+        long aggregateId2 = queryAggregateId(contactId2);
+        long aggregateId3 = queryAggregateId(contactId3);
+
+        assertSuggestions(aggregateId1, aggregateId2, aggregateId3);
+    }
+
+    public void testAggregationSuggestionsBasedOnPhoneNumber() {
+
+        // Create two contacts that would not be aggregated because of name mismatch
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Lord", "Farquaad");
+        insertPhoneNumber(contactId1, "(888)555-1236");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Talking", "Donkey");
+        insertPhoneNumber(contactId2, "1(888)555-1236");
+
+        long aggregateId1 = queryAggregateId(contactId1);
+        long aggregateId2 = queryAggregateId(contactId2);
+        assertTrue(aggregateId1 != aggregateId2);
+
+        assertSuggestions(aggregateId1, aggregateId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnEmailAddress() {
+
+        // Create two contacts that would not be aggregated because of name mismatch
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Carl", "Fredricksen");
+        insertEmail(contactId1, "up@android.com");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Charles", "Muntz");
+        insertEmail(contactId2, "up@android.com");
+
+        long aggregateId1 = queryAggregateId(contactId1);
+        long aggregateId2 = queryAggregateId(contactId2);
+        assertTrue(aggregateId1 != aggregateId2);
+
+        assertSuggestions(aggregateId1, aggregateId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnEmailAddressApproximateMatch() {
+
+        // Create two contacts that would not be aggregated because of name mismatch
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Bob", null);
+        insertEmail(contactId1, "incredible2004@android.com");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Lucius", "Best");
+        insertEmail(contactId2, "incrediball@androidd.com");
+
+        long aggregateId1 = queryAggregateId(contactId1);
+        long aggregateId2 = queryAggregateId(contactId2);
+        assertTrue(aggregateId1 != aggregateId2);
+
+        assertSuggestions(aggregateId1, aggregateId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnNickname() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Peter", "Parker");
+        insertNickname(contactId1, "Spider-Man");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Manny", "Spider");
+
+        long aggregateId1 = queryAggregateId(contactId1);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, aggregateId1, contactId2);
+
+        long aggregateId2 = queryAggregateId(contactId2);
+        assertSuggestions(aggregateId1, aggregateId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnNicknameMatchingName() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Clark", "Kent");
+        insertNickname(contactId1, "Superman");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Roy", "Williams");
+        insertNickname(contactId2, "superman");
+
+        long aggregateId1 = queryAggregateId(contactId1);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, aggregateId1, contactId2);
+
+        long aggregateId2 = queryAggregateId(contactId2);
+        assertSuggestions(aggregateId1, aggregateId2);
+    }
+
+    public void testAggregationSuggestionsBasedOnCommonNickname() {
+        long contactId1 = createContact();
+        insertStructuredName(contactId1, "Dick", "Cherry");
+
+        long contactId2 = createContact();
+        insertStructuredName(contactId2, "Richard", "Cherry");
+
+        long aggregateId1 = queryAggregateId(contactId1);
+        setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, aggregateId1, contactId2);
+
+        long aggregateId2 = queryAggregateId(contactId2);
+        assertSuggestions(aggregateId1, aggregateId2);
+    }
+
+    private void assertSuggestions(long aggregateId, long... suggestions) {
+        final Uri aggregateUri = ContentUris.withAppendedId(Aggregates.CONTENT_URI, aggregateId);
+        Uri uri = Uri.withAppendedPath(aggregateUri,
+                Aggregates.AggregationSuggestions.CONTENT_DIRECTORY);
+        final Cursor cursor = mResolver.query(uri, new String[] { Aggregates._ID },
+                null, null, null);
+
+        assertEquals(suggestions.length, cursor.getCount());
+
+        for (int i = 0; i < suggestions.length; i++) {
+            cursor.moveToNext();
+            assertEquals(suggestions[i], cursor.getLong(0));
+        }
+
+        cursor.close();
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
new file mode 100644
index 0000000..5936f8f
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.Aggregates;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RestrictionExceptions;
+import android.test.IsolatedContext;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.test.mock.MockPackageManager;
+import android.util.Log;
+
+import java.util.HashMap;
+
+/**
+ * Helper class that encapsulates an "actor" which is owned by a specific
+ * package name. It correctly maintains a wrapped {@link Context} and an
+ * attached {@link MockContentResolver}. Multiple actors can be used to test
+ * security scenarios between multiple packages.
+ */
+public class ContactsActor {
+    private static final String FILENAME_PREFIX = "test.";
+
+    public static final String PACKAGE_GREY = "edu.example.grey";
+    public static final String PACKAGE_RED = "net.example.red";
+    public static final String PACKAGE_GREEN = "com.example.green";
+    public static final String PACKAGE_BLUE = "org.example.blue";
+
+    public Context context;
+    public String packageName;
+    public MockContentResolver resolver;
+    public SynchronousContactsProvider2 provider;
+
+    /**
+     * Create an "actor" using the given parent {@link Context} and the specific
+     * package name. Internally, all {@link Context} method calls are passed to
+     * a new instance of {@link RestrictionMockContext}, which stubs out the
+     * security infrastructure.
+     */
+    public ContactsActor(Context overallContext, String packageName) {
+        context = new RestrictionMockContext(overallContext, packageName);
+        this.packageName = packageName;
+        resolver = new MockContentResolver();
+
+        RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(
+                context, overallContext, FILENAME_PREFIX);
+        Context providerContext = new IsolatedContext(resolver, targetContextWrapper);
+
+        provider = new SynchronousContactsProvider2();
+        provider.attachInfo(providerContext, null);
+        resolver.addProvider(ContactsContract.AUTHORITY, provider);
+    }
+
+    /**
+     * Mock {@link Context} that reports specific well-known values for testing
+     * data protection. The creator can override the owner package name, and
+     * force the {@link PackageManager} to always return a well-known package
+     * list for any call to {@link PackageManager#getPackagesForUid(int)}.
+     * <p>
+     * For example, the creator could request that the {@link Context} lives in
+     * package name "com.example.red", and also cause the {@link PackageManager}
+     * to report that no UID contains that package name.
+     */
+    private static class RestrictionMockContext extends MockContext {
+        private final Context mOverallContext;
+        private final String mReportedPackageName;
+        private final RestrictionMockPackageManager mPackageManager;
+
+        /**
+         * Create a {@link Context} under the given package name.
+         */
+        public RestrictionMockContext(Context overallContext, String reportedPackageName) {
+            mOverallContext = overallContext;
+            mReportedPackageName = reportedPackageName;
+            mPackageManager = new RestrictionMockPackageManager();
+            mPackageManager.addPackage(1000, PACKAGE_GREY);
+            mPackageManager.addPackage(2000, PACKAGE_RED);
+            mPackageManager.addPackage(3000, PACKAGE_GREEN);
+            mPackageManager.addPackage(4000, PACKAGE_BLUE);
+        }
+
+        @Override
+        public String getPackageName() {
+            return mReportedPackageName;
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return mPackageManager;
+        }
+
+        @Override
+        public Resources getResources() {
+            return mOverallContext.getResources();
+        }
+    }
+
+    /**
+     * Mock {@link PackageManager} that knows about a specific set of packages
+     * to help test security models. Because {@link Binder#getCallingUid()}
+     * can't be mocked, you'll have to find your mock-UID manually using your
+     * {@link Context#getPackageName()}.
+     */
+    private static class RestrictionMockPackageManager extends MockPackageManager {
+        private final HashMap<Integer, String> mForward = new HashMap<Integer, String>();
+        private final HashMap<String, Integer> mReverse = new HashMap<String, Integer>();
+
+        /**
+         * Add a UID-to-package mapping, which is then stored internally.
+         */
+        public void addPackage(int packageUid, String packageName) {
+            mForward.put(packageUid, packageName);
+            mReverse.put(packageName, packageUid);
+        }
+
+        @Override
+        public String[] getPackagesForUid(int uid) {
+            return new String[] { mForward.get(uid) };
+        }
+
+        @Override
+        public ApplicationInfo getApplicationInfo(String packageName, int flags) {
+            ApplicationInfo info = new ApplicationInfo();
+            Integer uid = mReverse.get(packageName);
+            info.uid = (uid != null) ? uid : -1;
+            return info;
+        }
+    }
+
+    public long createContact(boolean isRestricted, String name) {
+        long contactId = createContact(isRestricted);
+        createName(contactId, name);
+        return contactId;
+    }
+
+    public long createContact(boolean isRestricted) {
+        final ContentValues values = new ContentValues();
+        values.put(Contacts.PACKAGE, packageName);
+        if (isRestricted) {
+            values.put(Contacts.IS_RESTRICTED, 1);
+        }
+
+        Uri contactUri = resolver.insert(Contacts.CONTENT_URI, values);
+        return ContentUris.parseId(contactUri);
+    }
+
+    public long createName(long contactId, String name) {
+        final ContentValues values = new ContentValues();
+        values.put(Data.CONTACT_ID, contactId);
+        values.put(Data.IS_PRIMARY, 1);
+        values.put(Data.IS_SUPER_PRIMARY, 1);
+        values.put(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
+        values.put(CommonDataKinds.StructuredName.FAMILY_NAME, name);
+        Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI,
+                contactId), Contacts.Data.CONTENT_DIRECTORY);
+        Uri dataUri = resolver.insert(insertUri, values);
+        return ContentUris.parseId(dataUri);
+    }
+
+    public long createPhone(long contactId, String phoneNumber) {
+        final ContentValues values = new ContentValues();
+        values.put(Data.CONTACT_ID, contactId);
+        values.put(Data.IS_PRIMARY, 1);
+        values.put(Data.IS_SUPER_PRIMARY, 1);
+        values.put(Data.MIMETYPE, Phones.CONTENT_ITEM_TYPE);
+        values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
+        Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI,
+                contactId), Contacts.Data.CONTENT_DIRECTORY);
+        Uri dataUri = resolver.insert(insertUri, values);
+        return ContentUris.parseId(dataUri);
+    }
+
+    public void updateException(String packageProvider, String packageClient, boolean allowAccess) {
+        final ContentValues values = new ContentValues();
+        values.put(RestrictionExceptions.PACKAGE_PROVIDER, packageProvider);
+        values.put(RestrictionExceptions.PACKAGE_CLIENT, packageClient);
+        values.put(RestrictionExceptions.ALLOW_ACCESS, allowAccess ? 1 : 0);
+        resolver.update(RestrictionExceptions.CONTENT_URI, values, null, null);
+    }
+
+    public long getAggregateForContact(long contactId) {
+        Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        final Cursor cursor = resolver.query(contactUri, Projections.PROJ_CONTACTS, null,
+                null, null);
+        if (!cursor.moveToFirst()) {
+            cursor.close();
+            throw new RuntimeException("Contact didn't have an aggregate");
+        }
+        final long aggId = cursor.getLong(Projections.COL_CONTACTS_AGGREGATE);
+        cursor.close();
+        return aggId;
+    }
+
+    public int getDataCountForAggregate(long aggId) {
+        Uri contactUri = Uri.withAppendedPath(ContentUris.withAppendedId(Aggregates.CONTENT_URI,
+                aggId), Aggregates.Data.CONTENT_DIRECTORY);
+        final Cursor cursor = resolver.query(contactUri, Projections.PROJ_ID, null, null,
+                null);
+        final int count = cursor.getCount();
+        cursor.close();
+        return count;
+    }
+
+    public void setSuperPrimaryPhone(long dataId) {
+        final ContentValues values = new ContentValues();
+        values.put(Data.IS_PRIMARY, 1);
+        values.put(Data.IS_SUPER_PRIMARY, 1);
+        Uri updateUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
+        resolver.update(updateUri, values, null, null);
+    }
+
+    public long getPrimaryPhoneId(long aggId) {
+        Uri aggUri = ContentUris.withAppendedId(Aggregates.CONTENT_URI, aggId);
+        final Cursor cursor = resolver.query(aggUri, Projections.PROJ_AGGREGATES, null,
+                null, null);
+        long primaryPhoneId = -1;
+        if (cursor.moveToFirst()) {
+            primaryPhoneId = cursor.getLong(Projections.COL_AGGREGATES_PRIMARY_PHONE_ID);
+        }
+        cursor.close();
+        return primaryPhoneId;
+    }
+
+    public long createGroup(String groupName) {
+        final ContentValues values = new ContentValues();
+        values.put(ContactsContract.Groups.PACKAGE, packageName);
+        values.put(ContactsContract.Groups.TITLE, groupName);
+        Uri groupUri = resolver.insert(ContactsContract.Groups.CONTENT_URI, values);
+        return ContentUris.parseId(groupUri);
+    }
+
+    public long createGroupMembership(long contactId, long groupId) {
+        final ContentValues values = new ContentValues();
+        values.put(Data.CONTACT_ID, contactId);
+        values.put(Data.MIMETYPE, CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE);
+        values.put(CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId);
+        Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI,
+                contactId), Contacts.Data.CONTENT_DIRECTORY);
+        Uri dataUri = resolver.insert(insertUri, values);
+        return ContentUris.parseId(dataUri);
+    }
+
+    /**
+     * Various internal database projections.
+     */
+    private interface Projections {
+        static final String[] PROJ_ID = new String[] {
+                BaseColumns._ID,
+        };
+
+        static final int COL_ID = 0;
+
+        static final String[] PROJ_CONTACTS = new String[] {
+                Contacts.AGGREGATE_ID
+        };
+
+        static final int COL_CONTACTS_AGGREGATE = 0;
+
+        static final String[] PROJ_AGGREGATES = new String[] {
+                Aggregates.PRIMARY_PHONE_ID
+        };
+
+        static final int COL_AGGREGATES_PRIMARY_PHONE_ID = 0;
+
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
new file mode 100644
index 0000000..fc7beb3
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.providers.contacts.BaseContactsProvider2Test;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Aggregates;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+/**
+ * Unit tests for {@link ContactsProvider2}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -w \
+ *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class ContactsProvider2Test extends BaseContactsProvider2Test {
+
+    public void testDisplayNameParsingWhenPartsUnspecified() {
+        long contactId = createContact();
+        ContentValues values = new ContentValues();
+        values.put(StructuredName.DISPLAY_NAME, "Mr.John Kevin von Smith, Jr.");
+        insertStructuredName(contactId, values);
+
+        assertStructuredName(contactId, "Mr", "John", "Kevin", "von Smith", "Jr");
+    }
+
+    public void testDisplayNameParsingWhenPartsSpecified() {
+        long contactId = createContact();
+        ContentValues values = new ContentValues();
+        values.put(StructuredName.DISPLAY_NAME, "Mr.John Kevin von Smith, Jr.");
+        values.put(StructuredName.FAMILY_NAME, "Johnson");
+        insertStructuredName(contactId, values);
+
+        assertStructuredName(contactId, null, null, null, "Johnson", null);
+    }
+
+    public void testSendToVoicemailDefault() {
+        long contactId = createContact();
+        long aggregateId = queryAggregateId(contactId);
+
+        Cursor c = queryAggregate(aggregateId);
+        assertTrue(c.moveToNext());
+        int sendToVoicemail = c.getInt(c.getColumnIndex(Aggregates.SEND_TO_VOICEMAIL));
+        assertEquals(0, sendToVoicemail);
+        c.close();
+    }
+
+    public void testSetSendToVoicemailAndRingtone() {
+        long contactId = createContact();
+        long aggregateId = queryAggregateId(contactId);
+
+        updateSendToVoicemailAndRingtone(aggregateId, true, "foo");
+        assertSendToVoicemailAndRingtone(aggregateId, true, "foo");
+    }
+
+    public void testSendToVoicemailAndRingtoneAfterAggregation() {
+        long contactId1 = createContact();
+        long aggregateId1 = queryAggregateId(contactId1);
+        updateSendToVoicemailAndRingtone(aggregateId1, true, "foo");
+
+        long contactId2 = createContact();
+        long aggregateId2 = queryAggregateId(contactId2);
+        updateSendToVoicemailAndRingtone(aggregateId2, true, "bar");
+
+        // Aggregate them
+        setAggregationException(AggregationExceptions.TYPE_KEEP_IN, aggregateId1, contactId2);
+
+        // Both contacts had "send to VM", the aggregate now has the same value
+        assertSendToVoicemailAndRingtone(aggregateId1, true, "foo,bar"); // Either foo or bar
+    }
+
+    public void testDoNotSendToVoicemailAfterAggregation() {
+        long contactId1 = createContact();
+        long aggregateId1 = queryAggregateId(contactId1);
+        updateSendToVoicemailAndRingtone(aggregateId1, true, null);
+
+        long contactId2 = createContact();
+        long aggregateId2 = queryAggregateId(contactId2);
+        updateSendToVoicemailAndRingtone(aggregateId2, false, null);
+
+        // Aggregate them
+        setAggregationException(AggregationExceptions.TYPE_KEEP_IN, aggregateId1, contactId2);
+
+        // Since one of the contacts had "don't send to VM" that setting wins for the aggregate
+        assertSendToVoicemailAndRingtone(aggregateId1, false, null);
+    }
+
+    public void testSetSendToVoicemailAndRingtonePreservedAfterJoinAndSplit() {
+        long contactId1 = createContact();
+        long aggregateId1 = queryAggregateId(contactId1);
+        updateSendToVoicemailAndRingtone(aggregateId1, true, "foo");
+
+        long contactId2 = createContact();
+        long aggregateId2 = queryAggregateId(contactId2);
+        updateSendToVoicemailAndRingtone(aggregateId2, false, "bar");
+
+        // Aggregate them
+        setAggregationException(AggregationExceptions.TYPE_KEEP_IN, aggregateId1, contactId2);
+
+        // Split them
+        setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, aggregateId1, contactId2);
+
+        assertSendToVoicemailAndRingtone(aggregateId1, true, "foo");
+        assertSendToVoicemailAndRingtone(queryAggregateId(contactId2), false, "bar");
+    }
+
+    public void testSinglePresenceRowPerAggregate() {
+        int protocol1 = Im.PROTOCOL_GOOGLE_TALK;
+        String handle1 = "test@gmail.com";
+
+        long contactId1 = createContact();
+        insertImHandle(contactId1, protocol1, handle1);
+
+        insertPresence(protocol1, handle1, Presence.AVAILABLE);
+        insertPresence(protocol1, handle1, Presence.AWAY);
+        insertPresence(protocol1, handle1, Presence.INVISIBLE);
+
+        Cursor c = queryAggregateSummary(queryAggregateId(contactId1),
+                new String[] {Presence.PRESENCE_STATUS});
+        assertEquals(c.getCount(), 1);
+
+        c.moveToFirst();
+        assertEquals(c.getInt(0), Presence.AVAILABLE);
+
+    }
+
+    private void updateSendToVoicemailAndRingtone(long aggregateId, boolean sendToVoicemail,
+            String ringtone) {
+        ContentValues values = new ContentValues();
+        values.put(Aggregates.SEND_TO_VOICEMAIL, sendToVoicemail);
+        if (ringtone != null) {
+            values.put(Aggregates.CUSTOM_RINGTONE, ringtone);
+        }
+
+        final Uri uri = ContentUris.withAppendedId(Aggregates.CONTENT_URI, aggregateId);
+        int count = mResolver.update(uri, values, null, null);
+        assertEquals(1, count);
+    }
+
+    private void assertSendToVoicemailAndRingtone(long aggregateId, boolean expectedSendToVoicemail,
+            String expectedRingtone) {
+        Cursor c = queryAggregate(aggregateId);
+        assertTrue(c.moveToNext());
+        int sendToVoicemail = c.getInt(c.getColumnIndex(Aggregates.SEND_TO_VOICEMAIL));
+        assertEquals(expectedSendToVoicemail ? 1 : 0, sendToVoicemail);
+        String ringtone = c.getString(c.getColumnIndex(Aggregates.CUSTOM_RINGTONE));
+        if (expectedRingtone == null) {
+            assertNull(ringtone);
+        } else {
+            assertTrue(ArrayUtils.contains(expectedRingtone.split(","), ringtone));
+        }
+        c.close();
+    }
+}
+
diff --git a/tests/src/com/android/providers/contacts/GroupsTest.java b/tests/src/com/android/providers/contacts/GroupsTest.java
new file mode 100644
index 0000000..a9428ae
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/GroupsTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
+
+import android.database.Cursor;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Unit tests for {@link Groups} and {@link GroupMembership}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -w \
+ *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class GroupsTest extends AndroidTestCase {
+
+    private ContactsActor mActor;
+    private MockContentResolver mResolver;
+
+    public GroupsTest() {
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mActor = new ContactsActor(getContext(), PACKAGE_GREY);
+        mResolver = mActor.resolver;
+    }
+
+    private static final String GROUP_GREY = "Grey";
+    private static final String GROUP_RED = "Red";
+    private static final String GROUP_GREEN = "Green";
+    private static final String GROUP_BLUE = "Blue";
+
+    private static final String PERSON_ALPHA = "Alpha";
+    private static final String PERSON_BRAVO = "Bravo";
+    private static final String PERSON_CHARLIE = "Charlie";
+    private static final String PERSON_DELTA = "Delta";
+
+    private static final String PHONE_ALPHA = "555-1111";
+    private static final String PHONE_BRAVO_1 = "555-2222";
+    private static final String PHONE_BRAVO_2 = "555-3333";
+    private static final String PHONE_CHARLIE_1 = "555-4444";
+    private static final String PHONE_CHARLIE_2 = "555-5555";
+
+    public void testGroupSummary() {
+
+        // Clear any existing data before starting
+        mActor.provider.wipeData();
+
+        // Create a handful of groups
+        long groupGrey = mActor.createGroup(GROUP_GREY);
+        long groupRed = mActor.createGroup(GROUP_RED);
+        long groupGreen = mActor.createGroup(GROUP_GREEN);
+        long groupBlue = mActor.createGroup(GROUP_BLUE);
+
+        // Create a handful of contacts
+        long contactAlpha = mActor.createContact(false, PERSON_ALPHA);
+        long contactBravo = mActor.createContact(false, PERSON_BRAVO);
+        long contactCharlie = mActor.createContact(false, PERSON_CHARLIE);
+        long contactCharlieDupe = mActor.createContact(false, PERSON_CHARLIE);
+        long contactDelta = mActor.createContact(false, PERSON_DELTA);
+
+        // Make sure that Charlie was aggregated
+        {
+            long aggCharlie = mActor.getAggregateForContact(contactCharlie);
+            long aggCharlieDupe = mActor.getAggregateForContact(contactCharlieDupe);
+            assertTrue("Didn't aggregate two contacts with identical names",
+                    (aggCharlie == aggCharlieDupe));
+        }
+
+        // Add phone numbers to specific contacts
+        mActor.createPhone(contactAlpha, PHONE_ALPHA);
+        mActor.createPhone(contactBravo, PHONE_BRAVO_1);
+        mActor.createPhone(contactBravo, PHONE_BRAVO_2);
+        mActor.createPhone(contactCharlie, PHONE_CHARLIE_1);
+        mActor.createPhone(contactCharlieDupe, PHONE_CHARLIE_2);
+
+        // Add contacts to various mixture of groups. Grey will have all
+        // contacts, Red only with phone numbers, Green with no phones, and Blue
+        // with no contacts at all.
+        mActor.createGroupMembership(contactAlpha, groupGrey);
+        mActor.createGroupMembership(contactBravo, groupGrey);
+        mActor.createGroupMembership(contactCharlie, groupGrey);
+        mActor.createGroupMembership(contactDelta, groupGrey);
+
+        mActor.createGroupMembership(contactAlpha, groupRed);
+        mActor.createGroupMembership(contactBravo, groupRed);
+        mActor.createGroupMembership(contactCharlie, groupRed);
+
+        mActor.createGroupMembership(contactDelta, groupGreen);
+
+        // Walk across groups summary cursor and verify returned counts.
+        final Cursor cursor = mActor.resolver.query(Groups.CONTENT_SUMMARY_URI,
+                Projections.PROJ_SUMMARY, null, null, null);
+
+        // Require that each group has a summary row
+        assertTrue("Didn't return summary for all groups", (cursor.getCount() == 4));
+
+        while (cursor.moveToNext()) {
+            final long groupId = cursor.getLong(Projections.COL_ID);
+            final int summaryCount = cursor.getInt(Projections.COL_SUMMARY_COUNT);
+            final int summaryWithPhones = cursor.getInt(Projections.COL_SUMMARY_WITH_PHONES);
+
+            if (groupId == groupGrey) {
+                // Grey should have four aggregates, three with phones.
+                assertTrue("Incorrect Grey count", (summaryCount == 4));
+                assertTrue("Incorrect Grey with phones count", (summaryWithPhones == 3));
+            } else if (groupId == groupRed) {
+                // Red should have 3 aggregates, all with phones.
+                assertTrue("Incorrect Red count", (summaryCount == 3));
+                assertTrue("Incorrect Red with phones count", (summaryWithPhones == 3));
+            } else if (groupId == groupGreen) {
+                // Green should have 1 aggregate, none with phones.
+                assertTrue("Incorrect Green count", (summaryCount == 1));
+                assertTrue("Incorrect Green with phones count", (summaryWithPhones == 0));
+            } else if (groupId == groupBlue) {
+                // Blue should have no contacts.
+                assertTrue("Incorrect Blue count", (summaryCount == 0));
+                assertTrue("Incorrect Blue with phones count", (summaryWithPhones == 0));
+            } else {
+                fail("Unrecognized group in summary cursor");
+            }
+        }
+
+    }
+
+    private interface Projections {
+        public static final String[] PROJ_SUMMARY = new String[] {
+            Groups._ID,
+            Groups.SUMMARY_COUNT,
+            Groups.SUMMARY_WITH_PHONES,
+        };
+
+        public static final int COL_ID = 0;
+        public static final int COL_SUMMARY_COUNT = 1;
+        public static final int COL_SUMMARY_WITH_PHONES = 2;
+    }
+
+}
diff --git a/tests/src/com/android/providers/contacts/JaroWinklerDistanceTest.java b/tests/src/com/android/providers/contacts/JaroWinklerDistanceTest.java
new file mode 100644
index 0000000..ad34bba
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/JaroWinklerDistanceTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+@SmallTest
+public class JaroWinklerDistanceTest extends TestCase {
+
+    private JaroWinklerDistance mJaroWinklerDistance;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mJaroWinklerDistance = new JaroWinklerDistance(10);
+    }
+
+    public void testExactMatch() {
+        assertFloat(1, "Dwayne", "Dwayne");
+    }
+
+    public void testWinklerBonus() {
+        assertFloat(0.961f, "Martha", "Marhta");
+        assertFloat(0.840f, "Dwayne", "Duane");
+        assertFloat(0.813f, "DIXON", "DICKSONX");
+    }
+
+    public void testJaroDistance() {
+        assertFloat(0.600f, "Donny", "Duane");
+    }
+
+    public void testPoorMatch() {
+        assertFloat(0.467f, "Johny", "Duane");
+    }
+
+    public void testNoMatches() {
+        assertFloat(0, "Abcd", "Efgh");
+    }
+
+    private void assertFloat(float expected, String name1, String name2) {
+        byte[] s1 = Hex.decodeHex(NameNormalizer.normalize(name1));
+        byte[] s2 = Hex.decodeHex(NameNormalizer.normalize(name2));
+
+        float actual = mJaroWinklerDistance.getDistance(s1, s2);
+        assertTrue("Expected Jaro-Winkler distance: " + expected + ", actual: " + actual,
+                Math.abs(actual - expected) < 0.001);
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/NameNormalizerTest.java b/tests/src/com/android/providers/contacts/NameNormalizerTest.java
new file mode 100644
index 0000000..08d873f
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/NameNormalizerTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link NameNormalizer}.
+ */
+@SmallTest
+public class NameNormalizerTest extends TestCase {
+
+    public void testDifferent() {
+        final String name1 = NameNormalizer.normalize("Helene");
+        final String name2 = NameNormalizer.normalize("Francesca");
+        assertFalse(name2.equals(name1));
+    }
+
+    public void testAccents() {
+        final String name1 = NameNormalizer.normalize("Helene");
+        final String name2 = NameNormalizer.normalize("H\u00e9l\u00e8ne");
+        assertTrue(name2.equals(name1));
+    }
+
+    public void testMixedCase() {
+        final String name1 = NameNormalizer.normalize("Helene");
+        final String name2 = NameNormalizer.normalize("hELENE");
+        assertTrue(name2.equals(name1));
+    }
+
+    public void testNonLetters() {
+        final String name1 = NameNormalizer.normalize("h-e?l e+n=e");
+        final String name2 = NameNormalizer.normalize("helene");
+        assertTrue(name2.equals(name1));
+    }
+
+    public void testComplexityCase() {
+        assertTrue(NameNormalizer.compareComplexity("Helene", "helene") > 0);
+    }
+
+    public void testComplexityAccent() {
+        assertTrue(NameNormalizer.compareComplexity("H\u00e9lene", "Helene") > 0);
+    }
+
+    public void testComplexityLength() {
+        assertTrue(NameNormalizer.compareComplexity("helene2009", "helene") > 0);
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/NameSplitterTest.java b/tests/src/com/android/providers/contacts/NameSplitterTest.java
new file mode 100644
index 0000000..91ce025
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/NameSplitterTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import junit.framework.TestCase;
+
+import com.android.providers.contacts.NameSplitter.Name;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+/**
+ * Tests for {@link NameSplitter}.
+ */
+@SmallTest
+public class NameSplitterTest extends TestCase {
+    private NameSplitter mNameSplitter;
+    private Name mName;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mNameSplitter = new NameSplitter("Mr, Ms, Mrs", "d', st, st., von", "Jr, M.D., MD, D.D.S.",
+                "&, AND");
+        mName = new Name();
+    }
+
+    public void testNull() {
+        assertSplitName(null, null, null, null, null, null);
+    }
+
+    public void testEmpty() {
+        assertSplitName("", null, null, null, null, null);
+    }
+
+    public void testSpaces() {
+        assertSplitName(" ", null, null, null, null, null);
+    }
+
+    public void testLastName() {
+        assertSplitName("Smith", null, null, null, "Smith", null);
+    }
+
+    public void testFirstLastName() {
+        assertSplitName("John Smith", null, "John", null, "Smith", null);
+    }
+
+    public void testFirstMiddleLastName() {
+        assertSplitName("John Edward Smith", null, "John", "Edward", "Smith", null);
+    }
+
+    public void testThreeNamesAndLastName() {
+        assertSplitName("John Edward Kevin Smith", null, "John Edward", "Kevin", "Smith", null);
+    }
+
+    public void testPrefixFirstLastName() {
+        assertSplitName("Mr. John Smith", "Mr", "John", null, "Smith", null);
+        assertSplitName("Mr.John Smith", "Mr", "John", null, "Smith", null);
+    }
+
+    public void testFirstLastNameSuffix() {
+        assertSplitName("John Smith Jr.", null, "John", null, "Smith", "Jr");
+    }
+
+    public void testFirstLastNameSuffixWithDot() {
+        assertSplitName("John Smith M.D.", null, "John", null, "Smith", "M.D.");
+        assertSplitName("John Smith D D S", null, "John", null, "Smith", "D D S");
+    }
+
+    public void testFirstSuffixLastName() {
+        assertSplitName("John von Smith", null, "John", null, "von Smith", null);
+    }
+
+    public void testFirstSuffixLastNameWithDot() {
+        assertSplitName("John St.Smith", null, "John", null, "St. Smith", null);
+    }
+
+    public void testPrefixFirstMiddleLast() {
+        assertSplitName("Mr. John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
+        assertSplitName("Mr.John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
+    }
+
+    public void testPrefixFirstMiddleLastSuffix() {
+        assertSplitName("Mr. John Kevin Smith Jr.", "Mr", "John", "Kevin", "Smith", "Jr");
+    }
+
+    public void testPrefixFirstMiddlePrefixLastSuffixWrongCapitalization() {
+        assertSplitName("MR. john keVin VON SmiTh JR.", "MR", "john", "keVin", "VON SmiTh", "JR");
+    }
+
+    public void testPrefixLastSuffix() {
+        assertSplitName("von Smith Jr.", null, null, null, "von Smith", "Jr");
+    }
+
+    public void testTwoNamesAndLastNameWithAmpersand() {
+        assertSplitName("John & Edward Smith", null, "John & Edward", null, "Smith", null);
+        assertSplitName("John and Edward Smith", null, "John and Edward", null, "Smith", null);
+    }
+
+    public void testWithMiddleInitialAndNoDot() {
+        assertSplitName("John E. Smith", null, "John", "E", "Smith", null);
+    }
+
+    public void testWithLongFirstNameAndDot() {
+        assertSplitName("John Ed. K. Smith", null, "John Ed.", "K", "Smith", null);
+      }
+
+    private void assertSplitName(String fullName, String prefix, String givenNames,
+            String middleName, String lastName, String suffix) {
+        mNameSplitter.split(mName, fullName);
+        assertEquals(prefix, mName.getPrefix());
+        assertEquals(givenNames, mName.getGivenNames());
+        assertEquals(middleName, mName.getMiddleName());
+        assertEquals(lastName, mName.getFamilyName());
+        assertEquals(suffix, mName.getSuffix());
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
new file mode 100644
index 0000000..aad267f
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import static com.android.providers.contacts.ContactsActor.PACKAGE_BLUE;
+import static com.android.providers.contacts.ContactsActor.PACKAGE_GREEN;
+import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
+import static com.android.providers.contacts.ContactsActor.PACKAGE_RED;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.Aggregates;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RestrictionExceptions;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Unit tests for {@link RestrictionExceptions}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -w \
+ *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class RestrictionExceptionsTest extends AndroidTestCase {
+    private static final String TAG = "RestrictionExceptionsTest";
+
+    private static ContactsActor mGrey;
+    private static ContactsActor mRed;
+    private static ContactsActor mGreen;
+    private static ContactsActor mBlue;
+
+    private static final String PHONE_GREY = "555-1111";
+    private static final String PHONE_RED = "555-2222";
+    private static final String PHONE_GREEN = "555-3333";
+    private static final String PHONE_BLUE = "555-4444";
+
+    private static final String GENERIC_NAME = "Smith";
+
+    public RestrictionExceptionsTest() {
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        final Context overallContext = this.getContext();
+
+        // Build each of our specific actors in their own Contexts
+        mGrey = new ContactsActor(overallContext, PACKAGE_GREY);
+        mRed = new ContactsActor(overallContext, PACKAGE_RED);
+        mGreen = new ContactsActor(overallContext, PACKAGE_GREEN);
+        mBlue = new ContactsActor(overallContext, PACKAGE_BLUE);
+    }
+
+    /**
+     * Create various contacts that are both open and restricted, and assert
+     * that both {@link Contacts#IS_RESTRICTED} and
+     * {@link RestrictionExceptions} are being applied correctly.
+     */
+    public void testDataRestriction() {
+
+        // Clear all previous data before starting this test
+        mGrey.provider.wipeData();
+
+        // Grey creates an unprotected contact
+        long greyContact = mGrey.createContact(false);
+        long greyData = mGrey.createPhone(greyContact, PHONE_GREY);
+        long greyAgg = mGrey.getAggregateForContact(greyContact);
+
+        // Assert that both Grey and Blue can read contact
+        assertTrue("Owner of unrestricted contact unable to read",
+                (mGrey.getDataCountForAggregate(greyAgg) == 1));
+        assertTrue("Non-owner of unrestricted contact unable to read",
+                (mBlue.getDataCountForAggregate(greyAgg) == 1));
+
+        // Red grants protected access to itself
+        mRed.updateException(PACKAGE_RED, PACKAGE_RED, true);
+
+        // Red creates a protected contact
+        long redContact = mRed.createContact(true);
+        long redData = mRed.createPhone(redContact, PHONE_RED);
+        long redAgg = mRed.getAggregateForContact(redContact);
+
+        // Assert that only Red can read contact
+        assertTrue("Owner of restricted contact unable to read",
+                (mRed.getDataCountForAggregate(redAgg) == 1));
+        assertTrue("Non-owner of restricted contact able to read",
+                (mBlue.getDataCountForAggregate(redAgg) == 0));
+        assertTrue("Non-owner of restricted contact able to read",
+                (mGreen.getDataCountForAggregate(redAgg) == 0));
+
+        try {
+            // Blue tries to grant an exception for Red data, which should throw
+            // exception. If it somehow worked, fail this test.
+            mBlue.updateException(PACKAGE_RED, PACKAGE_BLUE, true);
+            fail("Non-owner able to grant restriction exception");
+
+        } catch (RuntimeException e) {
+        }
+
+        // Red grants exception to Blue for contact
+        mRed.updateException(PACKAGE_RED, PACKAGE_BLUE, true);
+
+        // Both Blue and Red can read Red contact, but still not Green
+        assertTrue("Owner of restricted contact unable to read",
+                (mRed.getDataCountForAggregate(redAgg) == 1));
+        assertTrue("Non-owner with restriction exception unable to read",
+                (mBlue.getDataCountForAggregate(redAgg) == 1));
+        assertTrue("Non-owner of restricted contact able to read",
+                (mGreen.getDataCountForAggregate(redAgg) == 0));
+
+        // Red revokes exception to Blue
+        mRed.updateException(PACKAGE_RED, PACKAGE_BLUE, false);
+
+        // Assert that only Red can read contact
+        assertTrue("Owner of restricted contact unable to read",
+                (mRed.getDataCountForAggregate(redAgg) == 1));
+        assertTrue("Non-owner of restricted contact able to read",
+                (mBlue.getDataCountForAggregate(redAgg) == 0));
+        assertTrue("Non-owner of restricted contact able to read",
+                (mGreen.getDataCountForAggregate(redAgg) == 0));
+
+    }
+
+    /**
+     * Create an aggregate that has multiple contacts with various levels of
+     * protected data, and ensure that {@link Aggregates#CONTENT_SUMMARY_URI}
+     * details don't expose {@link Contacts#IS_RESTRICTED} data.
+     */
+    public void testAggregateSummary() {
+
+        // Clear all previous data before starting this test
+        mGrey.provider.wipeData();
+
+        // Red grants exceptions to itself and Grey
+        mRed.updateException(PACKAGE_RED, PACKAGE_RED, true);
+        mRed.updateException(PACKAGE_RED, PACKAGE_GREY, true);
+
+        // Red creates a protected contact
+        long redContact = mRed.createContact(true);
+        long redName = mRed.createName(redContact, GENERIC_NAME);
+        long redPhone = mRed.createPhone(redContact, PHONE_RED);
+
+        // Blue grants exceptions to itself and Grey
+        mBlue.updateException(PACKAGE_BLUE, PACKAGE_BLUE, true);
+        mBlue.updateException(PACKAGE_BLUE, PACKAGE_GREY, true);
+
+        // Blue creates a protected contact
+        long blueContact = mBlue.createContact(true);
+        long blueName = mBlue.createName(blueContact, GENERIC_NAME);
+        long bluePhone = mBlue.createPhone(blueContact, PHONE_BLUE);
+
+        // Set the super-primary phone number to Red
+        mRed.setSuperPrimaryPhone(redPhone);
+
+        // Make sure both aggregates were joined
+        long singleAgg;
+        {
+            long redAgg = mRed.getAggregateForContact(redContact);
+            long blueAgg = mBlue.getAggregateForContact(blueContact);
+            assertTrue("Two contacts with identical name not aggregated correctly",
+                    (redAgg == blueAgg));
+            singleAgg = redAgg;
+        }
+
+        // Grey and Red querying summary should see Red phone. Blue shouldn't
+        // see any summary data, since it's own data is protected and it's not
+        // the super-primary. Green shouldn't know this aggregate exists.
+        assertTrue("Participant with restriction exception reading incorrect summary",
+                (mGrey.getPrimaryPhoneId(singleAgg) == redPhone));
+        assertTrue("Participant with super-primary restricted data reading incorrect summary",
+                (mRed.getPrimaryPhoneId(singleAgg) == redPhone));
+        assertTrue("Participant with non-super-primary restricted data reading incorrect summary",
+                (mBlue.getPrimaryPhoneId(singleAgg) == 0));
+        assertTrue("Non-participant able to discover aggregate existance",
+                (mGreen.getPrimaryPhoneId(singleAgg) == 0));
+
+        // Add an unprotected Grey contact into the mix
+        long greyContact = mGrey.createContact(false);
+        long greyName = mGrey.createName(greyContact, GENERIC_NAME);
+        long greyPhone = mGrey.createPhone(greyContact, PHONE_GREY);
+
+        // Set the super-primary phone number to Blue
+        mBlue.setSuperPrimaryPhone(bluePhone);
+
+        // Make sure all three aggregates were joined
+        {
+            long redAgg = mRed.getAggregateForContact(redContact);
+            long blueAgg = mBlue.getAggregateForContact(blueContact);
+            long greyAgg = mGrey.getAggregateForContact(greyContact);
+            assertTrue("Three contacts with identical name not aggregated correctly",
+                    (redAgg == blueAgg) && (blueAgg == greyAgg));
+            singleAgg = redAgg;
+        }
+
+        // Grey and Blue querying summary should see Blue phone. Red should see
+        // the Grey phone in its summary, since it's the unprotected fallback.
+        // Red doesn't see its own phone number because it's not super-primary,
+        // and is protected. Again, green shouldn't know this exists.
+        assertTrue("Participant with restriction exception reading incorrect summary",
+                (mGrey.getPrimaryPhoneId(singleAgg) == bluePhone));
+        assertTrue("Participant with non-super-primary restricted data reading incorrect summary",
+                (mRed.getPrimaryPhoneId(singleAgg) == greyPhone));
+        assertTrue("Participant with super-primary restricted data reading incorrect summary",
+                (mBlue.getPrimaryPhoneId(singleAgg) == bluePhone));
+        assertTrue("Non-participant couldn't find unrestricted primary through summary",
+                (mGreen.getPrimaryPhoneId(singleAgg) == greyPhone));
+
+    }
+
+    /**
+     * Create a contact that is completely restricted and isolated in its own
+     * aggregate, and make sure that another actor can't detect its existence.
+     */
+    public void testRestrictionSilence() {
+        Cursor cursor;
+
+        // Clear all previous data before starting this test
+        mGrey.provider.wipeData();
+
+        // Green grants exception to itself
+        mGreen.updateException(PACKAGE_GREEN, PACKAGE_GREEN, true);
+
+        // Green creates a protected contact
+        long greenContact = mGreen.createContact(true);
+        long greenData = mGreen.createPhone(greenContact, PHONE_GREEN);
+        long greenAgg = mGreen.getAggregateForContact(greenContact);
+
+        // AGGREGATES
+        cursor = mRed.resolver
+                .query(Aggregates.CONTENT_URI, Projections.PROJ_ID, null, null, null);
+        while (cursor.moveToNext()) {
+            assertTrue("Discovered restricted contact",
+                    (cursor.getLong(Projections.COL_ID) != greenAgg));
+        }
+        cursor.close();
+
+        // AGGREGATES_ID
+        cursor = mRed.resolver.query(ContentUris.withAppendedId(Aggregates.CONTENT_URI, greenAgg),
+                Projections.PROJ_ID, null, null, null);
+        assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+        cursor.close();
+
+        // AGGREGATES_DATA
+        cursor = mRed.resolver.query(Uri.withAppendedPath(ContentUris.withAppendedId(
+                Aggregates.CONTENT_URI, greenAgg), Aggregates.Data.CONTENT_DIRECTORY),
+                Projections.PROJ_ID, null, null, null);
+        assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+        cursor.close();
+
+        // AGGREGATES_SUMMARY
+        cursor = mRed.resolver.query(Aggregates.CONTENT_SUMMARY_URI, Projections.PROJ_ID, null,
+                null, null);
+        while (cursor.moveToNext()) {
+            assertTrue("Discovered restricted contact",
+                    (cursor.getLong(Projections.COL_ID) != greenAgg));
+        }
+        cursor.close();
+
+        // AGGREGATES_SUMMARY_ID
+        cursor = mRed.resolver.query(ContentUris.withAppendedId(Aggregates.CONTENT_SUMMARY_URI,
+                greenAgg), Projections.PROJ_ID, null, null, null);
+        assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+        cursor.close();
+
+        // TODO: AGGREGATES_SUMMARY_FILTER
+        // TODO: =========================
+
+        // TODO: AGGREGATION_SUGGESTIONS
+        // TODO: =======================
+
+        // CONTACTS
+        cursor = mRed.resolver.query(Contacts.CONTENT_URI, Projections.PROJ_ID, null, null, null);
+        while (cursor.moveToNext()) {
+            assertTrue("Discovered restricted contact",
+                    (cursor.getLong(Projections.COL_ID) != greenContact));
+        }
+        cursor.close();
+
+        // CONTACTS_ID
+        cursor = mRed.resolver.query(ContentUris
+                .withAppendedId(Contacts.CONTENT_URI, greenContact), Projections.PROJ_ID, null,
+                null, null);
+        assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+        cursor.close();
+
+        // CONTACTS_DATA
+        cursor = mRed.resolver.query(Uri.withAppendedPath(ContentUris.withAppendedId(
+                Contacts.CONTENT_URI, greenContact), Contacts.Data.CONTENT_DIRECTORY),
+                Projections.PROJ_ID, null, null, null);
+        assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+        cursor.close();
+
+        // TODO: CONTACTS_FILTER_EMAIL
+        // TODO: =====================
+
+        // DATA
+        cursor = mRed.resolver.query(Data.CONTENT_URI, Projections.PROJ_ID, null, null, null);
+        while (cursor.moveToNext()) {
+            assertTrue("Discovered restricted contact",
+                    (cursor.getLong(Projections.COL_ID) != greenData));
+        }
+        cursor.close();
+
+        // DATA_ID
+        cursor = mRed.resolver.query(ContentUris.withAppendedId(Data.CONTENT_URI, greenData),
+                Projections.PROJ_ID, null, null, null);
+        assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+        cursor.close();
+
+        // TODO: PHONE_LOOKUP
+        // TODO: ============
+
+    }
+
+    private interface Projections {
+        static final String[] PROJ_ID = new String[] {
+                BaseColumns._ID,
+        };
+
+        static final int COL_ID = 0;
+    }
+
+}
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
new file mode 100644
index 0000000..fa3c2a6
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import android.content.Context;
+
+/**
+ * A version of {@link ContactsProvider2} class that performs aggregation
+ * synchronously and wipes all data at construction time.
+ */
+public class SynchronousContactsProvider2 extends ContactsProvider2 {
+    private static Boolean sDataWiped = false;
+    private static OpenHelper mOpenHelper;
+
+    public SynchronousContactsProvider2() {
+        super(new SynchronousAggregationScheduler());
+    }
+
+    @Override
+    protected OpenHelper getOpenHelper(final Context context) {
+        if (mOpenHelper == null) {
+            mOpenHelper = new OpenHelper(context);
+        }
+        return mOpenHelper;
+    }
+
+    @Override
+    public boolean onCreate() {
+        boolean created = super.onCreate();
+        synchronized (sDataWiped) {
+            if (!sDataWiped) {
+                sDataWiped = true;
+                wipeData();
+            }
+        }
+        return created;
+    }
+
+    private static class SynchronousAggregationScheduler extends ContactAggregationScheduler {
+
+        @Override
+        public void start() {
+        }
+
+        @Override
+        public void stop() {
+        }
+
+        @Override
+        long currentTime() {
+            return 0;
+        }
+
+        @Override
+        void runDelayed() {
+            super.run();
+        }
+
+    }
+}