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();
+ }
+
+ }
+}