Introduce data usage table for per-method ranking.

- have a hidden table for per-method promotion
- make filter API use it (phone, email only)
- add a unit test
- remove an old test using previous API

Must be after: I602c0b83afca674904946f59bbdfc4dca07d46e4

Bug: 4371572
Change-Id: I82052953d5dad42ac171df29248ed25e9b4a2434
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 5eb9243..473b30a 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -96,7 +96,7 @@
      *   600-699 Ice Cream Sandwich
      * </pre>
      */
-    static final int DATABASE_VERSION = 600;
+    static final int DATABASE_VERSION = 601;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
@@ -124,6 +124,12 @@
         public static final String DEFAULT_DIRECTORY = "default_directory";
         public static final String SEARCH_INDEX = "search_index";
 
+        /**
+         * For {@link ContactsContract.DataUsageFeedback}. The table structure itself
+         * is not exposed outside.
+         */
+        public static final String DATA_USAGE_STAT = "data_usage_stat";
+
         public static final String DATA_JOIN_MIMETYPES = "data "
                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
 
@@ -507,6 +513,43 @@
         public static final String TOKENS = "tokens";
     }
 
+    /**
+     * Private table for calculating per-contact-method ranking.
+     */
+    public static final class DataUsageStatColumns {
+        /** type: INTEGER (long) */
+        public static final String _ID = "stat_id";
+        public static final String CONCRETE_ID = Tables.DATA_USAGE_STAT + "." + _ID;
+
+        /** type: INTEGER (long) */
+        public static final String DATA_ID = "data_id";
+        public static final String CONCRETE_DATA_ID = Tables.DATA_USAGE_STAT + "." + DATA_ID;
+
+        /** type: INTEGER (long) */
+        public static final String LAST_TIME_USED = "last_time_used";
+        public static final String CONCRETE_LAST_TIME_USED =
+                Tables.DATA_USAGE_STAT + "." + LAST_TIME_USED;
+
+        /** type: INTEGER */
+        public static final String TIMES_USED = "times_used";
+        public static final String CONCRETE_TIMES_USED =
+                Tables.DATA_USAGE_STAT + "." + TIMES_USED;
+
+        /** type: INTEGER */
+        public static final String USAGE_TYPE_INT = "usage_type";
+        public static final String CONCRETE_USAGE_TYPE =
+                Tables.DATA_USAGE_STAT + "." + USAGE_TYPE_INT;
+
+        /**
+         * Integer values for USAGE_TYPE.
+         *
+         * @see ContactsContract.DataUsageFeedback#USAGE_TYPE
+         */
+        public static final int USAGE_TYPE_INT_CALL = 0;
+        public static final int USAGE_TYPE_INT_LONG_TEXT = 1;
+        public static final int USAGE_TYPE_INT_SHORT_TEXT = 2;
+    }
+
     /** In-memory cache of previously found MIME-type mappings */
     private final HashMap<String, Long> mMimetypeCache = new HashMap<String, Long>();
     /** In-memory cache of previously found package name mappings */
@@ -1075,6 +1118,21 @@
         createDirectoriesTable(db);
         createSearchIndexTable(db);
 
+        db.execSQL("CREATE TABLE " + Tables.DATA_USAGE_STAT + "(" +
+                DataUsageStatColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                DataUsageStatColumns.DATA_ID + " INTEGER NOT NULL, " +
+                DataUsageStatColumns.USAGE_TYPE_INT + " INTEGER NOT NULL DEFAULT 0, " +
+                DataUsageStatColumns.TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
+                DataUsageStatColumns.LAST_TIME_USED + " INTERGER NOT NULL DEFAULT 0, " +
+                "FOREIGN KEY(" + DataUsageStatColumns.DATA_ID + ") REFERENCES "
+                        + Tables.DATA + "(" + Data._ID + ")" +
+        ");");
+        db.execSQL("CREATE UNIQUE INDEX data_usage_stat_index ON " +
+                Tables.DATA_USAGE_STAT + " (" +
+                DataUsageStatColumns.DATA_ID + ", " +
+                DataUsageStatColumns.USAGE_TYPE_INT +
+        ");");
+
         createContactsViews(db);
         createGroupsView(db);
         createContactsTriggers(db);
