Merge "Remove hasColumn() and use isInProjection() instead."
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 3b34b6f..46e4061 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -300,6 +300,9 @@
     private static final int EMAILS_FILTER = 3008;
     private static final int POSTALS = 3009;
     private static final int POSTALS_ID = 3010;
+    private static final int CALLABLES = 3011;
+    private static final int CALLABLES_ID = 3012;
+    private static final int CALLABLES_FILTER = 3013;
 
     private static final int PHONE_LOOKUP = 4000;
 
@@ -1156,6 +1159,10 @@
         matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
         /** "*" is in CSV form with data ids ("123,456,789") */
         matcher.addURI(ContactsContract.AUTHORITY, "data/usagefeedback/*", DATA_USAGE_FEEDBACK_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/callables/", CALLABLES);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/callables/#", CALLABLES_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter", CALLABLES_FILTER);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter/*", CALLABLES_FILTER);
 
         matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
         matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
@@ -3580,6 +3587,7 @@
             case DATA_ID:
             case PHONES_ID:
             case EMAILS_ID:
+            case CALLABLES_ID:
             case POSTALS_ID:
             case PROFILE_DATA_ID: {
                 long dataId = ContentUris.parseId(uri);
@@ -3929,6 +3937,7 @@
             case DATA_ID:
             case PHONES_ID:
             case EMAILS_ID:
+            case CALLABLES_ID:
             case POSTALS_ID: {
                 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
                 if (count > 0) {
@@ -5422,10 +5431,19 @@
                 return cursor;
             }
 
-            case PHONES: {
+            case PHONES:
+            case CALLABLES: {
+                final String mimeTypeIsPhoneExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
+                final String mimeTypeIsSipExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
-                qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + "=" +
-                        mDbHelper.get().getMimeTypeIdForPhone());
+                if (match == CALLABLES) {
+                    qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
+                            ") OR (" + mimeTypeIsSipExpression + "))");
+                } else {
+                    qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
+                }
 
                 final boolean removeDuplicates = readBooleanQueryParameter(
                         uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
@@ -5445,31 +5463,50 @@
                 break;
             }
 
-            case PHONES_ID: {
+            case PHONES_ID:
+            case CALLABLES_ID: {
+                final String mimeTypeIsPhoneExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
+                final String mimeTypeIsSipExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
-                qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
-                        + mDbHelper.get().getMimeTypeIdForPhone());
+                if (match == CALLABLES_ID) {
+                    qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
+                            ") OR (" + mimeTypeIsSipExpression + "))");
+                } else {
+                    qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
+                }
                 qb.appendWhere(" AND " + Data._ID + "=?");
                 break;
             }
 
-            case PHONES_FILTER: {
+            case PHONES_FILTER:
+            case CALLABLES_FILTER: {
+                final String mimeTypeIsPhoneExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
+                final String mimeTypeIsSipExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
+
                 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
                 Integer typeInt = sDataUsageTypeMap.get(typeParam);
                 if (typeInt == null) {
                     typeInt = DataUsageStatColumns.USAGE_TYPE_INT_CALL;
                 }
                 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt);
-                qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
-                        + mDbHelper.get().getMimeTypeIdForPhone());
+                if (match == CALLABLES_FILTER) {
+                    qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
+                            ") OR (" + mimeTypeIsSipExpression + "))");
+                } else {
+                    qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
+                }
+
                 if (uri.getPathSegments().size() > 2) {
                     String filterParam = uri.getLastPathSegment();
                     StringBuilder sb = new StringBuilder();
                     sb.append(" AND (");
 
                     boolean hasCondition = false;
-                    boolean orNeeded = false;
                     final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery(
                             filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING);
                     if (ftsMatchQuery.length() > 0) {
@@ -5482,13 +5519,12 @@
                                 " WHERE " + SearchIndexColumns.NAME + " MATCH '");
                         sb.append(ftsMatchQuery);
                         sb.append("')");
-                        orNeeded = true;
                         hasCondition = true;
                     }
 
                     String number = PhoneNumberUtils.normalizeNumber(filterParam);
                     if (!TextUtils.isEmpty(number)) {
-                        if (orNeeded) {
+                        if (hasCondition) {
                             sb.append(" OR ");
                         }
                         sb.append(Data._ID +
@@ -5500,6 +5536,23 @@
                         hasCondition = true;
                     }
 
+                    if (!TextUtils.isEmpty(filterParam) && match == CALLABLES_FILTER) {
+                        // If the request is via Callable uri, Sip addresses matching the filter
+                        // parameter should be returned.
+                        if (hasCondition) {
+                            sb.append(" OR ");
+                        }
+                        sb.append("(");
+                        sb.append(mimeTypeIsSipExpression);
+                        sb.append(" AND ((" + Data.DATA1 + " LIKE ");
+                        DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
+                        sb.append(") OR (" + Data.DATA1 + " LIKE ");
+                        // Users may want SIP URIs starting from "sip:"
+                        DatabaseUtils.appendEscapedSQLString(sb, "sip:"+ filterParam + '%');
+                        sb.append(")))");
+                        hasCondition = true;
+                    }
+
                     if (!hasCondition) {
                         // If it is neither a phone number nor a name, the query should return
                         // an empty cursor.  Let's ensure that.
@@ -5508,9 +5561,21 @@
                     sb.append(")");
                     qb.appendWhere(sb);
                 }
-                groupBy = "(CASE WHEN " + PhoneColumns.NORMALIZED_NUMBER
+                if (match == CALLABLES_FILTER) {
+                    // If the row is for a phone number that has a normalized form, we should use
+                    // the normalized one as PHONES_FILTER does, while we shouldn't do that
+                    // if the row is for a sip address.
+                    String isPhoneAndHasNormalized = "("
+                        + mimeTypeIsPhoneExpression + " AND "
+                        + PhoneColumns.NORMALIZED_NUMBER + " IS NOT NULL)";
+                    groupBy = "(CASE WHEN " + isPhoneAndHasNormalized
+                        + " THEN " + PhoneColumns.NORMALIZED_NUMBER
+                        + " ELSE " + Data.DATA1 + " END), " + RawContacts.CONTACT_ID;
+                } else {
+                    groupBy = "(CASE WHEN " + PhoneColumns.NORMALIZED_NUMBER
                         + " IS NOT NULL THEN " + PhoneColumns.NORMALIZED_NUMBER
                         + " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID;
+                }
                 if (sortOrder == null) {
                     final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
                     if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 6080f7e..87196bf 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -41,6 +41,7 @@
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Contacts;
@@ -336,6 +337,23 @@
         return resultUri;
     }
 
+    protected Uri insertSipAddress(long rawContactId, String sipAddress) {
+        return insertSipAddress(rawContactId, sipAddress, false);
+    }
+
+    protected Uri insertSipAddress(long rawContactId, String sipAddress, boolean primary) {
+        ContentValues values = new ContentValues();
+        values.put(Data.RAW_CONTACT_ID, rawContactId);
+        values.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+        values.put(SipAddress.SIP_ADDRESS, sipAddress);
+        if (primary) {
+            values.put(Data.IS_PRIMARY, 1);
+        }
+
+        Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
     protected Uri insertNickname(long rawContactId, String nickname) {
         ContentValues values = new ContentValues();
         values.put(Data.RAW_CONTACT_ID, rawContactId);
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index eae168f..599f1e7 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -36,12 +36,14 @@
 import android.os.AsyncTask;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.ContactCounts;
@@ -917,15 +919,33 @@
     }
 
     public void testPhonesFilterQuery() {
-        long rawContactId1 = createRawContactWithName("Hot", "Tamale", ACCOUNT_1);
+        testPhonesFilterQueryInter(Phone.CONTENT_FILTER_URI);
+    }
+
+    /**
+     * A convenient method for {@link #testPhonesFilterQuery()} and
+     * {@link #testCallablesFilterQuery()}.
+     *
+     * This confirms if both URIs return identical results for phone-only contacts and
+     * appropriately different results for contacts with sip addresses.
+     *
+     * @param baseFilterUri Either {@link Phone#CONTENT_FILTER_URI} or
+     * {@link Callable#CONTENT_FILTER_URI}.
+     */
+    private void testPhonesFilterQueryInter(Uri baseFilterUri) {
+        assertTrue("Unsupported Uri (" + baseFilterUri + ")",
+                Phone.CONTENT_FILTER_URI.equals(baseFilterUri)
+                        || Callable.CONTENT_FILTER_URI.equals(baseFilterUri));
+
+        final long rawContactId1 = createRawContactWithName("Hot", "Tamale", ACCOUNT_1);
         insertPhoneNumber(rawContactId1, "1-800-466-4411");
 
-        long rawContactId2 = createRawContactWithName("Chilled", "Guacamole", ACCOUNT_2);
+        final long rawContactId2 = createRawContactWithName("Chilled", "Guacamole", ACCOUNT_2);
         insertPhoneNumber(rawContactId2, "1-800-466-5432");
         insertPhoneNumber(rawContactId2, "0@example.com", false, Phone.TYPE_PAGER);
         insertPhoneNumber(rawContactId2, "1@example.com", false, Phone.TYPE_PAGER);
 
-        Uri filterUri1 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "tamale");
+        final Uri filterUri1 = Uri.withAppendedPath(baseFilterUri, "tamale");
         ContentValues values = new ContentValues();
         values.put(Contacts.DISPLAY_NAME, "Hot Tamale");
         values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
@@ -934,16 +954,16 @@
         values.putNull(Phone.LABEL);
         assertStoredValuesWithProjection(filterUri1, values);
 
-        Uri filterUri2 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "1-800-GOOG-411");
+        final Uri filterUri2 = Uri.withAppendedPath(baseFilterUri, "1-800-GOOG-411");
         assertStoredValues(filterUri2, values);
 
-        Uri filterUri3 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "18004664");
+        final Uri filterUri3 = Uri.withAppendedPath(baseFilterUri, "18004664");
         assertStoredValues(filterUri3, values);
 
-        Uri filterUri4 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "encilada");
+        final Uri filterUri4 = Uri.withAppendedPath(baseFilterUri, "encilada");
         assertEquals(0, getCount(filterUri4, null, null));
 
