Merge "Don't expose raw usage stats to clients; return "low-res" values."
diff --git a/run-all-tests.sh b/run-all-tests.sh
index 4a64d35..84a2ce3 100755
--- a/run-all-tests.sh
+++ b/run-all-tests.sh
@@ -23,6 +23,7 @@
 
 mmm -j32 packages/providers/ContactsProvider
 adb install -t -r -g $ANDROID_PRODUCT_OUT/system/priv-app/ContactsProvider/ContactsProvider.apk
+adb install -t -r -g $ANDROID_PRODUCT_OUT/data/app/ContactsProviderTests/ContactsProviderTests.apk
 adb install -t -r -g $ANDROID_PRODUCT_OUT/data/app/ContactsProviderTests2/ContactsProviderTests2.apk
 
 runtest() {
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index b001653..8d0d06a 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -136,7 +136,7 @@
      *   1200-1299 O
      * </pre>
      */
-    static final int DATABASE_VERSION = 1200;
+    static final int DATABASE_VERSION = 1201;
     private static final int MINIMUM_SUPPORTED_VERSION = 700;
 
     public interface Tables {
@@ -325,7 +325,12 @@
         public static final String ENTITIES = "view_entities";
         public static final String RAW_ENTITIES = "view_raw_entities";
         public static final String GROUPS = "view_groups";
+
+        /** The data_usage_stat table joined with other tables. */
         public static final String DATA_USAGE_STAT = "view_data_usage_stat";
+
+        /** The data_usage_stat table with the low-res columns. */
+        public static final String DATA_USAGE_LR = "view_data_usage";
         public static final String STREAM_ITEMS = "view_stream_items";
         public static final String METADATA_SYNC = "view_metadata_sync";
         public static final String METADATA_SYNC_STATE = "view_metadata_sync_state";
@@ -408,10 +413,12 @@
 
         public static final String CONCRETE_PHOTO_FILE_ID = Tables.CONTACTS + "."
                 + Contacts.PHOTO_FILE_ID;
-        public static final String CONCRETE_TIMES_CONTACTED = Tables.CONTACTS + "."
-                + Contacts.TIMES_CONTACTED;
-        public static final String CONCRETE_LAST_TIME_CONTACTED = Tables.CONTACTS + "."
-                + Contacts.LAST_TIME_CONTACTED;
+
+        public static final String CONCRETE_RAW_TIMES_CONTACTED = Tables.CONTACTS + "."
+                + Contacts.RAW_TIMES_CONTACTED;
+        public static final String CONCRETE_RAW_LAST_TIME_CONTACTED = Tables.CONTACTS + "."
+                + Contacts.RAW_LAST_TIME_CONTACTED;
+
         public static final String CONCRETE_STARRED = Tables.CONTACTS + "." + Contacts.STARRED;
         public static final String CONCRETE_PINNED = Tables.CONTACTS + "." + Contacts.PINNED;
         public static final String CONCRETE_CUSTOM_RINGTONE = Tables.CONTACTS + "."
@@ -456,10 +463,10 @@
                 Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE;
         public static final String CONCRETE_SEND_TO_VOICEMAIL =
                 Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL;
-        public static final String CONCRETE_LAST_TIME_CONTACTED =
-                Tables.RAW_CONTACTS + "." + RawContacts.LAST_TIME_CONTACTED;
-        public static final String CONCRETE_TIMES_CONTACTED =
-                Tables.RAW_CONTACTS + "." + RawContacts.TIMES_CONTACTED;
+        public static final String CONCRETE_RAW_LAST_TIME_CONTACTED =
+                Tables.RAW_CONTACTS + "." + RawContacts.RAW_LAST_TIME_CONTACTED;
+        public static final String CONCRETE_RAW_TIMES_CONTACTED =
+                Tables.RAW_CONTACTS + "." + RawContacts.RAW_TIMES_CONTACTED;
         public static final String CONCRETE_STARRED =
                 Tables.RAW_CONTACTS + "." + RawContacts.STARRED;
         public static final String CONCRETE_PINNED =
@@ -726,14 +733,24 @@
         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;
+        public static final String RAW_LAST_TIME_USED = Data.RAW_LAST_TIME_USED;
+        public static final String LR_LAST_TIME_USED = Data.LR_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;
+        public static final String RAW_TIMES_USED = Data.RAW_TIMES_USED;
+        public static final String LR_TIMES_USED = Data.LR_TIMES_USED;
+
+        public static final String CONCRETE_RAW_LAST_TIME_USED =
+                Tables.DATA_USAGE_STAT + "." + RAW_LAST_TIME_USED;
+
+        public static final String CONCRETE_RAW_TIMES_USED =
+                Tables.DATA_USAGE_STAT + "." + RAW_TIMES_USED;
+
+        public static final String CONCRETE_LR_LAST_TIME_USED =
+                Tables.DATA_USAGE_STAT + "." + LR_LAST_TIME_USED;
+
+        public static final String CONCRETE_LR_TIMES_USED =
+                Tables.DATA_USAGE_STAT + "." + LR_TIMES_USED;
 
         /** type: INTEGER */
         public static final String USAGE_TYPE_INT = "usage_type";
@@ -903,6 +920,60 @@
         }
     }
 
+    /** Placeholder for the methods to build the "low-res" SQL expressions. */
+    @VisibleForTesting
+    interface LowRes {
+        /** To be replaced with a real column name.  Only used within this interface. */
+        String TEMPLATE_PLACEHOLDER = "XX";
+
+        /**
+         * To be replaced with a constant in the expression.
+         * Only used within this interface.
+         */
+        String CONSTANT_PLACEHOLDER = "YY";
+
+        /** Only used within this interface. */
+        int TIMES_USED_GRANULARITY = 10;
+
+        /** Only used within this interface. */
+        int LAST_TIME_USED_GRANULARITY = 24 * 60 * 60;
+
+        /**
+         * Template to build the "low-res times used/contacted".  Only used within this interface.
+         * The outermost cast is needed to tell SQLite that the result is of the integer type.
+         */
+        String TEMPLATE_TIMES_USED =
+                ("cast(ifnull((case when (XX) <= 0 then 0"
+                + " when (XX) < (YY) then 1"
+                + " else (cast((XX) as int) / (YY)) * (YY) end), 0) as int)")
+                .replaceAll(CONSTANT_PLACEHOLDER, String.valueOf(TIMES_USED_GRANULARITY));
+
+        /**
+         * Template to build the "low-res last time used/contacted".
+         * Only used within this interface.
+         * The outermost cast is needed to tell SQLite that the result is of the integer type.
+         */
+        String TEMPLATE_LAST_TIME_USED =
+                ("cast((cast((XX) as int) / (YY)) * (YY) as int)")
+                .replaceAll(CONSTANT_PLACEHOLDER, String.valueOf(LAST_TIME_USED_GRANULARITY));
+
+        /**
+         * Build the SQL expression for the "low-res times used/contacted" expression from the
+         * give column name.
+         */
+        static String getTimesUsedExpression(String column) {
+            return TEMPLATE_TIMES_USED.replaceAll(TEMPLATE_PLACEHOLDER, column);
+        }
+
+        /**
+         * Build the SQL expression for the "low-res last time used/contacted" expression from the
+         * give column name.
+         */
+        static String getLastTimeUsedExpression(String column) {
+            return TEMPLATE_LAST_TIME_USED.replaceAll(TEMPLATE_PLACEHOLDER, column);
+        }
+    }
+
     private static final String TAG = "ContactsDatabaseHelper";
 
     private static final String DATABASE_NAME = "contacts2.db";
@@ -1122,6 +1193,20 @@
                 AccountsColumns.DATA_SET + " TEXT" +
         ");");
 
+        // Note, there are two sets of the usage stat columns: LR_* and RAW_*.
+        // RAW_* contain the real values, which clients can't access.  The column names start
+        // with a special prefix, which clients are prohibited from using in queries (including
+        // "where" of deletes/updates.)
+        // The LR_* columns have the original, public names.  The views have the LR columns too,
+        // which contain the "low-res" numbers.  The tables, though, do *not* have to have these
+        // columns, because we won't use them anyway.  However, because old versions of the tables
+        // had those columns, and SQLite doesn't allow removing existing columns, meaning upgraded
+        // tables will have these LR_* columns anyway.  So, in order to make a new database look
+        // the same as an upgraded database, we create the LR columns in a new database too.
+        // Otherwise, we would easily end up with writing SQLs that will run fine in a new DB
+        // but not in an upgraded database, and because all unit tests will run with a new database,
+        // we can't easily catch these sort of issues.
+
         // One row per group of contacts corresponding to the same person
         db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
                 BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -1130,8 +1215,13 @@
                 Contacts.PHOTO_FILE_ID + " INTEGER REFERENCES photo_files(_id)," +
                 Contacts.CUSTOM_RINGTONE + " TEXT," +
                 Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
-                Contacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
-                Contacts.LAST_TIME_CONTACTED + " INTEGER," +
+
+                Contacts.RAW_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+                Contacts.RAW_LAST_TIME_CONTACTED + " INTEGER," +
+
+                Contacts.LR_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+                Contacts.LR_LAST_TIME_CONTACTED + " INTEGER," +
+
                 Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
                 Contacts.PINNED + " INTEGER NOT NULL DEFAULT " + PinnedPositions.UNPINNED + "," +
                 Contacts.HAS_PHONE_NUMBER + " INTEGER NOT NULL DEFAULT 0," +
@@ -1163,8 +1253,13 @@
                 RawContactsColumns.AGGREGATION_NEEDED + " INTEGER NOT NULL DEFAULT 1," +
                 RawContacts.CUSTOM_RINGTONE + " TEXT," +
                 RawContacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
-                RawContacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
-                RawContacts.LAST_TIME_CONTACTED + " INTEGER," +
+
+                RawContacts.RAW_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+                RawContacts.RAW_LAST_TIME_CONTACTED + " INTEGER," +
+
+                RawContacts.LR_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+                RawContacts.LR_LAST_TIME_CONTACTED + " INTEGER," +
+
                 RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.PINNED + " INTEGER NOT NULL DEFAULT "  + PinnedPositions.UNPINNED +
                     "," + RawContacts.DISPLAY_NAME_PRIMARY + " TEXT," +
@@ -1452,8 +1547,13 @@
                 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 + " INTEGER NOT NULL DEFAULT 0, " +
+
+                DataUsageStatColumns.RAW_TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
+                DataUsageStatColumns.RAW_LAST_TIME_USED + " INTEGER NOT NULL DEFAULT 0, " +
+
+                DataUsageStatColumns.LR_TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
+                DataUsageStatColumns.LR_LAST_TIME_USED + " INTEGER NOT NULL DEFAULT 0, " +
+
                 "FOREIGN KEY(" + DataUsageStatColumns.DATA_ID + ") REFERENCES "
                         + Tables.DATA + "(" + Data._ID + ")" +
         ");");
@@ -1735,6 +1835,7 @@
         db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_ENTITIES + ";");
         db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";");
         db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_STAT + ";");
+        db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_LR + ";");
         db.execSQL("DROP VIEW IF EXISTS " + Views.STREAM_ITEMS + ";");
         db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC_STATE + ";");
         db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC + ";");
@@ -1803,17 +1904,24 @@
 
         String contactOptionColumns =
                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
-                        + " AS " + RawContacts.CUSTOM_RINGTONE + ","
+                        + " AS " + Contacts.CUSTOM_RINGTONE + ","
                 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
-                        + " AS " + RawContacts.SEND_TO_VOICEMAIL + ","
-                + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
-                        + " AS " + RawContacts.LAST_TIME_CONTACTED + ","
-                + ContactsColumns.CONCRETE_TIMES_CONTACTED
-                        + " AS " + RawContacts.TIMES_CONTACTED + ","
+                        + " AS " + Contacts.SEND_TO_VOICEMAIL + ","
+
+                + ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED
+                        + " AS " + Contacts.RAW_LAST_TIME_CONTACTED + ","
+                + ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED
+                        + " AS " + Contacts.RAW_TIMES_CONTACTED + ","
+
+                + LowRes.getLastTimeUsedExpression(ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED)
+                        + " AS " + Contacts.LR_LAST_TIME_CONTACTED + ","
+                + LowRes.getTimesUsedExpression(ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED)
+                        + " AS " + Contacts.LR_TIMES_CONTACTED + ","
+
                 + ContactsColumns.CONCRETE_STARRED
-                        + " AS " + RawContacts.STARRED + ","
+                        + " AS " + Contacts.STARRED + ","
                 + ContactsColumns.CONCRETE_PINNED
-                        + " AS " + RawContacts.PINNED;
+                        + " AS " + Contacts.PINNED;
 
         String contactNameColumns =
                 "name_raw_contact." + RawContacts.DISPLAY_NAME_SOURCE
@@ -1879,8 +1987,12 @@
         String rawContactOptionColumns =
                 RawContacts.CUSTOM_RINGTONE + ","
                 + RawContacts.SEND_TO_VOICEMAIL + ","
-                + RawContacts.LAST_TIME_CONTACTED + ","
-                + RawContacts.TIMES_CONTACTED + ","
+                + RawContacts.RAW_LAST_TIME_CONTACTED + ","
+                + LowRes.getLastTimeUsedExpression(RawContacts.RAW_LAST_TIME_CONTACTED)
+                        + " AS " + RawContacts.LR_LAST_TIME_CONTACTED + ","
+                + RawContacts.RAW_TIMES_CONTACTED + ","
+                + LowRes.getTimesUsedExpression(RawContacts.RAW_TIMES_CONTACTED)
+                        + " AS " + RawContacts.LR_TIMES_CONTACTED + ","
                 + RawContacts.STARRED + ","
                 + RawContacts.PINNED;
 
@@ -1917,16 +2029,23 @@
                         + " AS " + Contacts.CUSTOM_RINGTONE + ", "
                 + contactNameColumns + ", "
                 + baseContactColumns + ", "
-                + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
-                        + " AS " + Contacts.LAST_TIME_CONTACTED + ", "
+
+                + ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED
+                        + " AS " + Contacts.RAW_LAST_TIME_CONTACTED + ", "
+                + LowRes.getLastTimeUsedExpression(ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED)
+                        + " AS " + Contacts.LR_LAST_TIME_CONTACTED + ", "
+
                 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
                         + " AS " + Contacts.SEND_TO_VOICEMAIL + ", "
                 + ContactsColumns.CONCRETE_STARRED
                         + " AS " + Contacts.STARRED + ", "
                 + ContactsColumns.CONCRETE_PINNED
                 + " AS " + Contacts.PINNED + ", "
-                + ContactsColumns.CONCRETE_TIMES_CONTACTED
-                        + " AS " + Contacts.TIMES_CONTACTED;
+
+                + ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED
+                        + " AS " + Contacts.RAW_TIMES_CONTACTED + ", "
+                + LowRes.getTimesUsedExpression(ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED)
+                        + " AS " + Contacts.LR_TIMES_CONTACTED;
 
         String contactsSelect = "SELECT "
                 + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
@@ -2016,15 +2135,34 @@
         db.execSQL("CREATE VIEW " + Views.ENTITIES + " AS "
                 + entitiesSelect);
 