@@ -1859,6 +1917,11 @@
             oldVersion = 600;
         }
 
+        if (oldVersion < 601) {
+            upgradeToVersion601(db);
+            oldVersion = 601;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
@@ -2911,6 +2974,18 @@
                 " (profile_raw_contact_id);");
     }
 
+    private void upgradeToVersion601(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE data_usage_stat(" +
+                "stat_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                "data_id INTEGER NOT NULL, " +
+                "usage_type INTEGER NOT NULL DEFAULT 0, " +
+                "times_used INTEGER NOT NULL DEFAULT 0, " +
+                "last_time_used INTERGER NOT NULL DEFAULT 0, " +
+                "FOREIGN KEY(data_id) REFERENCES data(_id));");
+        db.execSQL("CREATE UNIQUE INDEX data_usage_stat_index ON " +
+                "data_usage_stat (data_id, usage_type)");
+    }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 8073177..054f18a 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -34,6 +34,7 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
@@ -111,6 +112,7 @@
 import android.provider.ContactsContract.PhoneLookup;
 import android.provider.ContactsContract.ProviderStatus;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.DataUsageFeedback;
 import android.provider.ContactsContract.SearchSnippetColumns;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
@@ -131,6 +133,7 @@
 import java.io.Writer;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -287,6 +290,8 @@
     private static final int PROFILE_RAW_CONTACTS_ID_DATA = 19007;
     private static final int PROFILE_RAW_CONTACTS_ID_ENTITIES = 19008;
 
+    private static final int DATA_USAGE_FEEDBACK_ID = 20001;
+
     private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
             RawContactsColumns.CONCRETE_ID + "=? AND "
                     + GroupsColumns.CONCRETE_ACCOUNT_NAME
@@ -411,25 +416,27 @@
     // Recent contacts - those contacted within the last 30 days (in seconds)
     private static final long EMAIL_FILTER_RECENT = 30 * 24 * 60 * 60;
 
-    private static final String TIME_SINCE_LAST_CONTACTED =
-            "(strftime('%s', 'now') - " + Contacts.LAST_TIME_CONTACTED + "/1000)";
-
     /*
      * Sorting order for email address suggestions: first starred, then the rest.
      * Within the starred/unstarred groups - three buckets: very recently contacted, then fairly
      * recently contacted, then the rest.  Within each of the bucket - descending count
-     * of times contacted. If all else fails, alphabetical.  (Super)primary email
-     * address is returned before other addresses for the same contact.
+     * of times contacted (both for data row and for contact row). If all else fails, alphabetical.
+     * (Super)primary email address is returned before other addresses for the same contact.
      */
     private static final String EMAIL_FILTER_SORT_ORDER =
-            "(CASE WHEN " + Contacts.STARRED + "=1 THEN 0 ELSE 1 END), "
-            + "(CASE WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + EMAIL_FILTER_CURRENT + " THEN 0 "
-            + " WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + EMAIL_FILTER_RECENT + " THEN 1 "
-            + " ELSE 2 END),"
-            + Contacts.TIMES_CONTACTED + " DESC, "
-            + Contacts.DISPLAY_NAME + ", "
-            + Data.CONTACT_ID + ", "
-            + Data.IS_SUPER_PRIMARY + " DESC";
+        "(CASE WHEN " + Contacts.STARRED + "=1 THEN 0 ELSE 1 END), "
+        + "(CASE WHEN " + DataUsageStatColumns.LAST_TIME_USED + " < " + EMAIL_FILTER_CURRENT
+        + " THEN 0 "
+                + " WHEN " + DataUsageStatColumns.LAST_TIME_USED + " < " + EMAIL_FILTER_RECENT
+        + " THEN 1 "
+        + " ELSE 2 END), "
+        + DataUsageStatColumns.TIMES_USED + " DESC, "
+        + Contacts.DISPLAY_NAME + ", "
+        + Data.CONTACT_ID + ", "
+        + Data.IS_SUPER_PRIMARY + " DESC";
+
+    /** Currently same as {@link #EMAIL_FILTER_SORT_ORDER} */
+    private static final String PHONE_FILTER_SORT_ORDER = EMAIL_FILTER_SORT_ORDER;
 
     /** Name lookup types used for contact filtering */
     private static final String CONTACT_LOOKUP_NAME_TYPES =