-        Uri filterUri5 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "*");
+        final Uri filterUri5 = Uri.withAppendedPath(baseFilterUri, "*");
         assertEquals(0, getCount(filterUri5, null, null));
 
         ContentValues values1 = new ContentValues();
@@ -967,7 +987,42 @@
         values3.put(Phone.TYPE, Phone.TYPE_PAGER);
         values3.putNull(Phone.LABEL);
 
-        Uri filterUri6 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "Chilled");
+        final Uri filterUri6 = Uri.withAppendedPath(baseFilterUri, "Chilled");
+        assertStoredValues(filterUri6, new ContentValues[] {values1, values2, values3} );
+
+        // Insert a SIP address. From here, Phone URI and Callable URI may return different results
+        // than each other.
+        insertSipAddress(rawContactId1, "sip_hot_tamale@example.com");
+        insertSipAddress(rawContactId1, "sip:sip_hot@example.com");
+
+        final Uri filterUri7 = Uri.withAppendedPath(baseFilterUri, "sip_hot");
+        final Uri filterUri8 = Uri.withAppendedPath(baseFilterUri, "sip_hot_tamale");
+        if (Callable.CONTENT_FILTER_URI.equals(baseFilterUri)) {
+            ContentValues values4 = new ContentValues();
+            values4.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+            values4.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+            values4.put(SipAddress.SIP_ADDRESS, "sip_hot_tamale@example.com");
+
+            ContentValues values5 = new ContentValues();
+            values5.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+            values5.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+            values5.put(SipAddress.SIP_ADDRESS, "sip:sip_hot@example.com");
+            assertStoredValues(filterUri1, new ContentValues[] {values, values4, values5});
+
+            assertStoredValues(filterUri7, new ContentValues[] {values4, values5});
+            assertStoredValues(filterUri8, values4);
+        } else {
+            // Sip address should not affect Phone URI.
+            assertStoredValuesWithProjection(filterUri1, values);
+            assertEquals(0, getCount(filterUri7, null, null));
+        }
+
+        // Sanity test. Run tests for "Chilled Guacamole" again and see nothing changes
+        // after the Sip address being inserted.
+        assertStoredValues(filterUri2, values);
+        assertStoredValues(filterUri3, values);
+        assertEquals(0, getCount(filterUri4, null, null));
+        assertEquals(0, getCount(filterUri5, null, null));
         assertStoredValues(filterUri6, new ContentValues[] {values1, values2, values3} );
     }
 
