Change default sort order for email queries.

- Prioritizing "default" emails over last used emails.
- Added ordering for same domain emails as last order by.

Bug: 7183241
Bug: 7346215
Change-Id: I6a8ba3cfe08792693eec4896f16853a88713bc3f
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 410aaf4..43afcb6 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -513,12 +513,12 @@
      */
     private static final String EMAIL_FILTER_SORT_ORDER =
         Contacts.STARRED + " DESC, "
+        + Data.IS_SUPER_PRIMARY + " DESC, "
+        + Data.IS_PRIMARY + " DESC, "
         + SORT_BY_DATA_USAGE + ", "
         + Contacts.IN_VISIBLE_GROUP + " DESC, "
         + Contacts.DISPLAY_NAME + ", "
-        + Data.CONTACT_ID + ", "
-        + Data.IS_SUPER_PRIMARY + " DESC, "
-        + Data.IS_PRIMARY + " DESC";
+        + Data.CONTACT_ID;
 
     /** Currently same as {@link #EMAIL_FILTER_SORT_ORDER} */
     private static final String PHONE_FILTER_SORT_ORDER = EMAIL_FILTER_SORT_ORDER;
@@ -5648,6 +5648,26 @@
                     } else {
                         sortOrder = EMAIL_FILTER_SORT_ORDER;
                     }
+
+                    final String primaryAccountName =
+                            uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME);
+                    if (!TextUtils.isEmpty(primaryAccountName)) {
+                        final int index = primaryAccountName.indexOf('@');
+                        if (index != -1) {
+                            // Purposely include '@' in matching.
+                            final String domain = primaryAccountName.substring(index);
+                            final char escapeChar = '\\';
+
+                            final StringBuilder likeValue = new StringBuilder();
+                            likeValue.append('%');
+                            DbQueryUtils.escapeLikeValue(likeValue, domain, escapeChar);
+                            selectionArgs = appendSelectionArg(selectionArgs, likeValue.toString());
+
+                            // similar email domains is the last sort preference.
+                            sortOrder += ", (CASE WHEN " + Data.DATA1 + " like ? ESCAPE '" +
+                                    escapeChar + "' THEN 0 ELSE 1 END)";
+                        }
+                    }
                 }
                 break;
             }
@@ -7860,6 +7880,18 @@
         }
     }
 
+    private String[] appendSelectionArg(String[] selectionArgs, String arg) {
+        if (selectionArgs == null) {
+            return new String[]{arg};
+        } else {
+            int newLength = selectionArgs.length + 1;
+            String[] newSelectionArgs = new String[newLength];
+            newSelectionArgs[newLength] = arg;
+            System.arraycopy(selectionArgs, 0, newSelectionArgs, 0, selectionArgs.length - 1);
+            return newSelectionArgs;
+        }
+    }
+
     protected Account getDefaultAccount() {
         AccountManager accountManager = AccountManager.get(getContext());
         try {
diff --git a/src/com/android/providers/contacts/util/DbQueryUtils.java b/src/com/android/providers/contacts/util/DbQueryUtils.java
index c853a96..c184613 100644
--- a/src/com/android/providers/contacts/util/DbQueryUtils.java
+++ b/src/com/android/providers/contacts/util/DbQueryUtils.java
@@ -94,4 +94,31 @@
             }
         }
     }
+
+    /**
+     * Escape values to be used in LIKE sqlite clause.
+     *
+     * The LIKE clause has two special characters: '%' and '_'.  If either of these
+     * characters need to be matched literally, then they must be escaped like so:
+     *
+     * WHERE value LIKE 'android\_%' ESCAPE '\'
+     *
+     * The ESCAPE clause is required and no default exists as the escape character in this context.
+     * Since the escape character needs to be defined as part of the sql string, it must be
+     * provided to this method so the escape characters match.
+     *
+     * @param sb The StringBuilder to append the escaped value to.
+     * @param value The value to be escaped.
+     * @param escapeChar The escape character to be defined in the sql ESCAPE clause.
+     */
+    public static void escapeLikeValue(StringBuilder sb, String value, char escapeChar) {
+        for (int i = 0; i < value.length(); i++) {
+            char ch = value.charAt(i);
+            if (ch == '%' || ch == '_') {
+                sb.append(escapeChar);
+            }
+            sb.append(ch);
+        }
+    }
+
 }
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 77789c3..1011ae2 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -1180,7 +1180,7 @@
         values3.putNull(Phone.LABEL);
 
         final Uri filterUri6 = Uri.withAppendedPath(baseFilterUri, "Chilled");
-        assertStoredValues(filterUri6, new ContentValues[] {values1, values2, values3} );
+        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.
@@ -1247,10 +1247,10 @@
                 );
         assertStoredValues(
                 Phone.CONTENT_FILTER_URI.buildUpon().appendPath("dad")
-                    .appendQueryParameter(Phone.SEARCH_DISPLAY_NAME_KEY, "0")
-                    .appendQueryParameter(Phone.SEARCH_PHONE_NUMBER_KEY, "0")
-                    .build()
-                );
+                        .appendQueryParameter(Phone.SEARCH_DISPLAY_NAME_KEY, "0")
+                        .appendQueryParameter(Phone.SEARCH_PHONE_NUMBER_KEY, "0")
+                        .build()
+        );
     }
 
     public void testPhoneLookup() {
@@ -1688,7 +1688,7 @@
         v3.put(Email.ADDRESS, "address3@email.com");
 
         Uri filterUri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "address");