@@ -866,6 +873,12 @@
 
     private Account mAccount;
 
+    /**
+     * Stores mapping from type Strings exposed via {@link DataUsageFeedback} to
+     * type integers in {@link DataUsageStatColumns}.
+     */
+    private static final Map<String, Integer> sDataUsageTypeMap;
+
     static {
         // Contacts URI matching table
         final UriMatcher matcher = sUriMatcher;
@@ -918,6 +931,8 @@
         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
         matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
         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, "groups", GROUPS);
         matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
@@ -973,6 +988,14 @@
                 PROFILE_RAW_CONTACTS_ID_DATA);
         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/entity",
                 PROFILE_RAW_CONTACTS_ID_ENTITIES);
+
+        HashMap<String, Integer> tmpTypeMap = new HashMap<String, Integer>();
+        tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_CALL, DataUsageStatColumns.USAGE_TYPE_INT_CALL);
+        tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_LONG_TEXT,
+                DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT);
+        tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_SHORT_TEXT,
+                DataUsageStatColumns.USAGE_TYPE_INT_SHORT_TEXT);
+        sDataUsageTypeMap = Collections.unmodifiableMap(tmpTypeMap);
     }
 
     private static class DirectoryInfo {
@@ -2754,6 +2777,15 @@
                 break;
             }
 
+            case DATA_USAGE_FEEDBACK_ID: {
+                if (handleDataUsageFeedback(uri)) {
+                    count = 1;
+                } else {
+                    count = 0;
+                }
+                break;
+            }
+
             default: {
                 mSyncToNetwork = true;
                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
@@ -3816,7 +3848,12 @@
             }
 
             case PHONES_FILTER: {
-                setTablesAndProjectionMapForData(qb, uri, projection, true);
+                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 " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
                 if (uri.getPathSegments().size() > 2) {
                     String filterParam = uri.getLastPathSegment();
@@ -3864,7 +3901,12 @@
                 }
                 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
                 if (sortOrder == null) {
-                    sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
+                    final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
+                    if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
+                        sortOrder = accountPromotionSortOrder + ", " + PHONE_FILTER_SORT_ORDER;
+                    } else {
+                        sortOrder = PHONE_FILTER_SORT_ORDER;
+                    }
                 }
                 break;
             }
@@ -3896,14 +3938,14 @@
             }
 
             case EMAILS_FILTER: {
-                setTablesAndProjectionMapForData(qb, uri, projection, true);
+                String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
+                Integer typeInt = sDataUsageTypeMap.get(typeParam);
+                if (typeInt == null) {
+                    typeInt = DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT;
+                }
+                setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt);
                 String filterParam = null;
 
-                String primaryAccountName =
-                        uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME);
-                String primaryAccountType =
-                        uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE);
-
                 if (uri.getPathSegments().size() > 3) {
                     filterParam = uri.getLastPathSegment();
                     if (TextUtils.isEmpty(filterParam)) {
@@ -3945,19 +3987,9 @@
                 }
                 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
                 if (sortOrder == null) {
-                    // Addresses associated with primary account should be promoted.
-                    if (!TextUtils.isEmpty(primaryAccountName)) {
-                        StringBuilder sb2 = new StringBuilder();
-                        sb2.append("(CASE WHEN " + RawContacts.ACCOUNT_NAME + "=");
-                        DatabaseUtils.appendEscapedSQLString(sb2, primaryAccountName);
-                        if (!TextUtils.isEmpty(primaryAccountType)) {
-                            sb2.append(" AND " + RawContacts.ACCOUNT_TYPE + "=");
-                            DatabaseUtils.appendEscapedSQLString(sb2, primaryAccountType);
-                        }
-                        sb2.append(" THEN 0 ELSE 1 END), ");
-                        sb2.append(EMAIL_FILTER_SORT_ORDER);
-
-                        sortOrder = sb2.toString();
+                    final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
+                    if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
+                        sortOrder = accountPromotionSortOrder + ", " + EMAIL_FILTER_SORT_ORDER;
                     } else {
                         sortOrder = EMAIL_FILTER_SORT_ORDER;
                     }
@@ -4931,6 +4963,15 @@
 
     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
             String[] projection, boolean distinct) {
+        setTablesAndProjectionMapForData(qb, uri, projection, distinct, null);
+    }
+
+    /**
+     * @param usageType when non-null {@link Tables#DATA_USAGE_STAT} is joined with the specified
+     * type.
+     */
+    private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
+            String[] projection, boolean distinct, Integer usageType) {
         StringBuilder sb = new StringBuilder();
         sb.append(mDbHelper.getDataView(shouldExcludeRestrictedData(uri)));
         sb.append(" data");
@@ -4940,6 +4981,10 @@
         appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
         appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
 
+        if (usageType != null) {
+            appendDataUsageStatJoin(sb, usageType, DataColumns.CONCRETE_ID);
+        }
+
         qb.setTables(sb.toString());
 
         boolean useDistinct = distinct
@@ -5006,6 +5051,12 @@
         }
     }
 
