Merge "CTS tests for Contactables api" into jb-mr2-dev
diff --git a/tests/tests/provider/src/android/provider/cts/ContactsContract_DataTest.java b/tests/tests/provider/src/android/provider/cts/ContactsContract_DataTest.java
index 612163c..51d62a3 100644
--- a/tests/tests/provider/src/android/provider/cts/ContactsContract_DataTest.java
+++ b/tests/tests/provider/src/android/provider/cts/ContactsContract_DataTest.java
@@ -22,9 +22,16 @@
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.database.Cursor;
 import android.net.Uri;
 import android.os.SystemClock;
 import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Contactables;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.cts.ContactsContract_TestDataBuilder.TestContact;
@@ -36,6 +43,8 @@
 import android.provider.cts.contacts.RawContactUtil;
 import android.test.InstrumentationTestCase;
 
+import java.util.ArrayList;
+
 public class ContactsContract_DataTest extends InstrumentationTestCase {
     private ContentResolver mResolver;
     private ContactsContract_TestDataBuilder mBuilder;
@@ -93,6 +102,86 @@
                 lookupContact.getId(), data.load().getRawContact().load().getContactId());
     }
 
+    public void testContactablesUri() throws Exception {
+        TestRawContact rawContact = mBuilder.newRawContact()
+                .with(RawContacts.ACCOUNT_TYPE, "test_account")
+                .with(RawContacts.ACCOUNT_NAME, "test_name")
+                .insert();
+        rawContact.newDataRow(CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+                .with(Email.DATA, "test@test.com")
+                .with(Email.TYPE, Email.TYPE_WORK)
+                .insert();
+        ContentValues cv = new ContentValues();
+        cv.put(Email.DATA, "test@test.com");
+        cv.put(Email.TYPE, Email.TYPE_WORK);
+
+        Uri contentUri = ContactsContract.CommonDataKinds.Contactables.CONTENT_URI;
+        try {
+            assertCursorStoredValuesWithRawContactsFilter(contentUri,
+                    new long[] {rawContact.getId()}, cv);
+            rawContact.newDataRow(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)
+                    .with(CommonDataKinds.StructuredPostal.DATA1, "100 Sesame Street")
+                    .insert();
+
+            rawContact.newDataRow(Phone.CONTENT_ITEM_TYPE)
+                    .with(Phone.DATA, "123456789")
+                    .with(Phone.TYPE, Phone.TYPE_MOBILE)
+                    .insert();
+
+            ContentValues cv2 = new ContentValues();
+            cv.put(Phone.DATA, "123456789");
+            cv.put(Phone.TYPE, Phone.TYPE_MOBILE);
+
+            // Contactables Uri should return only email and phone data items.
+            DatabaseAsserts.assertStoredValuesInUriMatchExactly(mResolver, contentUri, null,
+                    Data.RAW_CONTACT_ID + "=?", new String[] {String.valueOf(rawContact.getId())},
+                    null, cv, cv2);
+        } finally {
+            // Clean up
+            rawContact.delete();
+        }
+    }
+
+    public void testContactablesFilterByLastName_returnsCorrectDataRows() throws Exception {
+        long[] ids = setupContactablesTestData();
+        Uri filterUri = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "tamale");
+        assertCursorStoredValuesWithRawContactsFilter(filterUri, ids,
+                ContactablesTestHelper.getContentValues(0));
+    }
+
+    public void testContactablesFilterByFirstName_returnsCorrectDataRows() throws Exception {
+        long[] ids = setupContactablesTestData();
+        Uri filterUri = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "hot");
+        assertCursorStoredValuesWithRawContactsFilter(filterUri, ids,
+                ContactablesTestHelper.getContentValues(0));
+        Uri filterUri2 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "tam");
+        assertCursorStoredValuesWithRawContactsFilter(filterUri2, ids,
+                ContactablesTestHelper.getContentValues(0, 1));
+    }
+
+    public void testContactablesFilterByPhonePrefix_returnsCorrectDataRows() throws Exception {
+        long[] ids = setupContactablesTestData();
+        Uri filterUri = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "518");
+        assertCursorStoredValuesWithRawContactsFilter(filterUri, ids,
+                ContactablesTestHelper.getContentValues(2));
+        Uri filterUri2 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "51");
+        assertCursorStoredValuesWithRawContactsFilter(filterUri2, ids,
+                ContactablesTestHelper.getContentValues(0, 2));
+    }
+
+    public void testContactablesFilterByEmailPrefix_returnsCorrectDataRows() throws Exception {
+        long[] ids = setupContactablesTestData();
+        Uri filterUri = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "doeassoc");
+        assertCursorStoredValuesWithRawContactsFilter(filterUri, ids,
+                ContactablesTestHelper.getContentValues(2));
+    }
+
+    public void testContactablesFilter_doesNotExist_returnsCorrectDataRows() throws Exception {
+        long[] ids = setupContactablesTestData();
+        Uri filterUri = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "doesnotexist");
+        assertCursorStoredValuesWithRawContactsFilter(filterUri, ids, new ContentValues[0]);
+    }
+
     public void testDataInsert_updatesContactLastUpdatedTimestamp() {
         DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
         long baseTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
@@ -151,5 +240,155 @@
         values.put(CommonDataKinds.Phone.LABEL, "free directory assistance");
         return DataUtil.insertData(mResolver, rawContactId, values);
     }