+        // Data usage view, with the low res columns, with no joins.
+        final String dataUsageViewSelect = "SELECT "
+                + DataUsageStatColumns._ID + ", "
+                + DataUsageStatColumns.DATA_ID + ", "
+                + DataUsageStatColumns.USAGE_TYPE_INT + ", "
+                + DataUsageStatColumns.RAW_TIMES_USED + ", "
+                + DataUsageStatColumns.RAW_LAST_TIME_USED + ","
+                + LowRes.getTimesUsedExpression(DataUsageStatColumns.RAW_TIMES_USED)
+                    + " AS " + DataUsageStatColumns.LR_TIMES_USED + ","
+                + LowRes.getLastTimeUsedExpression(DataUsageStatColumns.RAW_LAST_TIME_USED)
+                    + " AS " + DataUsageStatColumns.LR_LAST_TIME_USED
+                + " FROM " + Tables.DATA_USAGE_STAT;
+
+        // When the data_usage_stat table is needed with the low-res columns, use this, which is
+        // faster than the DATA_USAGE_STAT view since it doesn't involve joins.
+        db.execSQL("CREATE VIEW " + Views.DATA_USAGE_LR + " AS " + dataUsageViewSelect);
+
         String dataUsageStatSelect = "SELECT "
                 + DataUsageStatColumns.CONCRETE_ID + " AS " + DataUsageStatColumns._ID + ", "
                 + DataUsageStatColumns.DATA_ID + ", "
                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
                 + MimetypesColumns.CONCRETE_MIMETYPE + " AS " + Data.MIMETYPE + ", "
                 + DataUsageStatColumns.USAGE_TYPE_INT + ", "
-                + DataUsageStatColumns.TIMES_USED + ", "
-                + DataUsageStatColumns.LAST_TIME_USED
-                + " FROM " + Tables.DATA_USAGE_STAT
+                + DataUsageStatColumns.RAW_TIMES_USED + ", "
+                + DataUsageStatColumns.RAW_LAST_TIME_USED + ", "
+                + DataUsageStatColumns.LR_TIMES_USED + ", "
+                + DataUsageStatColumns.LR_LAST_TIME_USED
+                + " FROM " + Views.DATA_USAGE_LR + " AS " + Tables.DATA_USAGE_STAT
                 + " JOIN " + Tables.DATA + " ON ("
                 +   DataColumns.CONCRETE_ID + "=" + DataUsageStatColumns.CONCRETE_DATA_ID + ")"
                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
@@ -2458,6 +2596,12 @@
             oldVersion = 1200;
         }
 
+        if (isUpgradeRequired(oldVersion, newVersion, 1201)) {
+            upgradeToVersion1201(db);
+            upgradeViewsAndTriggers = true;
+            oldVersion = 1201;
+        }
+
         // We extracted "calls" and "voicemail_status" at this point, but we can't remove them here
         // yet, until CallLogDatabaseHelper moves the data.
 
@@ -3201,6 +3345,35 @@
         FastScrollingIndexCache.getInstance(mContext).invalidate();
     }
 
+    private void upgradeToVersion1201(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE contacts ADD x_times_contacted INTEGER NOT NULL DEFAULT 0");
+        db.execSQL("ALTER TABLE contacts ADD x_last_time_contacted INTEGER");
+
+        db.execSQL("ALTER TABLE raw_contacts ADD x_times_contacted INTEGER NOT NULL DEFAULT 0");
+        db.execSQL("ALTER TABLE raw_contacts ADD x_last_time_contacted INTEGER");
+
+        db.execSQL("ALTER TABLE data_usage_stat ADD x_times_used INTEGER NOT NULL DEFAULT 0");
+        db.execSQL("ALTER TABLE data_usage_stat ADD x_last_time_used INTEGER NOT NULL DEFAULT 0");
+
+        db.execSQL("UPDATE contacts SET "
+                + "x_times_contacted = ifnull(times_contacted,0),"
+                + "x_last_time_contacted = ifnull(last_time_contacted,0),"
+                + "times_contacted = 0,"
+                + "last_time_contacted = 0");
+
+        db.execSQL("UPDATE raw_contacts SET "
+                + "x_times_contacted = ifnull(times_contacted,0),"
+                + "x_last_time_contacted = ifnull(last_time_contacted,0),"
+                + "times_contacted = 0,"
+                + "last_time_contacted = 0");
+
+        db.execSQL("UPDATE data_usage_stat SET "
+                + "x_times_used = ifnull(times_used,0),"
+                + "x_last_time_used = ifnull(last_time_used,0),"
+                + "times_used = 0,"
+                + "last_time_used = 0");
+    }
+
     /**
      * This method is only used in upgradeToVersion1101 method, and should not be used in other
      * places now. Because data15 is not used to generate hash_id for photo, and the new generating
@@ -4816,12 +4989,19 @@
     }
 
     private void reportInvalidSql(String callerPackage, InvalidSqlException e) {
-        final String message = String.format("%s caller=%s", e.getMessage(), callerPackage);
+        logWtf(String.format("%s caller=%s", e.getMessage(), callerPackage));
+        throw e; // STOPSHIP Don't throw for pre-O apps.
+    }
+
+    /**
+     * Calls WTF without crashing, so we can collect errors in the wild.  During unit tests, it'll
+     * log only.
+     */
+    public void logWtf(String message) {
         if (mIsTestInstance) {
             Slog.w(TAG, "[Test mode, warning only] " + message);
         } else {
             Slog.wtfStack(TAG, message);
         }
-        throw e; // STOPSHIP Don't throw for pre-O apps.
     }
 }
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index c598d1c..ad87643 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -221,13 +221,13 @@
     private static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS";
 
     /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
-          "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
-          " ifnull(" + Contacts.TIMES_CONTACTED + ",0)+1" +
+          "UPDATE " + Tables.CONTACTS + " SET " + Contacts.RAW_TIMES_CONTACTED + "=" +
+          " ifnull(" + Contacts.RAW_TIMES_CONTACTED + ",0)+1" +
           " WHERE " + Contacts._ID + "=?";
 
     /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
-          "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
-          " ifnull(" + RawContacts.TIMES_CONTACTED + ",0)+1 " +
+          "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.RAW_TIMES_CONTACTED + "=" +
+          " ifnull(" + RawContacts.RAW_TIMES_CONTACTED + ",0)+1 " +
           " WHERE " + RawContacts.CONTACT_ID + "=?";
 
     /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
@@ -314,7 +314,7 @@
     public static final ProfileAwareUriMatcher sUriMatcher =
             new ProfileAwareUriMatcher(UriMatcher.NO_MATCH);
 
-    private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.TIMES_USED + " DESC,"
+    private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.RAW_TIMES_USED + " DESC,"
             + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
 
     public static final int CONTACTS = 1000;
@@ -611,20 +611,23 @@
     // Contacts contacted within the last 30 days (in seconds)
     private static final long LAST_TIME_USED_30_DAYS_SEC = 30L * 24 * 60 * 60;
 
-    private static final String TIME_SINCE_LAST_USED_SEC =
-            "(strftime('%s', 'now') - " + DataUsageStatColumns.LAST_TIME_USED + "/1000)";
+    private static final String RAW_TIME_SINCE_LAST_USED_SEC =
+            "(strftime('%s', 'now') - " + DataUsageStatColumns.RAW_LAST_TIME_USED + "/1000)";
+
+    private static final String LR_TIME_SINCE_LAST_USED_SEC =
+            "(strftime('%s', 'now') - " + DataUsageStatColumns.LR_LAST_TIME_USED + "/1000)";
 
     private static final String SORT_BY_DATA_USAGE =
-            "(CASE WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_3_DAYS_SEC +
+            "(CASE WHEN " + RAW_TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_3_DAYS_SEC +
             " THEN 0 " +
-                    " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_7_DAYS_SEC +
+                    " WHEN " + RAW_TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_7_DAYS_SEC +
             " THEN 1 " +
-                    " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_14_DAYS_SEC +
+                    " WHEN " + RAW_TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_14_DAYS_SEC +
             " THEN 2 " +
-                    " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_30_DAYS_SEC +
+                    " WHEN " + RAW_TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_30_DAYS_SEC +
             " THEN 3 " +
             " ELSE 4 END), " +
-            DataUsageStatColumns.TIMES_USED + " DESC";
+            DataUsageStatColumns.RAW_TIMES_USED + " DESC";
 
     /*
      * Sorting order for email address suggestions: first starred, then the rest.
@@ -677,7 +680,7 @@
             .add(Contacts.DISPLAY_NAME_SOURCE)
             .add(Contacts.IN_DEFAULT_DIRECTORY)
             .add(Contacts.IN_VISIBLE_GROUP)
-            .add(Contacts.LAST_TIME_CONTACTED)
+            .add(Contacts.LR_LAST_TIME_CONTACTED)
             .add(Contacts.LOOKUP_KEY)
             .add(Contacts.PHONETIC_NAME)
             .add(Contacts.PHONETIC_NAME_STYLE)
@@ -694,7 +697,7 @@
             .add(ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE)
             .add(Contacts.STARRED)
             .add(Contacts.PINNED)
-            .add(Contacts.TIMES_CONTACTED)
+            .add(Contacts.LR_TIMES_CONTACTED)
             .add(Contacts.HAS_PHONE_NUMBER)
             .add(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)
             .build();
@@ -795,8 +798,8 @@
             .build();
 
     private static final ProjectionMap sDataUsageColumns = ProjectionMap.builder()
-            .add(Data.TIMES_USED, Tables.DATA_USAGE_STAT + "." + Data.TIMES_USED)
-            .add(Data.LAST_TIME_USED, Tables.DATA_USAGE_STAT + "." + Data.LAST_TIME_USED)
+            .add(Data.LR_TIMES_USED, Tables.DATA_USAGE_STAT + "." + Data.LR_TIMES_USED)
+            .add(Data.LR_LAST_TIME_USED, Tables.DATA_USAGE_STAT + "." + Data.LR_LAST_TIME_USED)
             .build();
 
     /** Contains just BaseColumns._COUNT */
@@ -823,16 +826,18 @@
     /** Used for pushing starred contacts to the top of a times contacted list **/
     private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder()
             .addAll(sContactsProjectionMap)
-            .add(DataUsageStatColumns.TIMES_USED, String.valueOf(Long.MAX_VALUE))
-            .add(DataUsageStatColumns.LAST_TIME_USED, String.valueOf(Long.MAX_VALUE))
+            .add(DataUsageStatColumns.LR_TIMES_USED, String.valueOf(Long.MAX_VALUE))
+            .add(DataUsageStatColumns.LR_LAST_TIME_USED, String.valueOf(Long.MAX_VALUE))
             .build();
 
     private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder()
             .addAll(sContactsProjectionMap)
-            .add(DataUsageStatColumns.TIMES_USED,
-                    "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED + ")")
-            .add(DataUsageStatColumns.LAST_TIME_USED,
-                    "MAX(" + DataUsageStatColumns.CONCRETE_LAST_TIME_USED + ")")
+            // Note this should ideally be "lowres(SUM)" rather than "SUM(lowres)", but we do it
+            // this way for performance reasons.
+            .add(DataUsageStatColumns.LR_TIMES_USED,
+                    "SUM(" + DataUsageStatColumns.CONCRETE_LR_TIMES_USED + ")")
+            .add(DataUsageStatColumns.LR_LAST_TIME_USED,
+                    "MAX(" + DataUsageStatColumns.CONCRETE_LR_LAST_TIME_USED + ")")
             .build();
 
     /**
@@ -844,8 +849,8 @@
     private static final ProjectionMap sStrequentPhoneOnlyProjectionMap
             = ProjectionMap.builder()
             .addAll(sContactsProjectionMap)
-            .add(DataUsageStatColumns.TIMES_USED, DataUsageStatColumns.CONCRETE_TIMES_USED)
-            .add(DataUsageStatColumns.LAST_TIME_USED, DataUsageStatColumns.CONCRETE_LAST_TIME_USED)
+            .add(DataUsageStatColumns.LR_TIMES_USED)
+            .add(DataUsageStatColumns.LR_LAST_TIME_USED)
             .add(Phone.NUMBER)
             .add(Phone.TYPE)
             .add(Phone.LABEL)
@@ -877,8 +882,8 @@
             .add(RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY)
             .add(RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE)
             .add(RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE)
-            .add(RawContacts.TIMES_CONTACTED)
-            .add(RawContacts.LAST_TIME_CONTACTED)
+            .add(RawContacts.LR_TIMES_CONTACTED)
+            .add(RawContacts.LR_LAST_TIME_CONTACTED)
             .add(RawContacts.CUSTOM_RINGTONE)
             .add(RawContacts.SEND_TO_VOICEMAIL)
             .add(RawContacts.STARRED)
@@ -985,8 +990,8 @@
             .add(PhoneLookup.PHONETIC_NAME_STYLE, "contacts_view." + Contacts.PHONETIC_NAME_STYLE)
             .add(PhoneLookup.SORT_KEY_PRIMARY, "contacts_view." + Contacts.SORT_KEY_PRIMARY)
             .add(PhoneLookup.SORT_KEY_ALTERNATIVE, "contacts_view." + Contacts.SORT_KEY_ALTERNATIVE)
-            .add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED)
-            .add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED)
+            .add(PhoneLookup.LR_LAST_TIME_CONTACTED, "contacts_view." + Contacts.LR_LAST_TIME_CONTACTED)
+            .add(PhoneLookup.LR_TIMES_CONTACTED, "contacts_view." + Contacts.LR_TIMES_CONTACTED)
             .add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED)
             .add(PhoneLookup.IN_DEFAULT_DIRECTORY, "contacts_view." + Contacts.IN_DEFAULT_DIRECTORY)
             .add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP)
@@ -2879,6 +2884,8 @@
     private long insertRawContact(
             Uri uri, ContentValues inputValues, boolean callerIsSyncAdapter) {
 
+        inputValues = fixUpUsageColumnsForEdit(inputValues);
+
         // Create a shallow copy and initialize the contact ID to null.
         final ContentValues values = new ContentValues(inputValues);
         values.putNull(RawContacts.CONTACT_ID);
@@ -4001,12 +4008,12 @@
     private int deleteDataUsage() {
         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
         db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
-                Contacts.TIMES_CONTACTED + "=0," +
-                Contacts.LAST_TIME_CONTACTED + "=NULL");
+                Contacts.RAW_TIMES_CONTACTED + "=0," +
+                Contacts.RAW_LAST_TIME_CONTACTED + "=NULL");
 
         db.execSQL("UPDATE " + Tables.CONTACTS + " SET " +
-                Contacts.TIMES_CONTACTED + "=0," +
-                Contacts.LAST_TIME_CONTACTED + "=NULL");
+                Contacts.RAW_TIMES_CONTACTED + "=0," +
+                Contacts.RAW_LAST_TIME_CONTACTED + "=NULL");
 
         db.delete(Tables.DATA_USAGE_STAT, null, null);
         return 1;
@@ -4529,11 +4536,42 @@
         return count;
     }
 
+    /**
+     * Used for insert/update raw_contacts/contacts to adjust TIMES_CONTACTED and
+     * LAST_TIME_CONTACTED.
+     */
+    private ContentValues fixUpUsageColumnsForEdit(ContentValues cv) {
+        if (!cv.containsKey(Contacts.LR_LAST_TIME_CONTACTED)
+                && !cv.containsKey(Contacts.LR_TIMES_CONTACTED)) {
+            return cv;
+        }
+        final ContentValues ret = new ContentValues(cv);
+
+        ContactsDatabaseHelper.copyLongValue(
+                ret, Contacts.RAW_LAST_TIME_CONTACTED,
+                ret, Contacts.LR_LAST_TIME_CONTACTED);
+        if (ret.containsKey(Contacts.LR_TIMES_CONTACTED)) {
+            getDatabaseHelper().logWtf(
+                    "Column '" + Contacts.LR_TIMES_CONTACTED + "' can no longer be modified"
+                    + "directly.");
+        }
+
+        ret.remove(Contacts.LR_LAST_TIME_CONTACTED);
+        ret.remove(Contacts.LR_TIMES_CONTACTED);
+        return ret;
+    }
+
     private int updateRawContact(SQLiteDatabase db, long rawContactId, ContentValues values,
             boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
         final String selection = RawContactsColumns.CONCRETE_ID + " = ?";
         mSelectionArgs1[0] = Long.toString(rawContactId);
 
+        values = fixUpUsageColumnsForEdit(values);
+
+        if (values.size() == 0) {
+            return 0; // Nothing to update; bail out.
+        }
+
         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
 
         final boolean requestUndoDelete = flagIsClear(values, RawContacts.DELETED);
@@ -4768,6 +4806,8 @@
     private int updateContactOptions(
             SQLiteDatabase db, long contactId, ContentValues inputValues, boolean callerIsSyncAdapter) {
 
+        inputValues = fixUpUsageColumnsForEdit(inputValues);
+
         final ContentValues values = new ContentValues();
         ContactsDatabaseHelper.copyStringValue(
                 values, RawContacts.CUSTOM_RINGTONE,
@@ -4776,11 +4816,11 @@
                 values, RawContacts.SEND_TO_VOICEMAIL,
                 inputValues, Contacts.SEND_TO_VOICEMAIL);
         ContactsDatabaseHelper.copyLongValue(
-                values, RawContacts.LAST_TIME_CONTACTED,
-                inputValues, Contacts.LAST_TIME_CONTACTED);
+                values, RawContacts.RAW_LAST_TIME_CONTACTED,
+                inputValues, Contacts.RAW_LAST_TIME_CONTACTED);
         ContactsDatabaseHelper.copyLongValue(
-                values, RawContacts.TIMES_CONTACTED,
-                inputValues, Contacts.TIMES_CONTACTED);
+                values, RawContacts.RAW_TIMES_CONTACTED,
+                inputValues, Contacts.RAW_TIMES_CONTACTED);
         ContactsDatabaseHelper.copyLongValue(
                 values, RawContacts.STARRED,
                 inputValues, Contacts.STARRED);