@@ -1100,6 +1155,43 @@
         assertNetworkNotified(true);
     }
 
+    /** Tests if {@link Callable#CONTENT_URI} returns both phones and sip addresses. */
+    public void testCallablesQuery() {
+        long rawContactId1 = createRawContactWithName("Meghan", "Knox");
+        long phoneId1 = ContentUris.parseId(insertPhoneNumber(rawContactId1, "18004664411"));
+        long contactId1 = queryContactId(rawContactId1);
+
+        long rawContactId2 = createRawContactWithName("John", "Doe");
+        long sipAddressId2 = ContentUris.parseId(
+                insertSipAddress(rawContactId2, "sip@example.com"));
+        long contactId2 = queryContactId(rawContactId2);
+
+        ContentValues values1 = new ContentValues();
+        values1.put(Data._ID, phoneId1);
+        values1.put(Data.RAW_CONTACT_ID, rawContactId1);
+        values1.put(RawContacts.CONTACT_ID, contactId1);
+        values1.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        values1.put(Phone.NUMBER, "18004664411");
+        values1.put(Phone.TYPE, Phone.TYPE_HOME);
+        values1.putNull(Phone.LABEL);
+        values1.put(Contacts.DISPLAY_NAME, "Meghan Knox");
+
+        ContentValues values2 = new ContentValues();
+        values2.put(Data._ID, sipAddressId2);
+        values2.put(Data.RAW_CONTACT_ID, rawContactId2);
+        values2.put(RawContacts.CONTACT_ID, contactId2);
+        values2.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+        values2.put(SipAddress.SIP_ADDRESS, "sip@example.com");
+        values2.put(Contacts.DISPLAY_NAME, "John Doe");
+
+        assertEquals(2, getCount(Callable.CONTENT_URI, null, null));
+        assertStoredValues(Callable.CONTENT_URI, new ContentValues[] { values1, values2 });
+    }
+
+    public void testCallablesFilterQuery() {
+        testPhonesFilterQueryInter(Callable.CONTENT_FILTER_URI);
+    }
+
     public void testEmailsQuery() {
         ContentValues values = new ContentValues();
         values.put(RawContacts.CUSTOM_RINGTONE, "d");