+    private void appendDataUsageStatJoin(StringBuilder sb, int usageType, String dataIdColumn) {
+        sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT +
+                " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" + dataIdColumn +
+                " AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" + usageType + ")");
+    }
+
     private void appendContactPresenceJoin(StringBuilder sb, String[] projection,
             String contactIdColumn) {
         if (mDbHelper.isInProjection(projection,
@@ -5769,4 +5820,126 @@
         }
         return sIsPhone;
     }
+
+    private boolean handleDataUsageFeedback(Uri uri) {
+        final long currentTimeMillis = System.currentTimeMillis();
+        final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
+        final String[] ids = uri.getLastPathSegment().trim().split(",");
+        final ArrayList<Long> dataIds = new ArrayList<Long>();
+
+        for (String id : ids) {
+            dataIds.add(Long.valueOf(id));
+        }
+        final boolean successful;
+        if (TextUtils.isEmpty(usageType)) {
+            Log.w(TAG, "Method for data usage feedback isn't specified. Ignoring.");
+            successful = false;
+        } else {
+            successful = updateDataUsageStat(dataIds, usageType, currentTimeMillis) > 0;
+        }
+
+        // Handle old API. This doesn't affect the result of this entire method.
+        final String[] questionMarks = new String[ids.length];
+        Arrays.fill(questionMarks, "?");
+        final String where = Data._ID + " IN (" + TextUtils.join(",", questionMarks) + ")";
+        final Cursor cursor = mDb.query(
+                mDbHelper.getDataView(shouldExcludeRestrictedData(uri)),
+                new String[] { Data.CONTACT_ID },
+                where, ids, null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                mSelectionArgs1[0] = cursor.getString(0);
+                ContentValues values2 = new ContentValues();
+                values2.put(Contacts.LAST_TIME_CONTACTED, currentTimeMillis);
+                mDb.update(Tables.CONTACTS, values2, Contacts._ID + "=?", mSelectionArgs1);
+                mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
+                mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return successful;
+    }
+
+    /**
+     * Update {@link Tables#DATA_USAGE_STAT}.
+     *
+     * @return the number of rows affected.
+     */
+    private int updateDataUsageStat(
+            ArrayList<Long> dataIds, String type, long currentTimeMillis) {
+        final int typeInt = sDataUsageTypeMap.get(type);
+        final String where = DataUsageStatColumns.DATA_ID + " =? AND "
+                + DataUsageStatColumns.USAGE_TYPE_INT + " =?";
+        final String[] columns =
+                new String[] { DataUsageStatColumns._ID, DataUsageStatColumns.TIMES_USED };
+        final ContentValues values = new ContentValues();
+        for (Long dataId : dataIds) {
+            final String[] args = new String[] { dataId.toString(), String.valueOf(typeInt) };
+            mDb.beginTransaction();
+            try {
+                final Cursor cursor = mDb.query(Tables.DATA_USAGE_STAT, columns, where, args,
+                        null, null, null);
+                try {
+                    if (cursor.getCount() > 0) {
+                        if (!cursor.moveToFirst()) {
+                            Log.e(TAG,
+                                    "moveToFirst() failed while getAccount() returned non-zero.");
+                        } else {
+                            values.clear();
+                            values.put(DataUsageStatColumns.TIMES_USED, cursor.getInt(1) + 1);
+                            values.put(DataUsageStatColumns.LAST_TIME_USED, currentTimeMillis);
+                            mDb.update(Tables.DATA_USAGE_STAT, values,
+                                    DataUsageStatColumns._ID + " =?",
+                                    new String[] { cursor.getString(0) });
+                        }
+                    } else {
+                        values.clear();
+                        values.put(DataUsageStatColumns.DATA_ID, dataId);
+                        values.put(DataUsageStatColumns.USAGE_TYPE_INT, typeInt);
+                        values.put(DataUsageStatColumns.TIMES_USED, 1);
+                        values.put(DataUsageStatColumns.LAST_TIME_USED, currentTimeMillis);
+                        mDb.insert(Tables.DATA_USAGE_STAT, null, values);
+                    }
+                    mDb.setTransactionSuccessful();
+                } finally {
+                    cursor.close();
+                }
+            } finally {
+                mDb.endTransaction();
+            }
+        }
+
+        return dataIds.size();
+    }
+
+    /**
+     * Returns a sort order String for promoting data rows (email addresses, phone numbers, etc.)
+     * associated with a primary account. The primary account should be supplied from applications
+     * with {@link ContactsContract#PRIMARY_ACCOUNT_NAME} and
+     * {@link ContactsContract#PRIMARY_ACCOUNT_TYPE}. Null will be returned when the primary
+     * account isn't available.
+     */
+    private String getAccountPromotionSortOrder(Uri uri) {
+        final String primaryAccountName =
+                uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME);
+        final String primaryAccountType =
+                uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE);
+
+        // Data rows associated with primary account should be promoted.
+        if (!TextUtils.isEmpty(primaryAccountName)) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("(CASE WHEN " + RawContacts.ACCOUNT_NAME + "=");
+            DatabaseUtils.appendEscapedSQLString(sb, primaryAccountName);
+            if (!TextUtils.isEmpty(primaryAccountType)) {
+                sb.append(" AND " + RawContacts.ACCOUNT_TYPE + "=");
+                DatabaseUtils.appendEscapedSQLString(sb, primaryAccountType);
+            }
+            sb.append(" THEN 0 ELSE 1 END)");
+            return sb.toString();
+        } else {
+            return null;
+        }
+    }
 }
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index a9f3988..ea76866 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -44,6 +44,7 @@
 import android.provider.ContactsContract.ContactCounts;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DataUsageFeedback;
 import android.provider.ContactsContract.Directory;
 import android.provider.ContactsContract.DisplayNameSources;
 import android.provider.ContactsContract.FullNameStyle;