@@ -4838,11 +4878,11 @@
                 values, RawContacts.SEND_TO_VOICEMAIL,
                 inputValues, Contacts.SEND_TO_VOICEMAIL);
         ContactsDatabaseHelper.copyLongValue(
-                values, RawContacts.LAST_TIME_CONTACTED,
-                inputValues, Contacts.LAST_TIME_CONTACTED);
+                values, RawContacts.RAW_LAST_TIME_CONTACTED,
+                inputValues, Contacts.RAW_LAST_TIME_CONTACTED);
         ContactsDatabaseHelper.copyLongValue(
-                values, RawContacts.TIMES_CONTACTED,
-                inputValues, Contacts.TIMES_CONTACTED);
+                values, RawContacts.RAW_TIMES_CONTACTED,
+                inputValues, Contacts.RAW_TIMES_CONTACTED);
         ContactsDatabaseHelper.copyLongValue(
                 values, RawContacts.STARRED,
                 inputValues, Contacts.STARRED);
@@ -4856,8 +4896,8 @@
         int rslt = db.update(Tables.CONTACTS, values, Contacts._ID + "=?",
                 mSelectionArgs1);
 
-        if (inputValues.containsKey(Contacts.LAST_TIME_CONTACTED) &&
-                !inputValues.containsKey(Contacts.TIMES_CONTACTED)) {
+        if (inputValues.containsKey(Contacts.RAW_LAST_TIME_CONTACTED) &&
+                !inputValues.containsKey(Contacts.RAW_TIMES_CONTACTED)) {
             db.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
             db.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
         }
@@ -5098,8 +5138,8 @@
                     ContentValues usageStatsValues = new ContentValues();
                     usageStatsValues.put(DataUsageStatColumns.DATA_ID, dataId);
                     usageStatsValues.put(DataUsageStatColumns.USAGE_TYPE_INT, typeInt);
-                    usageStatsValues.put(DataUsageStatColumns.LAST_TIME_USED, lastTimeUsed);
-                    usageStatsValues.put(DataUsageStatColumns.TIMES_USED, timesUsed);
+                    usageStatsValues.put(DataUsageStatColumns.RAW_LAST_TIME_USED, lastTimeUsed);
+                    usageStatsValues.put(DataUsageStatColumns.RAW_TIMES_USED, timesUsed);
                     updateDataUsageStats(db, usageStatsValues);
                 }
             }
@@ -5980,8 +6020,8 @@
                 if (projection != null) {
                     subProjection = new String[projection.length + 2];
                     System.arraycopy(projection, 0, subProjection, 0, projection.length);
-                    subProjection[projection.length + 0] = DataUsageStatColumns.TIMES_USED;
-                    subProjection[projection.length + 1] = DataUsageStatColumns.LAST_TIME_USED;
+                    subProjection[projection.length + 0] = DataUsageStatColumns.LR_TIMES_USED;
+                    subProjection[projection.length + 1] = DataUsageStatColumns.LR_LAST_TIME_USED;
                 }
 
                 // String that will store the query for starred contacts. For phone only queries,
@@ -6004,7 +6044,8 @@
                     // it is included in the list of strequent numbers.
                     tableBuilder.append("(SELECT * FROM " + Views.DATA + " WHERE "
                             + Contacts.STARRED + "=1)" + " AS " + Tables.DATA
-                        + " LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT
+                        + " LEFT OUTER JOIN " + Views.DATA_USAGE_LR
+                            + " AS " + Tables.DATA_USAGE_STAT
                             + " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="
                                 + DataColumns.CONCRETE_ID + " AND "
                             + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "="
@@ -6037,7 +6078,7 @@
                     // data rows (almost always it should be), and we don't want any phone
                     // numbers not used by the user. This way sqlite is able to drop a number of
                     // rows in view_data in the early stage of data lookup.
-                    tableBuilder.append(Tables.DATA_USAGE_STAT
+                    tableBuilder.append(Views.DATA_USAGE_LR + " AS " + Tables.DATA_USAGE_STAT
                             + " INNER JOIN " + Views.DATA + " " + Tables.DATA
                             + " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="
                                 + DataColumns.CONCRETE_ID + " AND "
@@ -6090,7 +6131,7 @@
 
                 // Phone numbers that were used more than 30 days ago are dropped from frequents
                 final String frequentQuery = "SELECT * FROM (" + frequentInnerQuery + ") WHERE " +
-                        TIME_SINCE_LAST_USED_SEC + "<" + LAST_TIME_USED_30_DAYS_SEC;
+                        LR_TIME_SINCE_LAST_USED_SEC + "<" + LAST_TIME_USED_30_DAYS_SEC;
                 final String starredQuery = "SELECT * FROM (" + starredInnerQuery + ")";
 
                 // Put them together
@@ -8010,7 +8051,7 @@
         if (includeDataUsageStat) {
             sb.append(" ON (" +
                     DbQueryUtils.concatenateClauses(
-                            DataUsageStatColumns.CONCRETE_TIMES_USED + " > 0",
+                            DataUsageStatColumns.CONCRETE_RAW_TIMES_USED + " > 0",
                             RawContacts.CONTACT_ID + "=" + Views.CONTACTS + "." + Contacts._ID) +
                     ")");
         }
@@ -8397,7 +8438,8 @@
 
     private void appendDataUsageStatJoin(StringBuilder sb, int usageType, String dataIdColumn) {
         if (usageType != USAGE_TYPE_ALL) {
-            sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT +
+            sb.append(" LEFT OUTER JOIN " + Views.DATA_USAGE_LR +
+                    " as " + Tables.DATA_USAGE_STAT +
                     " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=");
             sb.append(dataIdColumn);
             sb.append(" AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=");
@@ -8407,13 +8449,21 @@
             sb.append(
                     " LEFT OUTER JOIN " +
                         "(SELECT " +
-                            DataUsageStatColumns.CONCRETE_DATA_ID + " as STAT_DATA_ID, " +
-                            "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED +
-                                ") as " + DataUsageStatColumns.TIMES_USED + ", " +
-                            "MAX(" + DataUsageStatColumns.CONCRETE_LAST_TIME_USED +
-                                ") as " + DataUsageStatColumns.LAST_TIME_USED +
-                        " FROM " + Tables.DATA_USAGE_STAT + " GROUP BY " +
-                            DataUsageStatColumns.CONCRETE_DATA_ID + ") as " + Tables.DATA_USAGE_STAT
+                            DataUsageStatColumns.DATA_ID + " as STAT_DATA_ID," +
+                            " SUM(ifnull(" + DataUsageStatColumns.RAW_TIMES_USED +
+                                ",0)) as " + DataUsageStatColumns.RAW_TIMES_USED + ", " +
+                            " MAX(ifnull(" + DataUsageStatColumns.RAW_LAST_TIME_USED +
+                                ",0)) as " + DataUsageStatColumns.RAW_LAST_TIME_USED + "," +
+
+                            // Note this is not ideal -- we should use "lowres(sum(LR_TIMES_USED))"
+                            // here, but for performance reasons we just do it simple.
+                            " SUM(ifnull(" + DataUsageStatColumns.LR_TIMES_USED +
+                                ",0)) as " + DataUsageStatColumns.LR_TIMES_USED + ", " +
+
+                            " MAX(ifnull(" + DataUsageStatColumns.LR_LAST_TIME_USED +
+                                ",0)) as " + DataUsageStatColumns.LR_LAST_TIME_USED +
+                        " FROM " + Views.DATA_USAGE_LR + " GROUP BY " +
+                            DataUsageStatColumns.DATA_ID + ") as " + Tables.DATA_USAGE_STAT
                     );
             sb.append(" ON (STAT_DATA_ID=");
             sb.append(dataIdColumn);
@@ -9831,15 +9881,15 @@
         final String rids = TextUtils.join(",", rawContactIds);
 
         db.execSQL("UPDATE " + Tables.RAW_CONTACTS +
-                " SET " + RawContacts.LAST_TIME_CONTACTED + "=?" +
-                "," + RawContacts.TIMES_CONTACTED + "=" +
-                    "ifnull(" + RawContacts.TIMES_CONTACTED + ",0) + 1" +
+                " SET " + RawContacts.RAW_LAST_TIME_CONTACTED + "=?" +
+                "," + RawContacts.RAW_TIMES_CONTACTED + "=" +
+                    "ifnull(" + RawContacts.RAW_TIMES_CONTACTED + ",0) + 1" +
                 " WHERE " + RawContacts._ID + " IN (" + rids + ")"
                 , mSelectionArgs1);
         db.execSQL("UPDATE " + Tables.CONTACTS +
-                " SET " + Contacts.LAST_TIME_CONTACTED + "=?1" +
-                "," + Contacts.TIMES_CONTACTED + "=" +
-                    "ifnull(" + Contacts.TIMES_CONTACTED + ",0) + 1" +
+                " SET " + Contacts.RAW_LAST_TIME_CONTACTED + "=?1" +
+                "," + Contacts.RAW_TIMES_CONTACTED + "=" +
+                    "ifnull(" + Contacts.RAW_TIMES_CONTACTED + ",0) + 1" +
                 "," + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=?1" +
                 " WHERE " + Contacts._ID + " IN (SELECT " + RawContacts.CONTACT_ID +
                     " FROM " + Tables.RAW_CONTACTS +
@@ -9886,9 +9936,9 @@
                     mSelectionArgs2[1] = String.valueOf(id);
 
                     db.execSQL("UPDATE " + Tables.DATA_USAGE_STAT +
-                            " SET " + DataUsageStatColumns.TIMES_USED + "=" +
-                                "ifnull(" + DataUsageStatColumns.TIMES_USED +",0)+1" +
-                            "," + DataUsageStatColumns.LAST_TIME_USED + "=?" +
+                            " SET " + DataUsageStatColumns.RAW_TIMES_USED + "=" +
+                                "ifnull(" + DataUsageStatColumns.RAW_TIMES_USED +",0)+1" +
+                            "," + DataUsageStatColumns.RAW_LAST_TIME_USED + "=?" +
                             " WHERE " + DataUsageStatColumns._ID + "=?",
                             mSelectionArgs2);
                 } else {
@@ -9899,8 +9949,8 @@
                     db.execSQL("INSERT INTO " + Tables.DATA_USAGE_STAT +
                             "(" + DataUsageStatColumns.DATA_ID +
                             "," + DataUsageStatColumns.USAGE_TYPE_INT +
-                            "," + DataUsageStatColumns.TIMES_USED +
-                            "," + DataUsageStatColumns.LAST_TIME_USED +
+                            "," + DataUsageStatColumns.RAW_TIMES_USED +
+                            "," + DataUsageStatColumns.RAW_LAST_TIME_USED +
                             ") VALUES (?,?,?,?)",
                             mSelectionArgs4);
                 }
@@ -9913,14 +9963,14 @@
     }
 
     /**
-     * Update {@link Tables#DATA_USAGE_STAT}.
+     * Directly update {@link Tables#DATA_USAGE_STAT}; used for metadata sync.
      * Update or insert usageType, lastTimeUsed, and timesUsed for specific dataId.
      */
     private void updateDataUsageStats(SQLiteDatabase db, ContentValues values) {
         final String dataId = values.getAsString(DataUsageStatColumns.DATA_ID);
         final String type = values.getAsString(DataUsageStatColumns.USAGE_TYPE_INT);
-        final String lastTimeUsed = values.getAsString(DataUsageStatColumns.LAST_TIME_USED);
-        final String timesUsed = values.getAsString(DataUsageStatColumns.TIMES_USED);
+        final String lastTimeUsed = values.getAsString(DataUsageStatColumns.RAW_LAST_TIME_USED);
+        final String timesUsed = values.getAsString(DataUsageStatColumns.RAW_TIMES_USED);
 
         mSelectionArgs2[0] = dataId;
         mSelectionArgs2[1] = type;
@@ -9936,8 +9986,8 @@
                 mSelectionArgs3[1] = timesUsed;
                 mSelectionArgs3[2] = String.valueOf(id);
                 db.execSQL("UPDATE " + Tables.DATA_USAGE_STAT +
-                        " SET " + DataUsageStatColumns.LAST_TIME_USED + "=?" +
-                        "," + DataUsageStatColumns.TIMES_USED + "=?" +
+                        " SET " + DataUsageStatColumns.RAW_LAST_TIME_USED + "=?" +
+                        "," + DataUsageStatColumns.RAW_TIMES_USED + "=?" +
                         " WHERE " + DataUsageStatColumns._ID + "=?",
                         mSelectionArgs3);
             } else {
@@ -9948,8 +9998,8 @@
                 db.execSQL("INSERT INTO " + Tables.DATA_USAGE_STAT +
                         "(" + DataUsageStatColumns.DATA_ID +
                         "," + DataUsageStatColumns.USAGE_TYPE_INT +
-                        "," + DataUsageStatColumns.TIMES_USED +
-                        "," + DataUsageStatColumns.LAST_TIME_USED +
+                        "," + DataUsageStatColumns.RAW_TIMES_USED +
+                        "," + DataUsageStatColumns.RAW_LAST_TIME_USED +
                         ") VALUES (?,?,?,?)",
                         mSelectionArgs4);
             }
@@ -10186,4 +10236,9 @@
     public ContactsDatabaseHelper getContactsDatabaseHelperForTest() {
         return mContactsHelper;
     }
+
+    @VisibleForTesting
+    public ProfileProvider getProfileProviderForTest() {
+        return mProfileProvider;
+    }
 }
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index 20307d4..6678b95 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -232,7 +232,7 @@
                         + Contacts.PHOTO_THUMBNAIL_URI + ", "
                         + Contacts.DISPLAY_NAME + ", "
                         + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE + ", "
-                        + Contacts.LAST_TIME_CONTACTED);
+                        + Contacts.LR_LAST_TIME_CONTACTED);
         if (haveFilter) {
             sb.append(", " + SearchSnippets.SNIPPET);
         }
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index 961d2a4..741639a 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -181,15 +181,6 @@
                 + " ELSE " + Tables.DATA + "." + Email.DATA
                 + " END)";
 
-    private static final String CONTACTS_UPDATE_LASTTIMECONTACTED =
-            "UPDATE " + Tables.CONTACTS +
-            " SET " + Contacts.LAST_TIME_CONTACTED + "=? " +
-            "WHERE " + Contacts._ID + "=?";
-    private static final String RAWCONTACTS_UPDATE_LASTTIMECONTACTED =
-            "UPDATE " + Tables.RAW_CONTACTS + " SET "
-            + RawContacts.LAST_TIME_CONTACTED + "=? WHERE "
-            + RawContacts._ID + "=?";
-
     private String[] mSelectionArgs1 = new String[1];
     private String[] mSelectionArgs2 = new String[2];
 
@@ -546,10 +537,12 @@
                         + " AS " + People.NOTES + ", " +
                 AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
                 AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
-                Tables.RAW_CONTACTS + "." + RawContacts.TIMES_CONTACTED
-                        + " AS " + People.TIMES_CONTACTED + ", " +
-                Tables.RAW_CONTACTS + "." + RawContacts.LAST_TIME_CONTACTED
-                        + " AS " + People.LAST_TIME_CONTACTED + ", " +
+
+                // We no longer return even low-res values from CP1.
+                // Note if we just use the value 0 below, certain seletion wouldn't work.
+                "cast(0 as int) AS " + People.TIMES_CONTACTED + ", " +
+                "cast(0 as int) AS " + People.LAST_TIME_CONTACTED + ", " +
+
                 Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE
                         + " AS " + People.CUSTOM_RINGTONE + ", " +
                 Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL
@@ -926,7 +919,7 @@
         int count = 0;
         switch(match) {
             case PEOPLE_UPDATE_CONTACT_TIME: {
-                count = updateContactTime(uri, values);
+                count = 0; // No longer supported.
                 break;
             }
 
@@ -1055,11 +1048,6 @@
             }
         }
 