+
+    private void assertCursorStoredValuesWithRawContactsFilter(Uri uri, long[] rawContactsId,
+            ContentValues... expected) {
+        // We need this helper function to add a filter for specific raw contacts because
+        // otherwise tests will fail if performed on a device with existing contacts data
+        StringBuilder sb = new StringBuilder();
+        sb.append(Data.RAW_CONTACT_ID + " in ");
+        sb.append("(");
+        for (int i = 0; i < rawContactsId.length; i++) {
+            if (i != 0) sb.append(",");
+            sb.append(rawContactsId[i]);
+        }
+        sb.append(")");
+        DatabaseAsserts.assertStoredValuesInUriMatchExactly(mResolver, uri, null, sb.toString(),
+                null, null, expected);
+    }
+
+
+    private long[] setupContactablesTestData() throws Exception {
+        TestRawContact rawContact = mBuilder.newRawContact()
+                .with(RawContacts.ACCOUNT_TYPE, "test_account")
+                .with(RawContacts.ACCOUNT_NAME, "test_name")
+                .insert();
+        rawContact.newDataRow(StructuredName.CONTENT_ITEM_TYPE)
+                .with(StructuredName.DISPLAY_NAME, "Hot Tamale")
+                .insert();
+        rawContact.newDataRow(Email.CONTENT_ITEM_TYPE)
+                .with(Email.DATA, "tamale@acme.com")
+                .with(Email.TYPE, Email.TYPE_HOME)
+                .insert();
+        rawContact.newDataRow(Email.CONTENT_ITEM_TYPE)
+                .with(Email.DATA, "hot@google.com")
+                .with(Email.TYPE, Email.TYPE_WORK)
+                .insert();
+        rawContact.newDataRow(Phone.CONTENT_ITEM_TYPE)
+                .with(Phone.DATA, "510-123-5769")
+                .with(Email.TYPE, Phone.TYPE_HOME)
+                .insert();
+
+        TestRawContact rawContact2 = mBuilder.newRawContact()
+                .with(RawContacts.ACCOUNT_TYPE, "test_account")
+                .with(RawContacts.ACCOUNT_NAME, "test_name")
+                .insert();
+        rawContact2.newDataRow(StructuredName.CONTENT_ITEM_TYPE)
+                .with(StructuredName.DISPLAY_NAME, "Cold Tamago")
+                .insert();
+        rawContact2.newDataRow(Email.CONTENT_ITEM_TYPE)
+                .with(Email.DATA, "eggs@farmers.org")
+                .with(Email.TYPE, Email.TYPE_HOME)
+                .insert();
+
+        TestRawContact rawContact3 = mBuilder.newRawContact()
+                .with(RawContacts.ACCOUNT_TYPE, "test_account")
+                .with(RawContacts.ACCOUNT_NAME, "test_name")
+                .insert();
+        rawContact3.newDataRow(StructuredName.CONTENT_ITEM_TYPE)
+                .with(StructuredName.DISPLAY_NAME, "John Doe")
+                .insert();
+        rawContact3.newDataRow(Email.CONTENT_ITEM_TYPE)
+                .with(Email.DATA, "doeassociates@deer.com")
+                .with(Email.TYPE, Email.TYPE_WORK)
+                .insert();
+        rawContact3.newDataRow(Phone.CONTENT_ITEM_TYPE)
+                .with(Phone.DATA, "518-354-1111")
+                .with(Phone.TYPE, Phone.TYPE_HOME)
+                .insert();
+        rawContact3.newDataRow(Organization.CONTENT_ITEM_TYPE)
+                .with(Organization.DATA, "Doe Industries")
+                .insert();
+        return new long[] {rawContact.getId(), rawContact2.getId(), rawContact3.getId()};
+    }
+
+    // Provides functionality to set up content values for the Contactables tests
+    private static class ContactablesTestHelper {
+        private static ContentValues[] sContentValues = new ContentValues[6];
+        static {
+            ContentValues cv1 = new ContentValues();
+            cv1.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+            cv1.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+            cv1.put(Email.DATA, "tamale@acme.com");
+            cv1.put(Email.TYPE, Email.TYPE_HOME);
+            sContentValues[0] = cv1;
+
+            ContentValues cv2 = new ContentValues();
+            cv2.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+            cv2.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+            cv2.put(Phone.DATA, "510-123-5769");
+            cv2.put(Phone.TYPE, Phone.TYPE_HOME);
+            sContentValues[1] = cv2;
+
+            ContentValues cv3 = new ContentValues();
+            cv3.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+            cv3.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+            cv3.put(Email.DATA, "hot@google.com");
+            cv3.put(Email.TYPE, Email.TYPE_WORK);
+            sContentValues[2] = cv3;
+
+            ContentValues cv4 = new ContentValues();
+            cv4.put(Contacts.DISPLAY_NAME, "Cold Tamago");
+            cv4.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+            cv4.put(Email.DATA, "eggs@farmers.org");
+            cv4.put(Email.TYPE, Email.TYPE_HOME);
+            sContentValues[3] = cv4;
+
+            ContentValues cv5 = new ContentValues();
+            cv5.put(Contacts.DISPLAY_NAME, "John Doe");
+            cv5.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+            cv5.put(Email.DATA, "doeassociates@deer.com");
+            cv5.put(Email.TYPE, Email.TYPE_WORK);
+            sContentValues[4] = cv5;
+
+            ContentValues cv6 = new ContentValues();
+            cv6.put(Contacts.DISPLAY_NAME, "John Doe");
+            cv6.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+            cv6.put(Phone.DATA, "518-354-1111");
+            cv6.put(Phone.TYPE, Phone.TYPE_HOME);
+            sContentValues[5] = cv6;
+        }
+
+        /**
+         * @return An arraylist of contentValues that correspond to the provided raw contacts
+         */
+        public static ContentValues[] getContentValues(int... rawContacts) {
+            ArrayList<ContentValues> cv = new ArrayList<ContentValues>();
+            for (int i = 0; i < rawContacts.length; i++) {
+                switch (rawContacts[i]) {
+                    case 0:
+                        // rawContact 0 "Hot Tamale" contains ContentValues 0, 1, and 2
+                        cv.add(sContentValues[0]);
+                        cv.add(sContentValues[1]);
+                        cv.add(sContentValues[2]);
+                        break;
+                    case 1:
+                        // rawContact 1 "Cold Tamago" contains ContentValues 3
+                        cv.add(sContentValues[3]);
+                        break;
+                    case 2:
+                        // rawContact 1 "John Doe" contains ContentValues 4, 5
+                        cv.add(sContentValues[4]);
+                        cv.add(sContentValues[5]);
+                        break;
+                }
+            }
+            ContentValues[] toReturn = new ContentValues[cv.size()];
+            for (int i = 0; i < cv.size(); i++) {
+                toReturn[i] = cv.get(i);
+            }
+            return toReturn;
+        }
+    }
 }
 