@@ -1108,93 +1109,6 @@
         assertEquals(0, getCount(filterUri5, null, null));
     }
 
-    public void testEmailFilterSortOrder() {
-
-        // Adding contacts from the end to beginning of the expected order.
-
-        // Never contacted
-        insertContactWithEmail("never", false);
-        insertContactWithEmail("starred-never", true);
-
-        // Contacted a long time ago
-        insertContactWithEmail("a-longago", 10, 1800, false);
-        insertContactWithEmail("b-longago", 20, 1000, false);
-        insertContactWithEmail("c-longago", 30, 2000, false);
-
-        // Contacted fairly recently
-        insertContactWithEmail("a-recent", 10, 18, false);
-        insertContactWithEmail("b-recent", 20, 10, false);
-        insertContactWithEmail("c-recent", 30, 20, false);
-
-        // Contacted very recently
-        insertContactWithEmail("a-current", 10, 1, false);
-        insertContactWithEmail("b-current", 20, 0, false);
-        insertContactWithEmail("c-current", 30, 2, false);
-
-        // Starred
-        insertContactWithEmail("starred-longago", 10, 100, true);
-        insertContactWithEmail("starred-current", 10, 10, true);
-        insertContactWithEmail("starred-recent", 10, 1, true);
-
-        Uri filterUri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "findme");
-        Cursor cursor = mResolver.query(filterUri, new String[]{Contacts.DISPLAY_NAME},
-                null, null, null);
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "starred-recent");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "starred-current");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "starred-longago");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "starred-never");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "c-current");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "b-current");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "a-current");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "c-recent");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "b-recent");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "a-recent");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "c-longago");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "b-longago");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "a-longago");
-        cursor.moveToNext();
-        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "never");
-        cursor.close();
-    }
-
-    private void insertContactWithEmail(String name, boolean starred) {
-        long rawContactId = createRawContactWithName(name, null);
-        long contactId = queryContactId(rawContactId);
-        if (starred) {
-            storeValue(Contacts.CONTENT_URI, contactId, Contacts.STARRED, 1);
-        }
-        insertEmail(rawContactId, "findme" + name + "@acme.com");
-    }
-
-    private void insertContactWithEmail(
-            String name, int timesContacted, int lastTimeContactedDays, boolean starred) {
-        long rawContactId = createRawContactWithName(name, null);
-        long contactId = queryContactId(rawContactId);
-        ContentValues values = new ContentValues();
-        values.put(Contacts.TIMES_CONTACTED, timesContacted);
-        values.put(Contacts.LAST_TIME_CONTACTED,
-                System.currentTimeMillis() - (lastTimeContactedDays * 24 * 60 * 60 * 1000l));
-        if (starred) {
-            values.put(Contacts.STARRED, 1);
-        }
-        mResolver.update(
-                ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), values, null, null);
-        insertEmail(rawContactId, "findme" + name + "@acme.com");
-    }
-
     /**
      * Tests if ContactsProvider2 has email address associated with a primary account before the
      * other address.
@@ -1233,6 +1147,53 @@
         assertStoredValuesOrderly(filterUri4, new ContentValues[] { v2, v1 });
     }
 
+    /** Tests {@link DataUsageFeedback} correctly promotes a data row instead of a raw contact. */
+    public void testEmailFilterSortOrderWithFeedback() {
+        long rawContactId1 = createRawContact();
+        insertEmail(rawContactId1, "address1@email.com");
+        long rawContactId2 = createRawContact();
+        insertEmail(rawContactId2, "address2@email.com");
+        long dataId = ContentUris.parseId(insertEmail(rawContactId2, "address3@email.com"));
+
+        ContentValues v1 = new ContentValues();
+        v1.put(Email.ADDRESS, "address1@email.com");
+        ContentValues v2 = new ContentValues();
+        v2.put(Email.ADDRESS, "address2@email.com");
+        ContentValues v3 = new ContentValues();
+        v3.put(Email.ADDRESS, "address3@email.com");
+
+        Uri filterUri1 = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "address");
+        Uri filterUri2 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("address")
+                .appendQueryParameter(DataUsageFeedback.USAGE_TYPE,
+                        DataUsageFeedback.USAGE_TYPE_CALL)
+                .build();
+        Uri filterUri3 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("address")
+                .appendQueryParameter(DataUsageFeedback.USAGE_TYPE,
+                        DataUsageFeedback.USAGE_TYPE_LONG_TEXT)
+                .build();
+        Uri filterUri4 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("address")
+                .appendQueryParameter(DataUsageFeedback.USAGE_TYPE,
+                        DataUsageFeedback.USAGE_TYPE_SHORT_TEXT)
+                .build();
+        assertStoredValuesOrderly(filterUri1, new ContentValues[] { v1, v2, v3 });
+        assertStoredValuesOrderly(filterUri2, new ContentValues[] { v1, v2, v3 });
+        assertStoredValuesOrderly(filterUri3, new ContentValues[] { v1, v2, v3 });
+        assertStoredValuesOrderly(filterUri4, new ContentValues[] { v1, v2, v3 });
+
+        // Send feedback for address3 in the second account.
+        Uri feedbackUri = DataUsageFeedback.FEEDBACK_URI.buildUpon()
+                .appendPath(String.valueOf(dataId))
+                .appendQueryParameter(DataUsageFeedback.USAGE_TYPE,
+                        DataUsageFeedback.USAGE_TYPE_LONG_TEXT)
+                .build();
+        assertNotSame(0, mResolver.update(feedbackUri, new ContentValues(), null, null));
+
+        // account3@email.com should be the first. account2@email.com should also be promoted as
+        // it has same contact id.
+        assertStoredValuesOrderly(filterUri1, new ContentValues[] { v3, v1, v2 });
+        assertStoredValuesOrderly(filterUri3, new ContentValues[] { v3, v1, v2 });
+    }
+
     public void testPostalsQuery() {
         long rawContactId = createRawContactWithName("Alice", "Nextore");
         Uri dataUri = insertPostalAddress(rawContactId, "1600 Amphiteatre Ave, Mountain View");