-        if (values.containsKey(People.LAST_TIME_CONTACTED) &&
-                !values.containsKey(People.TIMES_CONTACTED)) {
-            updateContactTime(rawContactId, values);
-        }
-
         return count;
     }
 
@@ -1121,35 +1109,6 @@
                 Groups._ID + "=" + groupId, null);
     }
 
-    private int updateContactTime(Uri uri, ContentValues values) {
-        long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
-        updateContactTime(rawContactId, values);
-        return 1;
-    }
-
-    private void updateContactTime(long rawContactId, ContentValues values) {
-        final Long storedTimeContacted = values.getAsLong(People.LAST_TIME_CONTACTED);
-        final long lastTimeContacted = storedTimeContacted != null ?
-            storedTimeContacted : System.currentTimeMillis();
-
-        // TODO check sanctions
-        long contactId = mDbHelper.getContactId(rawContactId);
-        SQLiteDatabase mDb = mDbHelper.getWritableDatabase();
-        mSelectionArgs2[0] = String.valueOf(lastTimeContacted);
-        if (contactId != 0) {
-            mSelectionArgs2[1] = String.valueOf(contactId);
-            mDb.execSQL(CONTACTS_UPDATE_LASTTIMECONTACTED, mSelectionArgs2);
-            // increment times_contacted column
-            mSelectionArgs1[0] = String.valueOf(contactId);
-            mDb.execSQL(ContactsProvider2.UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
-        }
-        mSelectionArgs2[1] = String.valueOf(rawContactId);
-        mDb.execSQL(RAWCONTACTS_UPDATE_LASTTIMECONTACTED, mSelectionArgs2);
-        // increment times_contacted column
-        mSelectionArgs1[0] = String.valueOf(contactId);
-        mDb.execSQL(ContactsProvider2.UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
-    }
-
     private int updatePhoto(long rawContactId, ContentValues values) {
 
         // TODO check sanctions
@@ -1337,10 +1296,12 @@
                 values, People.CUSTOM_RINGTONE);
         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
                 values, People.SEND_TO_VOICEMAIL);
-        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
-                values, People.LAST_TIME_CONTACTED);
-        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
-                values, People.TIMES_CONTACTED);
+
+        // We no longer support the following fields in CP1.
+        // ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
+        //         values, People.LAST_TIME_CONTACTED);
+        // ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
+        //        values, People.TIMES_CONTACTED);
         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
                 values, People.STARRED);
         if (mAccount != null) {
diff --git a/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
index 1501138..2a36675 100644
--- a/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
+++ b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
@@ -1263,8 +1263,8 @@
                         + RawContacts.SOURCE_ID + ","
                         + RawContacts.CUSTOM_RINGTONE + ","
                         + RawContacts.SEND_TO_VOICEMAIL + ","
-                        + RawContacts.LAST_TIME_CONTACTED + ","
-                        + RawContacts.TIMES_CONTACTED + ","
+                        + RawContacts.RAW_LAST_TIME_CONTACTED + ","
+                        + RawContacts.RAW_TIMES_CONTACTED + ","
                         + RawContacts.STARRED + ","
                         + RawContacts.PINNED + ","
                         + DataColumns.CONCRETE_ID + ","
@@ -1299,8 +1299,8 @@
         int SOURCE_ID = 6;
         int CUSTOM_RINGTONE = 7;
         int SEND_TO_VOICEMAIL = 8;
-        int LAST_TIME_CONTACTED = 9;
-        int TIMES_CONTACTED = 10;
+        int RAW_LAST_TIME_CONTACTED = 9;
+        int RAW_TIMES_CONTACTED = 10;
         int STARRED = 11;
         int PINNED = 12;
         int DATA_ID = 13;
@@ -1319,8 +1319,8 @@
                         + Contacts.PHOTO_FILE_ID + "=?, "
                         + Contacts.SEND_TO_VOICEMAIL + "=?, "
                         + Contacts.CUSTOM_RINGTONE + "=?, "
-                        + Contacts.LAST_TIME_CONTACTED + "=?, "
-                        + Contacts.TIMES_CONTACTED + "=?, "
+                        + Contacts.RAW_LAST_TIME_CONTACTED + "=?, "
+                        + Contacts.RAW_TIMES_CONTACTED + "=?, "
                         + Contacts.STARRED + "=?, "
                         + Contacts.PINNED + "=?, "
                         + Contacts.HAS_PHONE_NUMBER + "=?, "
@@ -1335,8 +1335,8 @@
                         + Contacts.PHOTO_FILE_ID + ", "
                         + Contacts.SEND_TO_VOICEMAIL + ", "
                         + Contacts.CUSTOM_RINGTONE + ", "
-                        + Contacts.LAST_TIME_CONTACTED + ", "
-                        + Contacts.TIMES_CONTACTED + ", "
+                        + Contacts.RAW_LAST_TIME_CONTACTED + ", "
+                        + Contacts.RAW_TIMES_CONTACTED + ", "
                         + Contacts.STARRED + ", "
                         + Contacts.PINNED + ", "
                         + Contacts.HAS_PHONE_NUMBER + ", "
@@ -1350,8 +1350,8 @@
         int PHOTO_FILE_ID = 3;
         int SEND_TO_VOICEMAIL = 4;
         int CUSTOM_RINGTONE = 5;
-        int LAST_TIME_CONTACTED = 6;
-        int TIMES_CONTACTED = 7;
+        int RAW_LAST_TIME_CONTACTED = 6;
+        int RAW_TIMES_CONTACTED = 7;
         int STARRED = 8;
         int PINNED = 9;
         int HAS_PHONE_NUMBER = 10;
@@ -1439,12 +1439,12 @@
                         contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
                     }
 
-                    long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
+                    long lastTimeContacted = c.getLong(RawContactsQuery.RAW_LAST_TIME_CONTACTED);
                     if (lastTimeContacted > contactLastTimeContacted) {
                         contactLastTimeContacted = lastTimeContacted;
                     }
 
-                    int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
+                    int timesContacted = c.getInt(RawContactsQuery.RAW_TIMES_CONTACTED);
                     if (timesContacted > contactTimesContacted) {
                         contactTimesContacted = timesContacted;
                     }
@@ -1523,9 +1523,9 @@
                 totalRowCount == contactSendToVoicemail ? 1 : 0);
         DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
                 contactCustomRingtone);
-        statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
+        statement.bindLong(ContactReplaceSqlStatement.RAW_LAST_TIME_CONTACTED,
                 contactLastTimeContacted);
-        statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
+        statement.bindLong(ContactReplaceSqlStatement.RAW_TIMES_CONTACTED,
                 contactTimesContacted);
         statement.bindLong(ContactReplaceSqlStatement.STARRED,
                 contactStarred);
diff --git a/src/com/android/providers/contacts/sqlite/SqlChecker.java b/src/com/android/providers/contacts/sqlite/SqlChecker.java
index 6404220..2db1f47 100644
--- a/src/com/android/providers/contacts/sqlite/SqlChecker.java
+++ b/src/com/android/providers/contacts/sqlite/SqlChecker.java
@@ -33,6 +33,8 @@
 public class SqlChecker {
     private static final String TAG = "SqlChecker";
 
+    private static final String PRIVATE_PREFIX = "x_"; // MUST BE LOWERCASE.
+
     private static final boolean VERBOSE_LOGGING = AbstractContactsProvider.VERBOSE_LOGGING;
 
     private final ArraySet<String> mInvalidTokens;
@@ -47,7 +49,7 @@
             mInvalidTokens.add(invalidTokens.get(i).toLowerCase());
         }
         if (VERBOSE_LOGGING) {
-            Log.d(TAG, "Intialized with invalid tokens: " + invalidTokens);
+            Log.d(TAG, "Initialized with invalid tokens: " + invalidTokens);
         }
     }
 
@@ -82,7 +84,8 @@
     }
 
     private void throwIfContainsToken(String token, String sql) {
-        if (mInvalidTokens.contains(token.toLowerCase())) {
+        final String lower = token.toLowerCase();
+        if (mInvalidTokens.contains(lower) || lower.startsWith(PRIVATE_PREFIX)) {
             throw genException("Detected disallowed token: " + token, sql);
         }
     }
diff --git a/tests/assets/upgradeTest/pre_upgrade1201.sql b/tests/assets/upgradeTest/pre_upgrade1201.sql
new file mode 100644
index 0000000..6a284ea
--- /dev/null
+++ b/tests/assets/upgradeTest/pre_upgrade1201.sql
@@ -0,0 +1,27 @@
+DELETE FROM accounts;
+DELETE FROM contacts;
+DELETE FROM raw_contacts;
+DELETE FROM data;
+DELETE FROM data_usage_stat;
+
+--CREATE TABLE accounts (_id INTEGER PRIMARY KEY AUTOINCREMENT,account_name TEXT, account_type TEXT, data_set TEXT);
+
+INSERT INTO "accounts" VALUES(1,NULL,NULL,NULL);
+
+--CREATE TABLE contacts (_id INTEGER PRIMARY KEY AUTOINCREMENT,name_raw_contact_id INTEGER REFERENCES raw_contacts(_id),photo_id INTEGER REFERENCES data(_id),photo_file_id INTEGER REFERENCES photo_files(_id),custom_ringtone TEXT,send_to_voicemail INTEGER NOT NULL DEFAULT 0,times_contacted INTEGER NOT NULL DEFAULT 0,last_time_contacted INTEGER,starred INTEGER NOT NULL DEFAULT 0,pinned INTEGER NOT NULL DEFAULT 0,has_phone_number INTEGER NOT NULL DEFAULT 0,lookup TEXT,status_update_id INTEGER REFERENCES data(_id),contact_last_updated_timestamp INTEGER);
+
+INSERT INTO "contacts" VALUES(1,1,NULL,NULL,NULL,0,4,9940760264,0,0,1,NULL,NULL,9940760265);
+INSERT INTO "contacts" VALUES(2,2,NULL,NULL,NULL,0,0,0,0,0,1,NULL,NULL,9940366668);
+
+--CREATE TABLE raw_contacts (_id INTEGER PRIMARY KEY AUTOINCREMENT,account_id INTEGER REFERENCES accounts(_id),sourceid TEXT,backup_id TEXT,raw_contact_is_read_only INTEGER NOT NULL DEFAULT 0,version INTEGER NOT NULL DEFAULT 1,dirty INTEGER NOT NULL DEFAULT 0,deleted INTEGER NOT NULL DEFAULT 0,metadata_dirty INTEGER NOT NULL DEFAULT 0,contact_id INTEGER REFERENCES contacts(_id),aggregation_mode INTEGER NOT NULL DEFAULT 0,aggregation_needed INTEGER NOT NULL DEFAULT 1,custom_ringtone TEXT,send_to_voicemail INTEGER NOT NULL DEFAULT 0,times_contacted INTEGER NOT NULL DEFAULT 0,last_time_contacted INTEGER,starred INTEGER NOT NULL DEFAULT 0,pinned INTEGER NOT NULL DEFAULT 0,display_name TEXT,display_name_alt TEXT,display_name_source INTEGER NOT NULL DEFAULT 0,phonetic_name TEXT,phonetic_name_style TEXT,sort_key TEXT COLLATE PHONEBOOK,phonebook_label TEXT,phonebook_bucket INTEGER,sort_key_alt TEXT COLLATE PHONEBOOK,phonebook_label_alt TEXT,phonebook_bucket_alt INTEGER,name_verified INTEGER NOT NULL DEFAULT 0,sync1 TEXT, sync2 TEXT, sync3 TEXT, sync4 TEXT );
+
+INSERT INTO "raw_contacts" VALUES(1,1,NULL,NULL,0,3,1,0,0,1,0,0,NULL,0,4,9940760264,0,0,'Test','Test',40,NULL,'0','Test','T',20,'Test','T',20,0,NULL,NULL,NULL,NULL);
+INSERT INTO "raw_contacts" VALUES(2,1,NULL,NULL,0,3,1,0,0,2,0,0,NULL,0,0,NULL,0,0,'Test2','Test2',40,NULL,'0','Test2','T',20,'Test2','T',20,0,NULL,NULL,NULL,NULL);
+
+--CREATE TABLE data (_id INTEGER PRIMARY KEY AUTOINCREMENT,package_id INTEGER REFERENCES package(_id),mimetype_id INTEGER REFERENCES mimetype(_id) NOT NULL,raw_contact_id INTEGER REFERENCES raw_contacts(_id) NOT NULL,hash_id TEXT,is_read_only INTEGER NOT NULL DEFAULT 0,is_primary INTEGER NOT NULL DEFAULT 0,is_super_primary INTEGER NOT NULL DEFAULT 0,data_version INTEGER NOT NULL DEFAULT 0,data1 TEXT,data2 TEXT,data3 TEXT,data4 TEXT,data5 TEXT,data6 TEXT,data7 TEXT,data8 TEXT,data9 TEXT,data10 TEXT,data11 TEXT,data12 TEXT,data13 TEXT,data14 TEXT,data15 TEXT,data_sync1 TEXT, data_sync2 TEXT, data_sync3 TEXT, data_sync4 TEXT, carrier_presence INTEGER NOT NULL DEFAULT 0 );
+
+INSERT INTO "data" VALUES(1,NULL,5,1,NULL,0,0,0,0,'555','2',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0);
+
+--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 INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(data_id) REFERENCES data(_id));
+
+INSERT INTO "data_usage_stat" VALUES(1,1,0,4,9940760264);
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 9b249d0..c155734 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -18,6 +18,7 @@
 
 import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
 import static com.android.providers.contacts.TestUtils.cv;
