am 8d54a6e0: resolved conflicts for merge of b8fb609b to jb-mr1-dev

* commit '8d54a6e0fa9fb4bb0a2b3b2f36d0e9bf930d05c8':
  Do not allow updates to the _data column.
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 5baf9dc..ad602b8 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -107,7 +107,7 @@
      *   700-799 Jelly Bean
      * </pre>
      */
-    static final int DATABASE_VERSION = 705;
+    static final int DATABASE_VERSION = 706;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
@@ -1276,7 +1276,7 @@
         ");");
 
         createDirectoriesTable(db);
-        createSearchIndexTable(db);
+        createSearchIndexTable(db, false /* we build stats table later */);
 
         db.execSQL("CREATE TABLE " + Tables.DATA_USAGE_STAT + "(" +
                 DataUsageStatColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
@@ -1297,7 +1297,7 @@
         createContactsViews(db);
         createGroupsView(db);
         createContactsTriggers(db);
-        createContactsIndexes(db);
+        createContactsIndexes(db, false /* we build stats table later */);
 
         loadNicknameLookupTable(db);
 
@@ -1344,7 +1344,7 @@
         setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
     }
 
-    public void createSearchIndexTable(SQLiteDatabase db) {
+    public void createSearchIndexTable(SQLiteDatabase db, boolean rebuildSqliteStats) {
         db.execSQL("DROP TABLE IF EXISTS " + Tables.SEARCH_INDEX);
         db.execSQL("CREATE VIRTUAL TABLE " + Tables.SEARCH_INDEX
                 + " USING FTS4 ("
@@ -1353,6 +1353,9 @@
                     + SearchIndexColumns.NAME + " TEXT, "
                     + SearchIndexColumns.TOKENS + " TEXT"
                 + ")");
+        if (rebuildSqliteStats) {
+            updateSqliteStats(db);
+        }
     }
 
     private void createContactsTriggers(SQLiteDatabase db) {
@@ -1492,7 +1495,7 @@
                 + " END");
     }
 