-        assertStoredValuesOrderly(filterUri, new ContentValues[] { v1, v2, v3 });
+        assertStoredValuesOrderly(filterUri, new ContentValues[]{v1, v2, v3});
     }
 
     /**
@@ -1737,7 +1737,7 @@
         Uri filterUri3 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc")
                 .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, ACCOUNT_1.name)
                 .build();
-        assertStoredValuesOrderly(filterUri3, new ContentValues[] { v1, v2 });
+        assertStoredValuesOrderly(filterUri3, new ContentValues[]{v1, v2});
 
         Uri filterUri4 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc")
                 .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, ACCOUNT_2.name)
@@ -1745,6 +1745,48 @@
         assertStoredValuesOrderly(filterUri4, new ContentValues[] { v2, v1 });
     }
 
+    /**
+     * Test emails with the same domain as primary account are ordered first.
+     */
+    public void testEmailFilterSameDomainAccountOrder() {
+        final Account account = new Account("tester@email.com", "not_used");
+        final long rawContactId = createRawContact(account);
+        insertEmail(rawContactId, "account1@testemail.com");
+        insertEmail(rawContactId, "account1@email.com");
+
+        final ContentValues v1 = cv(Email.ADDRESS, "account1@testemail.com");
+        final ContentValues v2 = cv(Email.ADDRESS, "account1@email.com");
+
+        Uri filterUri1 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc")
+                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, account.name)
+                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE, account.type)
+                .build();
+        assertStoredValuesOrderly(filterUri1, v2, v1);
+    }
+
+    /**
+     * Test "default" emails are sorted above emails used last.
+     */
+    public void testEmailFilterDefaultOverUsageSort() {
+        final long rawContactId = createRawContact(ACCOUNT_1);
+        final Uri emailUri1 = insertEmail(rawContactId, "account1@testemail.com");
+        final Uri emailUri2 = insertEmail(rawContactId, "account2@testemail.com");
+        insertEmail(rawContactId, "account3@testemail.com", true);
+
+        // Update account1 and account 2 to have higher usage.
+        updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri1);
+        updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri1);
+        updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri2);
+
+        final ContentValues v1 = cv(Email.ADDRESS, "account1@testemail.com");
+        final ContentValues v2 = cv(Email.ADDRESS, "account2@testemail.com");
+        final ContentValues v3 = cv(Email.ADDRESS, "account3@testemail.com");
+
+        // Test that account 3 is first even though account 1 and 2 have higher usage.
+        Uri filterUri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "acc");
+        assertStoredValuesOrderly(filterUri, v3, v1, v2);
+    }
+
     /** Tests {@link DataUsageFeedback} correctly promotes a data row instead of a raw contact. */
     public void testEmailFilterSortOrderWithFeedback() {
         long rawContactId1 = createRawContact();
@@ -7207,6 +7249,12 @@
         }
     }
 
+    private void updateDataUsageFeedback(String usageType, Uri resultUri) {
+        final long id = ContentUris.parseId(resultUri);
+        final boolean successful = updateDataUsageFeedback(usageType, id) > 0;
+        assertTrue(successful);
+    }
+
     private int updateDataUsageFeedback(String usageType, long... ids) {
         final StringBuilder idList = new StringBuilder();
         for (long id : ids) {
diff --git a/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
index 7769b49..e09e59e 100644
--- a/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
+++ b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
@@ -18,14 +18,16 @@
 
 import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns;
 import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
+import static com.android.providers.contacts.util.DbQueryUtils.escapeLikeValue;
 
 import android.content.ContentValues;
-import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.common.content.ProjectionMap;
 import com.android.providers.contacts.EvenMoreAsserts;
 
+import junit.framework.TestCase;
+
 /**
  * Unit tests for the {@link DbQueryUtils} class.
  * Run the test like this:
@@ -34,7 +36,7 @@
  * </code>
  */
 @SmallTest
-public class DBQueryUtilsTest extends AndroidTestCase {
+public class DBQueryUtilsTest extends TestCase {
     public void testGetEqualityClause() {
         assertEquals("(foo = 'bar')", DbQueryUtils.getEqualityClause("foo", "bar"));
         assertEquals("(foo = 2)", DbQueryUtils.getEqualityClause("foo", 2));
@@ -71,4 +73,30 @@
             }
         });
     }
+
+    public void testEscapeLikeValuesEscapesUnderscores() {
+        StringBuilder sb = new StringBuilder();
+        DbQueryUtils.escapeLikeValue(sb, "my_test_string", '\\');
+        assertEquals("my\\_test\\_string", sb.toString());
+
+        sb = new StringBuilder();
+        DbQueryUtils.escapeLikeValue(sb, "_test_", '\\');
+        assertEquals("\\_test\\_", sb.toString());
+    }
+
+    public void testEscapeLikeValuesEscapesPercents() {
+        StringBuilder sb = new StringBuilder();
+        escapeLikeValue(sb, "my%test%string", '\\');
+        assertEquals("my\\%test\\%string", sb.toString());
+
+        sb = new StringBuilder();
+        escapeLikeValue(sb, "%test%", '\\');
+        assertEquals("\\%test\\%", sb.toString());
+    }
+
+    public void testEscapeLikeValuesNoChanges() {
+        StringBuilder sb = new StringBuilder();
+        escapeLikeValue(sb, "my test string", '\\');
+        assertEquals("my test string", sb.toString());
+    }
 }