+import static com.android.providers.contacts.TestUtils.dumpCursor;
 
 import android.accounts.Account;
 import android.content.ContentProvider;
@@ -1095,6 +1096,9 @@
             assertCursorValues(c, values);
         } catch (Error e) {
             TestUtils.dumpCursor(c);
+
+            // Dump with no selection.
+            TestUtils.dumpUri(mResolver, uri);
             throw e;
         } finally {
             c.close();
@@ -1158,7 +1162,7 @@
         }
     }
 
-    private void assertCursorValuesOrderly(Cursor cursor, ContentValues... expectedValues) {
+    public static void assertCursorValuesOrderly(Cursor cursor, ContentValues... expectedValues) {
         StringBuilder message = new StringBuilder();
         cursor.moveToPosition(-1);
         for (ContentValues v : expectedValues) {
@@ -1188,7 +1192,7 @@
         return true;
     }
 
-    private boolean equalsWithExpectedValues(Cursor cursor, ContentValues expectedValues,
+    private static boolean equalsWithExpectedValues(Cursor cursor, ContentValues expectedValues,
             StringBuilder msgBuffer) {
         for (String column : expectedValues.keySet()) {
             int index = cursor.getColumnIndex(column);
@@ -1229,6 +1233,7 @@
         final Cursor cursor = mResolver.query(uri, DATA_USAGE_PROJECTION, null, null,
                 null);
         try {
+            dumpCursor(cursor);
             assertCursorHasAnyRecordMatch(cursor, cv(Data.DATA1, data1, Data.TIMES_USED, timesUsed,
                     Data.LAST_TIME_USED, lastTimeUsed));
         } finally {
@@ -1367,6 +1372,17 @@
         long timeStamp = c.getLong(index);
         assertTrue(Math.abs(time - timeStamp) < tolerance);
     }
+
+    protected Uri insertRawContact(ContentValues values) {
+        return TestUtils.insertRawContact(mResolver,
+                getContactsProvider().getDatabaseHelper(), values);
+    }
+
+    protected Uri insertProfileRawContact(ContentValues values) {
+        return TestUtils.insertProfileRawContact(mResolver,
+                getContactsProvider().getProfileProviderForTest().getDatabaseHelper(), values);
+    }
+
     /**
      * A contact in the database, and the attributes used to create it.  Construct using
      * {@link GoldenContactBuilder#build()}.
diff --git a/tests/src/com/android/providers/contacts/BaseDatabaseHelperUpgradeTest.java b/tests/src/com/android/providers/contacts/BaseDatabaseHelperUpgradeTest.java
index 73303d0..0eb12ad 100644
--- a/tests/src/com/android/providers/contacts/BaseDatabaseHelperUpgradeTest.java
+++ b/tests/src/com/android/providers/contacts/BaseDatabaseHelperUpgradeTest.java
@@ -29,7 +29,7 @@
  * Run the test like this: <code> runtest -c com.android.providers.contacts.BaseDatabaseHelperUpgradeTest
  * contactsprov </code>
  */
-public class BaseDatabaseHelperUpgradeTest extends AndroidTestCase {
+public abstract class BaseDatabaseHelperUpgradeTest extends AndroidTestCase {
 
     protected static final String INTEGER = "INTEGER";
     protected static final String TEXT = "TEXT";
@@ -168,7 +168,14 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        mDb = SQLiteDatabase.create(null);
+
+        final String filename = getDatabaseFilename();
+        if (filename == null) {
+            mDb = SQLiteDatabase.create(null);
+        } else {
+            getContext().deleteDatabase(filename);
+            mDb = SQLiteDatabase.openOrCreateDatabase(filename, null);
+        }
     }
 
     @Override
@@ -177,6 +184,8 @@
         super.tearDown();
     }
 
+    protected abstract String getDatabaseFilename();
+
     protected void assertDatabaseStructureSameAsList(TableListEntry[] list, boolean isNewDatabase) {
         for (TableListEntry entry : list) {
             if (!entry.shouldBeInNewDb) {
diff --git a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
index be3a73b..e0b5ea5 100644
--- a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
+++ b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
@@ -25,6 +25,7 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.providers.contacts.ContactsDatabaseHelper.LowRes;
 import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
@@ -432,4 +433,56 @@
             cursor.close();
         }
     }
+
+    private Integer getIntegerFromExpression(String expression) {
+        try (Cursor c = mDb.rawQuery("SELECT " + expression, null)) {
+            assertTrue(c.moveToPosition(0));
+            if (c.isNull(0)) {
+                return null;
+            }
+            return c.getInt(0);
+        }
+    }
+
+    private Integer checkGetTimesUsedExpression(Integer value) {
+        return getIntegerFromExpression(LowRes.getTimesUsedExpression(
+                value == null ? "NULL" : String.valueOf(value)));
+    }
+
+    public void testGetTimesUsedExpression() {
+        assertEquals((Object) 0, checkGetTimesUsedExpression(null));
+        assertEquals((Object) 0, checkGetTimesUsedExpression(-1));
+        assertEquals((Object) 0, checkGetTimesUsedExpression(-10));
+        assertEquals((Object) 0, checkGetTimesUsedExpression(0));
+        for (int i = 1; i < 10; i++) {
+            assertEquals("value=" + i, (Object) 1, checkGetTimesUsedExpression(i));
+        }
+        for (int i = 10; i < 20; i++) {
+            assertEquals("value=" + i, (Object) 10, checkGetTimesUsedExpression(i));
+        }
+        for (int i = 20; i < 30; i++) {
+            assertEquals("value=" + i, (Object) 20, checkGetTimesUsedExpression(i));
+        }
+
+        assertEquals((Object) 123450, checkGetTimesUsedExpression(123456));
+    }
+
+    private Integer checkGetLastTimeUsedExpression(Integer value) {
+        return getIntegerFromExpression(LowRes.getLastTimeUsedExpression(
+                value == null ? "NULL" : String.valueOf(value)));
+    }
+
+    public void testGetLastTimeUsedExpression() {
+        assertEquals((Object) null, checkGetLastTimeUsedExpression(null));
+        assertEquals((Object) 0, checkGetLastTimeUsedExpression(0));
+        assertEquals((Object) 0, checkGetLastTimeUsedExpression(1));
+        assertEquals((Object) 0, checkGetLastTimeUsedExpression(86399));
+        assertEquals((Object) 86400, checkGetLastTimeUsedExpression(86400));
+
+        for (int i = 1; i < 3; i++) {
+            assertEquals((Object) (86400 * i), checkGetLastTimeUsedExpression(86400 * i));
+            assertEquals((Object) (86400 * i), checkGetLastTimeUsedExpression(86400 * i + 1));
+            assertEquals((Object) (86400 * i), checkGetLastTimeUsedExpression(86400 * i + 86399));
+        }
+    }
 }
diff --git a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java
index d543b7e..185fa03 100644
--- a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java
+++ b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java
@@ -16,6 +16,11 @@
 
 package com.android.providers.contacts;
 
+import static com.android.providers.contacts.TestUtils.createDatabaseSnapshot;
+import static com.android.providers.contacts.TestUtils.cv;
+import static com.android.providers.contacts.TestUtils.executeSqlFromAssetFile;
+
+import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.provider.BaseColumns;
 import android.provider.CallLog.Calls;
@@ -84,9 +89,12 @@
 
     private static final String CONTACTS2_DB_1108_ASSET_NAME = "upgradeTest/contacts2_1108.sql";
 
+    /**
+     * The helper instance.  Note we just use it to call the upgrade method.  The database
+     * hold by this instance is not used in this test.
+     */
     private ContactsDatabaseHelper mHelper;
 
-
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -101,6 +109,11 @@
         super.tearDown();
     }
 
+    @Override
+    protected String getDatabaseFilename() {
+        return TestUtils.getContactsDatabaseFilename(getContext(), "-upgrade-test");
+    }
+
     public void testDatabaseCreate() {
         mHelper.onCreate(mDb);
         assertDatabaseStructureSameAsList(TABLE_LIST, /* isNewDatabase =*/ true);
@@ -108,7 +121,10 @@
 
     public void testDatabaseUpgrade_UpgradeToCurrent() {
         create1108(mDb);
-        mHelper.onUpgrade(mDb, 1108, mHelper.DATABASE_VERSION);
+        int oldVersion = upgrade(1108, 1200);
+        oldVersion = upgradeTo1201(oldVersion);
+        oldVersion = upgrade(oldVersion, ContactsDatabaseHelper.DATABASE_VERSION);
+
         assertDatabaseStructureSameAsList(TABLE_LIST, /* isNewDatabase =*/ false);
     }
 
@@ -138,8 +154,62 @@
         return MY_VERSION;
     }
 
+    private int upgradeTo1201(int upgradeFrom) {
+        final int MY_VERSION = 1201;
+
+        executeSqlFromAssetFile(getTestContext(), mDb, "upgradeTest/pre_upgrade1201.sql");
+
+        mHelper.onUpgrade(mDb, upgradeFrom, MY_VERSION);
+
+        try (Cursor c = mDb.rawQuery("select * from contacts order by _id", null)) {
+            BaseContactsProvider2Test.assertCursorValuesOrderly(c,
+                    cv(Contacts._ID, 1,
+                            "last_time_contacted", 0,
+                            "x_last_time_contacted", 9940760264L,
+                            "times_contacted", 0,
+                            "x_times_contacted", 4
+                            ),
+                    cv(
+                            "last_time_contacted", 0,
+                            "x_last_time_contacted", 0,
+                            "times_contacted", 0,
+                            "x_times_contacted", 0
+                    ));
+        }
+
+        try (Cursor c = mDb.rawQuery("select * from raw_contacts order by _id", null)) {
+            BaseContactsProvider2Test.assertCursorValuesOrderly(c,
+                    cv("_id", 1,
+                            "last_time_contacted", 0,
+                            "x_last_time_contacted", 9940760264L,
+                            "times_contacted", 0,
+                            "x_times_contacted", 4
+                    ),
+                    cv(
+                            "last_time_contacted", 0,
+                            "x_last_time_contacted", 0,
+                            "times_contacted", 0,
+                            "x_times_contacted", 0
+                    ));
+        }
+
+        try (Cursor c = mDb.rawQuery("select * from data_usage_stat", null)) {
+            BaseContactsProvider2Test.assertCursorValuesOrderly(c,
+                    cv(
+                            "last_time_used", 0,
+                            "x_last_time_used", 9940760264L,
+                            "times_used", 0,
+                            "x_times_used", 4
+                    ));
+        }
+
+        return MY_VERSION;
+    }
+
     private int upgrade(int upgradeFrom, int upgradeTo) {
-        mHelper.onUpgrade(mDb, upgradeFrom, upgradeTo);
+        if (upgradeFrom < upgradeTo) {
+            mHelper.onUpgrade(mDb, upgradeFrom, upgradeTo);
+        }
         return upgradeTo;
     }
 
@@ -148,15 +218,7 @@
      * incrementally from this version.
      */
     private void create1108(SQLiteDatabase db) {
-        try (InputStream input = getTestContext().getAssets().open(CONTACTS2_DB_1108_ASSET_NAME);) {
-            BufferedReader r = new BufferedReader(new InputStreamReader(input));
-            String query;
-            while ((query = r.readLine()) != null) {
-                db.execSQL(query);
-            }
-        } catch (IOException e) {
-            throw new RuntimeException(e.toString());
-        }
+        executeSqlFromAssetFile(getTestContext(), db, CONTACTS2_DB_1108_ASSET_NAME);
     }
 
     /**
@@ -181,8 +243,10 @@
             new TableColumn(Contacts.PHOTO_FILE_ID, INTEGER, false, null),
             new TableColumn(Contacts.CUSTOM_RINGTONE, TEXT, false, null),
             new TableColumn(Contacts.SEND_TO_VOICEMAIL, INTEGER, true, "0"),
-            new TableColumn(Contacts.TIMES_CONTACTED, INTEGER, true, "0"),
-            new TableColumn(Contacts.LAST_TIME_CONTACTED, INTEGER, false, null),
+            new TableColumn(Contacts.RAW_TIMES_CONTACTED, INTEGER, true, "0"),
+            new TableColumn(Contacts.RAW_LAST_TIME_CONTACTED, INTEGER, false, null),
+            new TableColumn(Contacts.LR_TIMES_CONTACTED, INTEGER, true, "0"),
+            new TableColumn(Contacts.LR_LAST_TIME_CONTACTED, INTEGER, false, null),
             new TableColumn(Contacts.STARRED, INTEGER, true, "0"),
             new TableColumn(Contacts.PINNED, INTEGER, true,
                     String.valueOf(PinnedPositions.UNPINNED)),
@@ -213,8 +277,10 @@
             new TableColumn(RawContactsColumns.AGGREGATION_NEEDED, INTEGER, true, "1"),
             new TableColumn(RawContacts.CUSTOM_RINGTONE, TEXT, false, null),
             new TableColumn(RawContacts.SEND_TO_VOICEMAIL, INTEGER, true, "0"),
-            new TableColumn(RawContacts.TIMES_CONTACTED, INTEGER, true, "0"),
-            new TableColumn(RawContacts.LAST_TIME_CONTACTED, INTEGER, false, null),
+            new TableColumn(RawContacts.RAW_TIMES_CONTACTED, INTEGER, true, "0"),
+            new TableColumn(RawContacts.RAW_LAST_TIME_CONTACTED, INTEGER, false, null),
+            new TableColumn(RawContacts.LR_TIMES_CONTACTED, INTEGER, true, "0"),
+            new TableColumn(RawContacts.LR_LAST_TIME_CONTACTED, INTEGER, false, null),
             new TableColumn(RawContacts.STARRED, INTEGER, true, "0"),
             new TableColumn(RawContacts.PINNED, INTEGER, true,
                     String.valueOf(PinnedPositions.UNPINNED)),
@@ -464,8 +530,10 @@
             new TableColumn(DataUsageStatColumns._ID, INTEGER, false, null),
             new TableColumn(DataUsageStatColumns.DATA_ID, INTEGER, true, null),
             new TableColumn(DataUsageStatColumns.USAGE_TYPE_INT, INTEGER, true, "0"),
-            new TableColumn(DataUsageStatColumns.TIMES_USED, INTEGER, true, "0"),
-            new TableColumn(DataUsageStatColumns.LAST_TIME_USED, INTEGER, true, "0"),
+            new TableColumn(DataUsageStatColumns.RAW_TIMES_USED, INTEGER, true, "0"),
+            new TableColumn(DataUsageStatColumns.RAW_LAST_TIME_USED, INTEGER, true, "0"),
+            new TableColumn(DataUsageStatColumns.LR_TIMES_USED, INTEGER, true, "0"),
+            new TableColumn(DataUsageStatColumns.LR_LAST_TIME_USED, INTEGER, true, "0"),
     };
 
     private static final TableColumn[] METADATA_SYNC_COLUMNS = new TableColumn[] {
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index f951f6d..8ba7f8c 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -16,7 +16,9 @@
 
 package com.android.providers.contacts;
 
+import static com.android.providers.contacts.TestUtils.createDatabaseSnapshot;
 import static com.android.providers.contacts.TestUtils.cv;
+import static com.android.providers.contacts.TestUtils.dumpCursor;
 
 import android.accounts.Account;
 import android.content.ContentProviderOperation;
@@ -254,8 +256,8 @@
                 Contacts.CONTACT_STATUS_LABEL,
                 Contacts.CONTACT_STATUS_ICON,
                 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
-                DataUsageStatColumns.TIMES_USED,
-                DataUsageStatColumns.LAST_TIME_USED,
+                DataUsageStatColumns.LR_TIMES_USED,
+                DataUsageStatColumns.LR_LAST_TIME_USED,
         });
     }
 
@@ -299,8 +301,8 @@
                 Contacts.CONTACT_STATUS_LABEL,
                 Contacts.CONTACT_STATUS_ICON,
                 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
-                DataUsageStatColumns.TIMES_USED,
-                DataUsageStatColumns.LAST_TIME_USED,
+                DataUsageStatColumns.LR_TIMES_USED,
+                DataUsageStatColumns.LR_LAST_TIME_USED,
                 Phone.NUMBER,
                 Phone.TYPE,
                 Phone.LABEL,
@@ -650,8 +652,8 @@
                 Contacts.CONTACT_STATUS_ICON,
                 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
                 GroupMembership.GROUP_SOURCE_ID,
-                DataUsageStatColumns.TIMES_USED,
-                DataUsageStatColumns.LAST_TIME_USED,
+                Contacts.Entity.TIMES_USED,
+                Contacts.Entity.LAST_TIME_USED,
         });
     }
 
@@ -958,7 +960,8 @@
         values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
         values.put(RawContacts.CUSTOM_RINGTONE, "d");
         values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
-        values.put(RawContacts.LAST_TIME_CONTACTED, 12345);
+        values.put(RawContacts.LAST_TIME_CONTACTED, 86400 + 123);
+        values.put(RawContacts.TIMES_CONTACTED, 12);
         values.put(RawContacts.STARRED, 1);
         values.put(RawContacts.SYNC1, "e");
         values.put(RawContacts.SYNC2, "f");
@@ -968,6 +971,9 @@
         Uri rowUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         long rawContactId = ContentUris.parseId(rowUri);
 
+        values.put(RawContacts.LAST_TIME_CONTACTED, 86400);
+        values.put(RawContacts.TIMES_CONTACTED, 0); // This is not insertable.
+
         assertStoredValues(rowUri, values);
         assertSelection(RawContacts.CONTENT_URI, values, RawContacts._ID, rawContactId);
         assertNetworkNotified(true);
@@ -1290,11 +1296,11 @@
         ContentValues values = new ContentValues();
         values.put(RawContacts.CUSTOM_RINGTONE, "d");
         values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
-        values.put(RawContacts.LAST_TIME_CONTACTED, 12345);
+        values.put(RawContacts.LAST_TIME_CONTACTED, 86400 + 5);
         values.put(RawContacts.TIMES_CONTACTED, 54321);
         values.put(RawContacts.STARRED, 1);
 
-        Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
+        Uri rawContactUri = insertRawContact(values);
         long rawContactId = ContentUris.parseId(rawContactUri);
 
         DataUtil.insertStructuredName(mResolver, rawContactId, "Meghan", "Knox");
@@ -1314,8 +1320,8 @@
         values.put(Contacts.DISPLAY_NAME, "Meghan Knox");
         values.put(Contacts.CUSTOM_RINGTONE, "d");
         values.put(Contacts.SEND_TO_VOICEMAIL, 1);
-        values.put(Contacts.LAST_TIME_CONTACTED, 12345);
-        values.put(Contacts.TIMES_CONTACTED, 54321);
+        values.put(Contacts.LAST_TIME_CONTACTED, 86400);
+        values.put(Contacts.TIMES_CONTACTED, 54320);
         values.put(Contacts.STARRED, 1);
 
         assertStoredValues(ContentUris.withAppendedId(Phone.CONTENT_URI, phoneId), values);
@@ -2476,11 +2482,11 @@
         ContentValues values = new ContentValues();
         values.put(RawContacts.CUSTOM_RINGTONE, "d");
         values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
-        values.put(RawContacts.LAST_TIME_CONTACTED, 12345);
+        values.put(RawContacts.LAST_TIME_CONTACTED, 86400 + 5);
         values.put(RawContacts.TIMES_CONTACTED, 54321);
         values.put(RawContacts.STARRED, 1);
 
-        Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
+        Uri rawContactUri = insertRawContact(values);
         final long rawContactId = ContentUris.parseId(rawContactUri);
 
         DataUtil.insertStructuredName(mResolver, rawContactId, "Meghan", "Knox");
@@ -2499,8 +2505,8 @@
         values.put(Contacts.DISPLAY_NAME, "Meghan Knox");
         values.put(Contacts.CUSTOM_RINGTONE, "d");
         values.put(Contacts.SEND_TO_VOICEMAIL, 1);
-        values.put(Contacts.LAST_TIME_CONTACTED, 12345);
-        values.put(Contacts.TIMES_CONTACTED, 54321);
+        values.put(Contacts.LAST_TIME_CONTACTED, 86400);
+        values.put(Contacts.TIMES_CONTACTED, 54320);
         values.put(Contacts.STARRED, 1);
 
         assertStoredValues(Email.CONTENT_URI, values);
@@ -2855,7 +2861,7 @@
 
     /**
      * Tests {@link DataUsageFeedback} correctly bucketize contacts using each
-     * {@link DataUsageStatColumns#LAST_TIME_USED}
+     * {@link DataUsageStatColumns#RAW_LAST_TIME_USED}
      */
     public void testEmailFilterSortOrderWithOldHistory() {
         long rawContactId1 = RawContactUtil.createRawContact(mResolver);
@@ -3010,7 +3016,8 @@
         assertStoredValue(dataUri2, Data.IS_SUPER_PRIMARY, 0);
         final Uri dataUriWithUsageType = Data.CONTENT_URI.buildUpon().appendQueryParameter(
                 DataUsageFeedback.USAGE_TYPE, usageTypeString).build();
-        assertDataUsageCursorContains(dataUriWithUsageType, emailAddress, timesUsed, lastTimeUsed);
+        assertDataUsageCursorContains(dataUriWithUsageType, emailAddress, 1,
+                1111111 / 86400 * 86400); // Returned values are lowres.
 
         // Update AggregationException table.
         RawContactInfo aggregationContact = new RawContactInfo(
@@ -3475,6 +3482,7 @@
                 StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO);
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
 
+        values.put(Contacts.TIMES_CONTACTED, 1); // low res.
         assertStoredValues(contactUri, values);
         assertSelection(Contacts.CONTENT_URI, values, Contacts._ID, contactId);
     }
@@ -3487,6 +3495,9 @@
         values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
         values.put(Contacts.CONTACT_CHAT_CAPABILITY, StatusUpdates.CAPABILITY_HAS_CAMERA);
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+        values.put(Contacts.TIMES_CONTACTED, 1); // low res.
+
         assertStoredValuesWithProjection(contactUri, values);
         assertSelectionWithProjection(Contacts.CONTENT_URI, values, Contacts._ID, contactId);
     }
@@ -3509,6 +3520,7 @@
         values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
 
         Uri filterUri1 = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, "goulash");
+        values.put(Contacts.TIMES_CONTACTED, 1); // low res.
         assertStoredValuesWithProjection(filterUri1, values);
 
         assertContactFilter(contactId, "goolash");
@@ -3543,6 +3555,7 @@
         values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
 
         Uri filterUri1 = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, "goog411@acme.com");
+        values.put(Contacts.TIMES_CONTACTED, 1); // low res.
         assertStoredValuesWithProjection(filterUri1, values);
 
         assertContactFilter(contactId, "goog");
@@ -3569,6 +3582,7 @@
         values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
 
         Uri filterUri1 = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, "18004664411");