-    private void createContactsIndexes(SQLiteDatabase db) {
+    private void createContactsIndexes(SQLiteDatabase db, boolean rebuildSqliteStats) {
         db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
         db.execSQL("CREATE INDEX name_lookup_index ON " + Tables.NAME_LOOKUP + " (" +
                 NameLookupColumns.NORMALIZED_NAME + "," +
@@ -1510,6 +1513,10 @@
         db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
                 RawContacts.SORT_KEY_ALTERNATIVE +
         ");");
+
+        if (rebuildSqliteStats) {
+            updateSqliteStats(db);
+        }
     }
 
     private void createContactsViews(SQLiteDatabase db) {
@@ -1940,6 +1947,7 @@
         boolean upgradeLegacyApiSupport = false;
         boolean upgradeSearchIndex = false;
         boolean rescanDirectories = false;
+        boolean rebuildSqliteStats = false;
 
         if (oldVersion == 99) {
             upgradeViewsAndTriggers = true;
@@ -2383,13 +2391,20 @@
             oldVersion = 705;
         }
 
+        if (oldVersion < 706) {
+            // Prior to this version, we didn't rebuild the stats table after drop operations,
+            // which resulted in losing some of the rows from the stats table.
+            rebuildSqliteStats = true;
+            oldVersion = 706;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
             createContactsTriggers(db);
-            createContactsIndexes(db);
-            updateSqliteStats(db);
+            createContactsIndexes(db, false /* we build stats table later */);
             upgradeLegacyApiSupport = true;
+            rebuildSqliteStats = true;
         }
 
         if (upgradeLegacyApiSupport) {
@@ -2397,12 +2412,14 @@
         }
 
         if (upgradeNameLookup) {
-            rebuildNameLookup(db);
+            rebuildNameLookup(db, false /* we build stats table later */);
+            rebuildSqliteStats = true;
         }
 
         if (upgradeSearchIndex) {
-            createSearchIndexTable(db);
+            createSearchIndexTable(db, false /* we build stats table later */);
             setProperty(db, SearchIndexManager.PROPERTY_SEARCH_INDEX_VERSION, "0");
+            rebuildSqliteStats = true;
         }
 
         if (rescanDirectories) {
@@ -2411,6 +2428,10 @@
             setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
         }
 
+        if (rebuildSqliteStats) {
+            updateSqliteStats(db);
+        }
+
         if (oldVersion != newVersion) {
             throw new IllegalStateException(
                     "error upgrading the database to version " + newVersion);
@@ -2970,10 +2991,10 @@
                 "WHERE NOT EXISTS (SELECT 1 FROM raw_contacts WHERE contact_id=contacts._id)");
     }
 
-    private void rebuildNameLookup(SQLiteDatabase db) {
+    private void rebuildNameLookup(SQLiteDatabase db, boolean rebuildSqliteStats) {
         db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
         insertNameLookup(db);
-        createContactsIndexes(db);
+        createContactsIndexes(db, rebuildSqliteStats);
     }
 
     /**
@@ -2994,7 +3015,7 @@
             loadNicknameLookupTable(db);
             insertNameLookup(db);
             rebuildSortKeys(db, provider);
-            createContactsIndexes(db);
+            createContactsIndexes(db, true);
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
@@ -3844,16 +3865,51 @@
 
     /**
      * Adds index stats into the SQLite database to force it to always use the lookup indexes.
+     *
+     * Note if you drop a table or an index, the corresponding row will be removed from this table.
+     * Make sure to call this method after such operations.
      */
     private void updateSqliteStats(SQLiteDatabase db) {
+        if (!mDatabaseOptimizationEnabled) {
+            return; // We don't use sqlite_stat1 during tests.
+        }
 
         // Specific stats strings are based on an actual large database after running ANALYZE
         // Important here are relative sizes. Raw-Contacts is slightly bigger than Contacts
         // Warning: Missing tables in here will make SQLite assume to contain 1000000 rows,
         // which can lead to catastrophic query plans for small tables
 
-        // See the latest of version of http://www.sqlite.org/cgi/src/finfo?name=src/analyze.c
-        // for what these numbers mean.
+        // What these numbers mean is described in this file.
+        // http://www.sqlite.org/cgi/src/finfo?name=src/analyze.c
+
+        // Excerpt:
+        /*
+        ** Format of sqlite_stat1:
+        **
+        ** There is normally one row per index, with the index identified by the
+        ** name in the idx column.  The tbl column is the name of the table to
+        ** which the index belongs.  In each such row, the stat column will be
+        ** a string consisting of a list of integers.  The first integer in this
+        ** list is the number of rows in the index and in the table.  The second
+        ** integer is the average number of rows in the index that have the same
+        ** value in the first column of the index.  The third integer is the average
+        ** number of rows in the index that have the same value for the first two
+        ** columns.  The N-th integer (for N>1) is the average number of rows in
+        ** the index which have the same value for the first N-1 columns.  For
+        ** a K-column index, there will be K+1 integers in the stat column.  If
+        ** the index is unique, then the last integer will be 1.
+        **
+        ** The list of integers in the stat column can optionally be followed
+        ** by the keyword "unordered".  The "unordered" keyword, if it is present,
+        ** must be separated from the last integer by a single space.  If the
+        ** "unordered" keyword is present, then the query planner assumes that
+        ** the index is unordered and will not use the index for a range query.
+        **
+        ** If the sqlite_stat1.idx column is NULL, then the sqlite_stat1.stat
+        ** column contains a single integer which is the (estimated) number of
+        ** rows in the table identified by sqlite_stat1.tbl.
+        */
+
         try {
             db.execSQL("DELETE FROM sqlite_stat1");
             updateIndexStats(db, Tables.CONTACTS,
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/SearchIndexManager.java b/src/com/android/providers/contacts/SearchIndexManager.java
index 20fd16b..d45009e 100644
--- a/src/com/android/providers/contacts/SearchIndexManager.java
+++ b/src/com/android/providers/contacts/SearchIndexManager.java
@@ -271,7 +271,7 @@
         final long start = SystemClock.elapsedRealtime();
         int count = 0;
         try {
-            mDbHelper.createSearchIndexTable(db);
+            mDbHelper.createSearchIndexTable(db, true);
             count = buildAndInsertIndex(db, null);
         } finally {
             mContactsProvider.setProviderStatus(ProviderStatus.STATUS_NORMAL);
diff --git a/src/com/android/providers/contacts/util/DbQueryUtils.java b/src/com/android/providers/contacts/util/DbQueryUtils.java
index 2b976a1..d719313 100644
--- a/src/com/android/providers/contacts/util/DbQueryUtils.java
+++ b/src/com/android/providers/contacts/util/DbQueryUtils.java
@@ -105,4 +105,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());
+    }
 }