diff --git a/tests/tests/provider/src/android/provider/cts/contacts/DatabaseAsserts.java b/tests/tests/provider/src/android/provider/cts/contacts/DatabaseAsserts.java
index a163960..80a0a65 100644
--- a/tests/tests/provider/src/android/provider/cts/contacts/DatabaseAsserts.java
+++ b/tests/tests/provider/src/android/provider/cts/contacts/DatabaseAsserts.java
@@ -18,11 +18,14 @@
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.database.Cursor;
 import android.net.Uri;
 import android.test.MoreAsserts;
 
 import junit.framework.Assert;
 
+import java.util.BitSet;
+
 /**
  * Common methods for asserting database related operations.
  */
@@ -92,4 +95,107 @@
             this.mRawContactId = rawContactId;
         }
     }
+
+    /**
+     * Queries for a given {@link Uri} against a provided {@link ContentResolver}, and
+     * ensures that the returned cursor contains exactly the expected values.
+     *
+     * @param resolver - ContentResolver to query against
+     * @param uri - {@link Uri} to perform the query for
+     * contained in <code>expectedValues</code> in order for the assert to pass.
+     * @param expectedValues - Array of {@link ContentValues} which the cursor returned from the
+     * query should contain.
+     */
+    public static void assertStoredValuesInUriMatchExactly(ContentResolver resolver, Uri uri,
+            ContentValues... expectedValues) {
+        assertStoredValuesInUriMatchExactly(resolver, uri, null, null, null, null, expectedValues);
+    }
+
+    /**
+     * Queries for a given {@link Uri} against a provided {@link ContentResolver}, and
+     * ensures that the returned cursor contains exactly the expected values.
+     *
+     * @param resolver - ContentResolver to query against
+     * @param uri - {@link Uri} to perform the query for
+     * @param projection - Projection to use for the query. Must contain at least the columns
+     * contained in <code>expectedValues</code> in order for the assert to pass.
+     * @param selection - Selection string to use for the query.
+     * @param selectionArgs - Selection arguments to use for the query.
+     * @param sortOrder - Sort order to use for the query.
+     * @param expectedValues - Array of {@link ContentValues} which the cursor returned from the
+     * query should contain.
+     */
+    public static void assertStoredValuesInUriMatchExactly(ContentResolver resolver, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder,
+            ContentValues... expectedValues) {
+        final Cursor cursor = resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+        try {
+            assertCursorValuesMatchExactly(cursor, expectedValues);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Ensures that the rows in the cursor match the rows in the expected values exactly. However,
+     * does not require that the rows in the cursor are ordered the same way as those in the
+     * expected values.
+     *
+     * @param cursor - Cursor containing the values to check for
+     * @param expectedValues - Array of ContentValues that the cursor should be expected to
+     * contain.
+     */
+    public static void assertCursorValuesMatchExactly(Cursor cursor,
+            ContentValues... expectedValues) {
+        Assert.assertEquals("Cursor does not contain the number of expected rows",
+                expectedValues.length, cursor.getCount());
+        StringBuilder message = new StringBuilder();
+        // In case if expectedValues contains multiple identical values, remember which cursor
+        // rows are "consumed" to prevent multiple ContentValues from hitting the same row.
+        final BitSet used = new BitSet(cursor.getCount());
+
+        for (ContentValues v : expectedValues) {
+            boolean found = false;
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                final int pos = cursor.getPosition();
+                if (used.get(pos)) continue;
+                found = equalsWithExpectedValues(cursor, v, message);
+                if (found) {
+                    used.set(pos);
+                    break;
+                }
+            }
+            Assert.assertTrue("Expected values can not be found " + v + "," + message.toString(),
+                    found);
+        }
+    }
+
+    private static boolean equalsWithExpectedValues(Cursor cursor, ContentValues expectedValues,
+            StringBuilder msgBuffer) {
+        for (String column : expectedValues.keySet()) {
+            int index = cursor.getColumnIndex(column);
+            if (index == -1) {
+                msgBuffer.append(" No such column: ").append(column);
+                return false;
+            }
+            Object expectedValue = expectedValues.get(column);
+            String value;
+            expectedValue = expectedValues.getAsString(column);
+            value = cursor.getString(cursor.getColumnIndex(column));
+            if (expectedValue != null && !expectedValue.equals(value) || value != null
+                    && !value.equals(expectedValue)) {
+                msgBuffer
+                        .append(" Column value ")
+                        .append(column)
+                        .append(" expected <")
+                        .append(expectedValue)
+                        .append(">, but was <")
+                        .append(value)
+                        .append('>');
+                return false;
+            }
+        }
+        return true;
+    }
 }