+        values.put(Contacts.TIMES_CONTACTED, 1); // low res.
         assertStoredValuesWithProjection(filterUri1, values);
 
         assertContactFilter(contactId, "18004664411");
@@ -3600,7 +3614,7 @@
                 StatusUpdates.CAPABILITY_HAS_CAMERA);
         ContentValues values3 = new ContentValues();
         final String phoneNumber3 = "18004664413";
-        final int timesContacted3 = 5;
+        final int timesContacted3 = 9;
         createContact(values3, "Lotta", "Calling", phoneNumber3,
                 "c@acme.com", StatusUpdates.AWAY, timesContacted3, 0, 0,
                 StatusUpdates.CAPABILITY_HAS_VIDEO);
@@ -3616,6 +3630,8 @@
         // Send feedback for the 3rd phone number, pretending we called that person via phone.
         sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
 
+        values3.put(Contacts.TIMES_CONTACTED, 10); // Low res.
+
         // After the feedback, 3rd contact should be shown after starred one.
         assertStoredValuesOrderly(Contacts.CONTENT_STREQUENT_URI,
                 new ContentValues[] { values4, values3 });
@@ -3625,6 +3641,7 @@
         sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1);
 
         // After the feedback, 1st and 3rd contacts should be shown after starred one.
+        values1.put(Contacts.TIMES_CONTACTED, 1); // Low res.
         assertStoredValuesOrderly(Contacts.CONTENT_STREQUENT_URI,
                 new ContentValues[] { values4, values1, values3 });
 
@@ -3659,6 +3676,10 @@
         final ContentValues values6 = new ContentValues(values4);
         values6.put(Phone.NUMBER, phoneNumber6);
 
+        values4.put(Contacts.TIMES_CONTACTED, 1); // Low res.
+        values5.put(Contacts.TIMES_CONTACTED, 1); // Low res.
+        values6.put(Contacts.TIMES_CONTACTED, 1); // Low res.
+
         // Phone only strequent should return all phone numbers belonging to the 4th contact,
         // and then contact 3.
         assertStoredValuesOrderly(phoneOnlyStrequentUri, new ContentValues[] {values5, values6,
@@ -3667,6 +3688,8 @@
         // Send feedback for the 2rd phone number, pretending we send the person a SMS message.
         sendFeedback(phoneNumber2, DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, values1);
 
+        values1.put(Contacts.TIMES_CONTACTED, 1); // Low res.
+
         // SMS feedback shouldn't affect phone-only results.
         assertStoredValuesOrderly(phoneOnlyStrequentUri, new ContentValues[] {values5, values6,
                 values4, values3});
@@ -3749,6 +3772,8 @@
 
         // Contact cid1 again, but it's an email, not a phone call.
         updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1e);
+        updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1e);
+        updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1e);
 
         // Contacts in this bucket are considered more than 3 days old
         sMockClock.setCurrentTimeMillis(fourDaysAgoInMillis);
@@ -3821,12 +3846,15 @@
         sendFeedback(email2, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values2);
         sendFeedback(email2, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values2);
 
+        values1.put(Contacts.TIMES_CONTACTED, 1); // low res.
+        values2.put(Contacts.TIMES_CONTACTED, 1); // low res.
         assertStoredValues(Contacts.CONTENT_FREQUENT_URI, new ContentValues[] {values2, values1});
 
-        // Three times
-        sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
-        sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
-        sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
+        for (int i = 0; i < 10; i++) {
+            sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
+        }
+
+        values3.put(Contacts.TIMES_CONTACTED, 10); // low res.
 
         assertStoredValues(Contacts.CONTENT_FREQUENT_URI,
                 new ContentValues[] {values3, values2, values1});
@@ -3887,34 +3915,51 @@
 
         sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1);
 
-        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 1, 100);
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 1, 0);
 
-        sMockClock.setCurrentTimeMillis(111);
+        sMockClock.setCurrentTimeMillis(86400 + 123);
         sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1);
 
-        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 2, 111);
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 1, 86400);
 
-        sMockClock.setCurrentTimeMillis(123);
-        sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, values1);
+        sMockClock.setCurrentTimeMillis(86400 * 3 + 123);
+        for (int i = 0; i < 11; i++) {
+            sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, values1);
+        }
 
-        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 3, 123);
+        // Note here, "a@acme.com" has two data stats rows, 2 and 11.  What we get here's the sum
+        // of the lowres values, so # times will be 11, instead of 10 (which is the lowres of the
+        // sum).
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 11, 86400 * 3);
 
         final Uri dataUriWithUsageTypeLongText = Data.CONTENT_URI.buildUpon().appendQueryParameter(
                 DataUsageFeedback.USAGE_TYPE, DataUsageFeedback.USAGE_TYPE_LONG_TEXT).build();
 
-        assertDataUsageCursorContains(dataUriWithUsageTypeLongText, "a@acme.com", 2, 111);
+        assertDataUsageCursorContains(dataUriWithUsageTypeLongText, "a@acme.com", 1, 86400 * 1);
 
-        sMockClock.setCurrentTimeMillis(200);
+        sMockClock.setCurrentTimeMillis(86400 * 4 + 123);
         sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
         sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
         sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
 
-        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 6, 200);
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 12, 86400 * 4);
+
+        sMockClock.setCurrentTimeMillis(86400 * 5 + 123);
+        for (int i = 0; i < 10; i++) {
+            sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
+        }
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 21, 86400 * 5);
+
+        sMockClock.setCurrentTimeMillis(86400 * 6 + 123);
+        for (int i = 0; i < 10; i++) {
+            sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
+        }
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 31, 86400 * 6);
 
         final Uri dataUriWithUsageTypeCall = Data.CONTENT_URI.buildUpon().appendQueryParameter(
                 DataUsageFeedback.USAGE_TYPE, DataUsageFeedback.USAGE_TYPE_CALL).build();
 
-        assertDataUsageCursorContains(dataUriWithUsageTypeCall, "a@acme.com", 3, 200);
+        assertDataUsageCursorContains(dataUriWithUsageTypeCall, "a@acme.com", 20, 86400 * 6);
     }
 
     public void testQueryContactGroup() {
@@ -3934,6 +3979,7 @@
         Cursor c = mResolver.query(filterUri1, null, null, null, Contacts._ID);
         assertEquals(1, c.getCount());
         c.moveToFirst();
+        dumpCursor(c);
         assertCursorValues(c, values1);
         c.close();
 
@@ -4115,6 +4161,9 @@
         long nonProfileRawContactId = createBasicNonProfileContact(nonProfileValues);
         long nonProfileContactId = queryContactId(nonProfileRawContactId);
 
+        nonProfileValues.put(Contacts.TIMES_CONTACTED, 1); // low res.
+        profileValues.put(Contacts.TIMES_CONTACTED, 1); // low res.
+
         assertStoredValues(Contacts.CONTENT_URI, nonProfileValues);
         assertSelection(Contacts.CONTENT_URI, nonProfileValues, Contacts._ID, nonProfileContactId);
 
@@ -4128,7 +4177,7 @@
         // Create a non-profile contact - this should be returned.
         ContentValues nonProfileValues = new ContentValues();
         createBasicNonProfileContact(nonProfileValues);
-
+        nonProfileValues.put(Contacts.TIMES_CONTACTED, 1); // low res
         assertStoredValues(Contacts.CONTENT_URI, new ContentValues[] {nonProfileValues});
     }
 
@@ -4136,6 +4185,7 @@
         ContentValues profileValues = new ContentValues();
         createBasicProfileContact(profileValues);
 
+        profileValues.put(Contacts.TIMES_CONTACTED, 1); // low res
         assertStoredValues(Profile.CONTENT_URI, profileValues);
     }
 
@@ -4184,6 +4234,7 @@
 
         // The raw contact view doesn't include the photo ID.
         profileValues.remove(Contacts.PHOTO_ID);
+        profileValues.put(Contacts.TIMES_CONTACTED, 1); // low res
         assertStoredValues(Profile.CONTENT_RAW_CONTACTS_URI, profileValues);
     }
 
@@ -4193,6 +4244,7 @@
 
         // The raw contact view doesn't include the photo ID.
         profileValues.remove(Contacts.PHOTO_ID);
+        profileValues.put(Contacts.TIMES_CONTACTED, 1); // low res
         assertStoredValues(ContentUris.withAppendedId(
                 Profile.CONTENT_RAW_CONTACTS_URI, profileRawContactId), profileValues);
     }
@@ -8747,8 +8799,7 @@
         // Now, there's a single frequent.  (contact 1)
         assertRowCount(1, Contacts.CONTENT_STREQUENT_URI, null, null);
 
-        // time = startTime + 1
-        sMockClock.advance();
+        sMockClock.advanceDay();
 
         // Test 2. touch data 1a, 2a and 3a
         updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1a, did2a, did3a);
@@ -8756,8 +8807,7 @@
         // Now, contact 1 and 3 are in frequent.
         assertRowCount(2, Contacts.CONTENT_STREQUENT_URI, null, null);
 
-        // time = startTime + 2
-        sMockClock.advance();
+        sMockClock.advanceDay();
 
         // Test 2. touch data 2p (call)
         updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_CALL, did2p);
@@ -8765,71 +8815,70 @@
         // There're still two frequent.
         assertRowCount(2, Contacts.CONTENT_STREQUENT_URI, null, null);
 
-        // time = startTime + 3
-        sMockClock.advance();
+        sMockClock.advanceDay();
 
         // Test 3. touch data 2p and 3p (short text)
         updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, did2p, did3p);
 
         // Let's check the tables.
-
+// TODO more tests?
         // Fist, check the data_usage_stat table, which has no public URI.
         assertStoredValuesDb("SELECT " + DataUsageStatColumns.DATA_ID +
                 "," + DataUsageStatColumns.USAGE_TYPE_INT +
-                "," + DataUsageStatColumns.TIMES_USED +
-                "," + DataUsageStatColumns.LAST_TIME_USED +
+                "," + DataUsageStatColumns.RAW_TIMES_USED +
+                "," + DataUsageStatColumns.RAW_LAST_TIME_USED +
                 " FROM " + Tables.DATA_USAGE_STAT, null,
                 cv(DataUsageStatColumns.DATA_ID, did1a,
                         DataUsageStatColumns.USAGE_TYPE_INT,
                             DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT,
-                        DataUsageStatColumns.TIMES_USED, 2,
-                        DataUsageStatColumns.LAST_TIME_USED, startTime + 1
+                        DataUsageStatColumns.RAW_TIMES_USED, 2,
+                        DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400
                         ),
                 cv(DataUsageStatColumns.DATA_ID, did2a,
                         DataUsageStatColumns.USAGE_TYPE_INT,
                             DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT,
-                        DataUsageStatColumns.TIMES_USED, 1,
-                        DataUsageStatColumns.LAST_TIME_USED, startTime + 1
+                        DataUsageStatColumns.RAW_TIMES_USED, 1,
+                        DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400
                         ),
                 cv(DataUsageStatColumns.DATA_ID, did3a,
                         DataUsageStatColumns.USAGE_TYPE_INT,
                             DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT,
-                        DataUsageStatColumns.TIMES_USED, 1,
-                        DataUsageStatColumns.LAST_TIME_USED, startTime + 1
+                        DataUsageStatColumns.RAW_TIMES_USED, 1,
+                        DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400
                         ),
                 cv(DataUsageStatColumns.DATA_ID, did2p,
                         DataUsageStatColumns.USAGE_TYPE_INT,
                             DataUsageStatColumns.USAGE_TYPE_INT_CALL,
-                        DataUsageStatColumns.TIMES_USED, 1,
-                        DataUsageStatColumns.LAST_TIME_USED, startTime + 2
+                        DataUsageStatColumns.RAW_TIMES_USED, 1,
+                        DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400 * 2
                         ),
                 cv(DataUsageStatColumns.DATA_ID, did2p,
                         DataUsageStatColumns.USAGE_TYPE_INT,
                             DataUsageStatColumns.USAGE_TYPE_INT_SHORT_TEXT,
-                        DataUsageStatColumns.TIMES_USED, 1,
-                        DataUsageStatColumns.LAST_TIME_USED, startTime + 3
+                        DataUsageStatColumns.RAW_TIMES_USED, 1,
+                        DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400 * 3
                         ),
                 cv(DataUsageStatColumns.DATA_ID, did3p,
                         DataUsageStatColumns.USAGE_TYPE_INT,
                             DataUsageStatColumns.USAGE_TYPE_INT_SHORT_TEXT,
-                        DataUsageStatColumns.TIMES_USED, 1,
-                        DataUsageStatColumns.LAST_TIME_USED, startTime + 3
+                        DataUsageStatColumns.RAW_TIMES_USED, 1,
+                        DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400 * 3
                         )
                 );
 
         // Next, check the raw_contacts table
         assertStoredValuesWithProjection(RawContacts.CONTENT_URI,
                 cv(RawContacts._ID, rid1,
-                        RawContacts.TIMES_CONTACTED, 2,
-                        RawContacts.LAST_TIME_CONTACTED, startTime + 1
+                        RawContacts.TIMES_CONTACTED, 1,
+                        RawContacts.LAST_TIME_CONTACTED, (startTime + 86400) / 86400 * 86400
                         ),
                 cv(RawContacts._ID, rid2,
-                        RawContacts.TIMES_CONTACTED, 3,
-                        RawContacts.LAST_TIME_CONTACTED, startTime + 3
+                        RawContacts.TIMES_CONTACTED, 1,
+                        RawContacts.LAST_TIME_CONTACTED, (startTime + 86400 * 3) / 86400 * 86400
                         ),
                 cv(RawContacts._ID, rid3,
-                        RawContacts.TIMES_CONTACTED, 2,
-                        RawContacts.LAST_TIME_CONTACTED, startTime + 3
+                        RawContacts.TIMES_CONTACTED, 1,
+                        RawContacts.LAST_TIME_CONTACTED, (startTime + 86400 * 3) / 86400 * 86400
                         ),
                 cv(RawContacts._ID, rid4,
                         RawContacts.TIMES_CONTACTED, 0,
@@ -8844,16 +8893,16 @@
         // at once.
         assertStoredValuesWithProjection(Contacts.CONTENT_URI,
                 cv(Contacts._ID, cid1,
-                        Contacts.TIMES_CONTACTED, 4,
-                        Contacts.LAST_TIME_CONTACTED, startTime + 3
+                        Contacts.TIMES_CONTACTED, 1,
+                        Contacts.LAST_TIME_CONTACTED, (startTime + 86400 * 3) / 86400 * 86400
                         ),
                 cv(Contacts._ID, cid3,
-                        Contacts.TIMES_CONTACTED, 2,
-                        Contacts.LAST_TIME_CONTACTED, startTime + 3
+                        Contacts.TIMES_CONTACTED, 1,
+                        Contacts.LAST_TIME_CONTACTED, (startTime + 86400 * 3) / 86400 * 86400
                         ),
                 cv(Contacts._ID, cid4,
                         Contacts.TIMES_CONTACTED, 0,
-                        Contacts.LAST_TIME_CONTACTED, 0 // For contacts, the default is 0, not null.
+                        Contacts.LAST_TIME_CONTACTED, 0
                         )
                 );
 
@@ -9688,10 +9737,13 @@
         values.put(RawContacts.CUSTOM_RINGTONE, "beethoven5");
         values.put(RawContacts.TIMES_CONTACTED, timesContacted);
 
-        Uri insertionUri = isUserProfile
-                ? Profile.CONTENT_RAW_CONTACTS_URI
-                : RawContacts.CONTENT_URI;
-        Uri rawContactUri = mResolver.insert(insertionUri, values);
+        Uri rawContactUri;
+        if (isUserProfile) {
+            rawContactUri = insertProfileRawContact(values);
+        } else {
+            rawContactUri = insertRawContact(values);
+        }
+
         long rawContactId = ContentUris.parseId(rawContactUri);
         Uri photoUri = insertPhoto(rawContactId);
         long photoId = ContentUris.parseId(photoUri);
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2TransactionTest.java b/tests/src/com/android/providers/contacts/ContactsProvider2TransactionTest.java
index 6a82bf9..03c9e75 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2TransactionTest.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2TransactionTest.java
@@ -60,7 +60,7 @@
      */
     public void testTransactionCallback_insert() {
 
-        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 12345);
+        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);
 
         // Insert a raw contact.
         mProvider.resetTrasactionCallbackCalledFlags();
@@ -87,14 +87,14 @@
      */
     public void testTransactionCallback_update() {
 
-        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 12345);
+        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);
 
         // Make sure to create a raw contact and a profile raw contact.
         mResolver.insert(RawContacts.CONTENT_URI, values);
         mResolver.insert(Profile.CONTENT_RAW_CONTACTS_URI, values);
 
         values.clear();
-        values.put(RawContacts.LAST_TIME_CONTACTED, 99999);
+        values.put(RawContacts.LAST_TIME_CONTACTED, 86400 * 2);
 
         // Update all raw contacts.
         mProvider.resetTrasactionCallbackCalledFlags();
@@ -121,7 +121,7 @@
      */
     public void testTransactionCallback_delete() {
 
-        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 12345);
+        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);
 
         // Make sure to create a raw contact and a profile raw contact.
         mResolver.insert(RawContacts.CONTENT_URI, values);
@@ -150,7 +150,7 @@
      */
     public void testTransactionCallback_bulkInsert() {
 
-        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 12345);
+        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);
 
         // Insert a raw contact.
         mProvider.resetTrasactionCallbackCalledFlags();
@@ -179,7 +179,7 @@
         ContentProviderOperation.Builder b;
         b = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
         b.withValue(RawContacts.STARRED, 1);
-        b.withValue(RawContacts.TIMES_CONTACTED, 200001);
+        b.withValue(RawContacts.LAST_TIME_CONTACTED, 86400 * 21);
         ops.add(b.build());
 
         b = ContentProviderOperation.newInsert(Data.CONTENT_URI);
@@ -197,7 +197,7 @@
     private void checkStoredContact() {
         assertStoredValues(Contacts.CONTENT_URI, cv(
                 Contacts.DISPLAY_NAME, "Regular Contact",
-                RawContacts.TIMES_CONTACTED, 200001
+                RawContacts.LAST_TIME_CONTACTED, 86400 * 21
                 ));
     }
 
@@ -208,7 +208,7 @@
         ContentProviderOperation.Builder b;
         b = ContentProviderOperation.newInsert(Profile.CONTENT_RAW_CONTACTS_URI);
         b.withValue(RawContacts.STARRED, 1);
-        b.withValue(RawContacts.TIMES_CONTACTED, 100001);
+        b.withValue(RawContacts.LAST_TIME_CONTACTED, 86400 * 11);
         ops.add(b.build());
 
         b = ContentProviderOperation.newInsert(Data.CONTENT_URI);
@@ -227,7 +227,7 @@
     private void checkStoredProfile() {
         assertStoredValues(Profile.CONTENT_URI, cv(
                 Contacts.DISPLAY_NAME, "Profile Contact",
-                RawContacts.TIMES_CONTACTED, 100001
+                RawContacts.LAST_TIME_CONTACTED, 86400 * 11
                 ));
     }
 
diff --git a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
index 378c9eb..21d148c 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
@@ -16,6 +16,9 @@
 
 package com.android.providers.contacts;
 
+import static com.android.providers.contacts.TestUtils.dumpTable;
+import static com.android.providers.contacts.TestUtils.dumpUri;
+
 import android.app.SearchManager;
 import android.content.ContentProvider;
 import android.content.ContentUris;
@@ -64,12 +67,23 @@
         return Contacts.AUTHORITY + ";" + ContactsContract.AUTHORITY;
     }
 
+    private static ContentValues noStats(ContentValues v) {
+        final ContentValues ret = new ContentValues(v);
+        ret.put(People.TIMES_CONTACTED, 0);
+        ret.put(People.LAST_TIME_CONTACTED, 0);
+        return ret;
+    }
+
     public void testPeopleInsert() {
         ContentValues values = new ContentValues();
         putContactValues(values);
 
         Uri uri = mResolver.insert(People.CONTENT_URI, values);
+
+        values = noStats(values);
+
         assertStoredValues(uri, values);
+
         assertSelection(People.CONTENT_URI, values, "people", People._ID, ContentUris.parseId(uri));
     }
 
@@ -78,6 +92,7 @@
         putContactValues(values);
 
         Uri uri = mResolver.insert(People.CONTENT_URI, values);
+        values = noStats(values);
         long personId = ContentUris.parseId(uri);
         assertStoredValues(uri, values);
         assertSelection(People.CONTENT_URI, values, "people", People._ID, personId);
@@ -85,11 +100,13 @@
         values.clear();
         putContactValues2(values);
         mResolver.update(uri, values, null, null);
+        values = noStats(values);
         assertStoredValues(uri, values);
 
         values.clear();
         putContactValues(values);
         mResolver.update(People.CONTENT_URI, values, People._ID + "=" + personId, null);
+        values = noStats(values);
         assertStoredValues(uri, values);
     }
 
@@ -207,6 +224,7 @@
 
         values.clear();
         putContactValuesExceptName(values);
+        values = noStats(values);
         values.put(People.PRIMARY_PHONE_ID, ContentUris.parseId(phoneUri1));
         assertStoredValues(phoneUri2, values);
 
@@ -279,6 +297,7 @@
 
         values.clear();
         putContactValuesExceptName(values);
+        values = noStats(values);
         values.put(People.PRIMARY_EMAIL_ID, ContentUris.parseId(emailUri1));
         assertStoredValues(emailUri2, values);
 
@@ -314,9 +333,9 @@
         int timesContactedAfter =
             Integer.parseInt(getStoredValue(personUri, People.TIMES_CONTACTED));
 
-        assertTrue(lastContacted >= timeBefore);
-        assertTrue(lastContacted <= timeAfter);
-        assertEquals(timesContactedAfter, timesContactedBefore + 1);
+        // No longer supported as of O.
+        assertEquals(0, lastContacted);
+        assertEquals(0, timesContactedAfter);
     }
 
     public void testOrganizationsInsert() {
@@ -401,6 +420,7 @@
 
         // The result is joined with People
         putContactValues(expectedResults[0]);
+        expectedResults[0] = noStats(expectedResults[0]);
         assertStoredValues(uri, expectedResults);
         assertSelection(Phones.CONTENT_URI, values, "phones",
                 Phones._ID, ContentUris.parseId(uri));
@@ -412,6 +432,7 @@
         // Now the person should be joined with Phone
         values.clear();
         putContactValues(values);
+        values = noStats(values);
         values.put(People.TYPE, Phones.TYPE_CUSTOM);
         values.put(People.LABEL, "Directory");
         values.put(People.NUMBER, "1-800-4664-411");
@@ -541,6 +562,9 @@
 
         // The result is joined with People
         putContactValues(values);
+
+        values = noStats(values);
+
         assertStoredValues(uri, values);
         assertSelection(ContactMethods.CONTENT_URI, values, "contact_methods",
                 ContactMethods._ID, ContentUris.parseId(uri));
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index c96c5b2..6ed8711 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -54,6 +54,10 @@
         return new SynchronousProfileProvider(this);
     }
 
+    public ProfileDatabaseHelper getProfileDatabaseHelper() {
+        return getProfileProviderForTest().getDatabaseHelper();
+    }
+
     @Override
     public void onBegin() {
         super.onBegin();
diff --git a/tests/src/com/android/providers/contacts/TestUtils.java b/tests/src/com/android/providers/contacts/TestUtils.java
index 5e88548..4d1f2d6 100644
--- a/tests/src/com/android/providers/contacts/TestUtils.java
+++ b/tests/src/com/android/providers/contacts/TestUtils.java
@@ -16,18 +16,29 @@
 
 package com.android.providers.contacts;
 
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.os.FileUtils;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Profile;
+import android.provider.ContactsContract.RawContacts;
 import android.support.annotation.Nullable;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
 import android.util.Log;
 
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+
 import junit.framework.Assert;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 
 public class TestUtils {
     private static final String TAG = "ContactsTestUtils";
@@ -41,7 +52,7 @@
      * we'll switch to file-based DBs, so we can call {@link #createDatabaseSnapshot}
      * , pull the snapshot DBs and take a look at them.
      */
-    public static final boolean ENABLE_DATABASE_SNAPSHOT = false; // DO NOT SUBMIT WITH TRUE.
+    public static final boolean ENABLE_DATABASE_SNAPSHOT = true; // DO NOT SUBMIT WITH TRUE.
 
     private static final Object sDatabasePathLock = new Object();
     private static File sDatabasePath = null;
@@ -59,14 +70,18 @@
                 } else {
                     Assert.assertTrue("Unable to create directory: " + path, path.mkdirs());
                 }
+                Log.i(TAG, "Test DB directory: " + path);
 
                 sDatabasePath = path;
             }
+            final File ret;
             if (name == null) {
-                return sDatabasePath.getAbsolutePath();
+                ret = sDatabasePath;
             } else {
-                return new File(sDatabasePath, name).getAbsolutePath();
+                ret = new File(sDatabasePath, name);
+                Log.i(TAG, "Test DB file: " + ret);
             }
+            return ret.getAbsolutePath();
         }
     }
 
@@ -100,7 +115,7 @@
             Assert.assertTrue("Unable to create directory: " + toDir, toDir.mkdirs());
         }
 
-        Log.w(TAG, "Copying database files into '" + toDir + "'...");
+        Log.w(TAG, "Copying database files from '" + fromDir + "' into '" + toDir + "'...");
 
         for (File file : fromDir.listFiles()) {
             try {
@@ -140,20 +155,20 @@
      * Writes the content of a cursor to the log.
      */
     public static final void dumpCursor(Cursor c) {
-        final String TAG = "contacts";
-
         final StringBuilder sb = new StringBuilder();
         for (int i = 0; i < c.getColumnCount(); i++) {
-            if (sb.length() > 0) sb.append("|");
+            if (i > 0) sb.append("|");
             sb.append(c.getColumnName(i));
         }
         Log.i(TAG, sb.toString());
 
+        final int pos = c.getPosition();
+
         c.moveToPosition(-1);
         while (c.moveToNext()) {
             sb.setLength(0);
             for (int i = 0; i < c.getColumnCount(); i++) {
-                if (sb.length() > 0) sb.append("|");
+                if (i > 0) sb.append("|");
 
                 if (c.getType(i) == Cursor.FIELD_TYPE_BLOB) {
                     byte[] blob = c.getBlob(i);
@@ -166,6 +181,29 @@
             }
             Log.i(TAG, sb.toString());
         }
+
+        c.moveToPosition(pos);
+    }
+
+    public static void dumpTable(SQLiteDatabase db, String name) {
+        Log.i(TAG, "Dumping table: " + name);
+        try (Cursor c = db.rawQuery(String.format("SELECT * FROM %s", name), null)) {
+            dumpCursor(c);
+        }
+    }
+
+    public static void dumpUri(Context context, Uri uri) {
+        Log.i(TAG, "Dumping URI: " + uri);
+        try (Cursor c = context.getContentResolver().query(uri, null, null, null, null)) {
+            dumpCursor(c);
+        }
+    }
+
+    public static void dumpUri(ContentResolver resolver, Uri uri) {
+        Log.i(TAG, "Dumping URI: " + uri);
+        try (Cursor c = resolver.query(uri, null, null, null, null)) {
+            dumpCursor(c);
+        }
     }
 
     /**
@@ -183,4 +221,57 @@
             return "[Failed to write to file: " + e.getMessage() + "]";
         }
     }
+
+    public static Uri insertRawContact(
+            ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values) {
+        return insertRawContact(RawContacts.CONTENT_URI, resolver, dbh, values);
+    }
+
+    public static Uri insertProfileRawContact(
+            ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values) {
+        return insertRawContact(Profile.CONTENT_RAW_CONTACTS_URI, resolver, dbh, values);
+    }
+
+    private static Uri insertRawContact(Uri tableUri,
+            ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values) {
+        final SQLiteDatabase db = dbh.getWritableDatabase();
+
+        final Uri rowUri = resolver.insert(tableUri, values);
+        Long timesContacted = values.getAsLong(RawContacts.LR_TIMES_CONTACTED);
+        if (timesContacted != null) {
+            // TIMES_CONTACTED is no longer modifiable via resolver, so we update the DB directly.
+            final long rid = Long.parseLong(rowUri.getLastPathSegment());
+
+            final String[] args = {String.valueOf(rid)};
+
+            db.update(Tables.RAW_CONTACTS,
+                    cv(RawContacts.RAW_TIMES_CONTACTED, (long) timesContacted),
+                    "_id=?", args);
+
+            // Then propagate it to contacts too.
+            db.execSQL("UPDATE " + Tables.CONTACTS
+                    + " SET " + Contacts.RAW_TIMES_CONTACTED + " = ("
+                    + " SELECT sum(" + RawContacts.RAW_TIMES_CONTACTED + ") FROM "
+                    + Tables.RAW_CONTACTS + " AS r "
+                    + " WHERE " + Tables.CONTACTS + "._id = r." + RawContacts.CONTACT_ID
+                    + " GROUP BY r." + RawContacts.CONTACT_ID + ")");
+        }
+        return rowUri;
+    }
+
+    public static void executeSqlFromAssetFile(
+            Context context, SQLiteDatabase db, String assetName) {
+        try (InputStream input = context.getAssets().open(assetName);) {
+            BufferedReader r = new BufferedReader(new InputStreamReader(input));
+            String query;
+            while ((query = r.readLine()) != null) {
+                if (query.trim().length() == 0 || query.startsWith("--")) {
+                    continue;
+                }
+                db.execSQL(query);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e.toString());
+        }
+    }
 }
diff --git a/tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java b/tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java
index a0a8622..ee2b5be 100644
--- a/tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java
+++ b/tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java
@@ -193,6 +193,11 @@
 
         checkEnsureNoInvalidTokens(true, "a b ';' c");
         checkEnsureNoInvalidTokens(true, "a b /*;*/ c");
+
+        checkEnsureNoInvalidTokens(false, "a b x_ c");
+        checkEnsureNoInvalidTokens(false, "a b [X_OK] c");
+        checkEnsureNoInvalidTokens(true, "a b 'x_' c");
+        checkEnsureNoInvalidTokens(true, "a b /*x_*/ c");
     }
 
     private void checkEnsureSingleTokenOnly(boolean ok, String sql, String... tokens) {
diff --git a/tests/src/com/android/providers/contacts/util/MockClock.java b/tests/src/com/android/providers/contacts/util/MockClock.java
index dce06c9..03e8265 100644
--- a/tests/src/com/android/providers/contacts/util/MockClock.java
+++ b/tests/src/com/android/providers/contacts/util/MockClock.java
@@ -42,4 +42,8 @@
     public void advance() {
         mCurrentTimeMillis++;
     }
+
+    public void advanceDay() {
+        mCurrentTimeMillis += 24 * 60 * 60;
+    }
 }
diff --git a/tests2/src/com/android/providers/contacts/tests2/AllUrlTest.java b/tests2/src/com/android/providers/contacts/tests2/AllUrlTest.java
index 3bea2da..920243a 100644
--- a/tests2/src/com/android/providers/contacts/tests2/AllUrlTest.java
+++ b/tests2/src/com/android/providers/contacts/tests2/AllUrlTest.java
@@ -228,6 +228,12 @@
 
     private void addFailure(String message) {
         mFailures.add(message);
+        Log.e(TAG, "Failed: " + message);
+        if (mFailures.size() > 100) {
+            // Too many failures...
+        } else {
+            mFailures.add(message);
+        }
     }
 
     private void failIfFailed() {
@@ -282,19 +288,28 @@
                     selectionArgs, sortOrder)) {
                 c.moveToFirst();
             }
+        } catch (Throwable th) {
+            addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage());
+        }
+        try {
             // With CancellationSignal.
             try (Cursor c = getContext().getContentResolver().query(uri, projection, selection,
                     selectionArgs, sortOrder, new CancellationSignal())) {
                 c.moveToFirst();
             }
+        } catch (Throwable th) {
+            addFailure("Query with cancel failed: URI=" + uri + " Message=" + th.getMessage());
+        }
+        try {
             // With limit.
             try (Cursor c = getContext().getContentResolver().query(
-                    uri.buildUpon().appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "0").build(),
+                    uri.buildUpon().appendQueryParameter(
+                            ContactsContract.LIMIT_PARAM_KEY, "0").build(),
                     projection, selection, selectionArgs, sortOrder)) {
                 c.moveToFirst();
             }
         } catch (Throwable th) {
-            addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage());
+            addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage());
         }
     }
 
@@ -330,6 +345,20 @@
         failIfFailed();
     }
 
+    public void testNoHiddenColumns() {
+        for (String[] path : URIs) {
+            if (!supportsQuery(path)) continue;
+            final Uri uri = getUri(path);
+
+            for (String column : getColumns(uri)) {
+                if (column.toLowerCase().startsWith(ContactsContract.HIDDEN_COLUMN_PREFIX)) {  // doesn't seem to be working
+                    addFailure("Uri " + uri + " returned hidden column " + column);
+                }
+            }
+        }
+        failIfFailed();
+    }
+
     /**
      * Make sure all URLs are accessible with a projection.
      */
@@ -357,7 +386,8 @@
                     // These columns only show up when the projection contains certain columns.
 
                     projection = new String[]{"mode", column};
-                } else if (u.startsWith("content://com.android.contacts/search_suggest_query")
+                } else if ((u.startsWith("content://com.android.contacts/search_suggest_query")
+                        || u.startsWith("content://contacts/search_suggest_query"))
                         && "suggest_intent_action".equals(column)) {
                     // Can't be included in the projection due to a bug in GlobalSearchSupport.
                     continue;
@@ -395,6 +425,26 @@
         failIfFailed();
     }
 
+//    /**
+//     * Make sure all URLs are accessible with a selection.
+//     */
+//    public void testSelectWithSelectionUsingColumns() {
+//        for (String[] path : URIs) {
+//            if (!supportsQuery(path)) continue;
+//            final Uri uri = getUri(path);
+//
+//            for (String column : getColumns(uri)) {
+//                checkQueryExecutable(uri,
+//                        null, // projection
+//                        column + "=?", // selection
+//                        ARG1, // , // selection args
+//                        null // sort order
+//                );
+//            }
+//        }
+//        failIfFailed();
+//    }
+
     /**
      * Make sure all URLs are accessible with an order-by.
      */
@@ -403,12 +453,14 @@
             if (!supportsQuery(path)) continue;
             final Uri uri = getUri(path);
 
-            checkQueryExecutable(uri,
-                    null, // projection
-                    null, // selection
-                    null, // , // selection args
-                    getColumns(uri)[0] // sort order
-            );
+            for (String column : getColumns(uri)) {
+                checkQueryExecutable(uri,
+                        null, // projection
+                        "1=2", // selection
+                        null, // , // selection args
+                        column // sort order
+                );
+            }
         }
         failIfFailed();
     }