Merge "Prevent LAST_MODIFIED from changing after deletion" into nyc-mr2-dev
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index a3bed8c..9a5b7c4 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -46,11 +46,11 @@
 import android.telecom.TelecomManager;
 import android.text.TextUtils;
 import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties;
 import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
 import com.android.providers.contacts.util.SelectionBuilder;
 import com.android.providers.contacts.util.UserUtils;
-import com.google.common.annotations.VisibleForTesting;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -153,6 +153,19 @@
         sCallsProjectionMap.put(Calls.LAST_MODIFIED, Calls.LAST_MODIFIED);
     }
 
+    private static final String ALLOWED_PACKAGE_FOR_TESTING = "com.android.providers.contacts";
+
+    @VisibleForTesting
+    static final String PARAM_KEY_QUERY_FOR_TESTING = "query_for_testing";
+
+    /**
+     * A long to override the clock used for timestamps, or "null" to reset to the system clock.
+     */
+    @VisibleForTesting
+    static final String PARAM_KEY_SET_TIME_FOR_TESTING = "set_time_for_testing";
+
+    private static Long sTimeForTestMillis;
+
     private HandlerThread mBackgroundThread;
     private Handler mBackgroundHandler;
     private volatile CountDownLatch mReadAccessLatch;
@@ -223,6 +236,9 @@
                     "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
         }
+
+        queryForTesting(uri);
+
         waitForAccess(mReadAccessLatch);
         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
         qb.setTables(Tables.CALLS);
@@ -278,6 +294,30 @@
         return c;
     }
 
+    private void queryForTesting(Uri uri) {
+        if (!uri.getBooleanQueryParameter(PARAM_KEY_QUERY_FOR_TESTING, false)) {
+            return;
+        }
+        if (!getCallingPackage().equals(ALLOWED_PACKAGE_FOR_TESTING)) {
+            throw new IllegalArgumentException("query_for_testing set from foreign package "
+                    + getCallingPackage());
+        }
+
+        String timeString = uri.getQueryParameter(PARAM_KEY_SET_TIME_FOR_TESTING);
+        if (timeString != null) {
+            if (timeString.equals("null")) {
+                sTimeForTestMillis = null;
+            } else {
+                sTimeForTestMillis = Long.parseLong(timeString);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    static Long getTimeForTestMillis() {
+        return sTimeForTestMillis;
+    }
+
     /**
      * Gets an integer query parameter from a given uri.
      *
diff --git a/src/com/android/providers/contacts/DbModifierWithNotification.java b/src/com/android/providers/contacts/DbModifierWithNotification.java
index ef1b847..b3cf0ef 100644
--- a/src/com/android/providers/contacts/DbModifierWithNotification.java
+++ b/src/com/android/providers/contacts/DbModifierWithNotification.java
@@ -63,7 +63,8 @@
     private static final int SOURCE_PACKAGE_COLUMN_INDEX = 0;
     private static final String NON_NULL_SOURCE_PACKAGE_SELECTION =
             VoicemailContract.SOURCE_PACKAGE_FIELD + " IS NOT NULL";
-
+    private static final String NOT_DELETED_SELECTION =
+            Voicemails.DELETED + " == 0";
     private final String mTableName;
     private final SQLiteDatabase mDb;
     private final InsertHelper mInsertHelper;
@@ -98,7 +99,7 @@
     public long insert(String table, String nullColumnHack, ContentValues values) {
         Set<String> packagesModified = getModifiedPackages(values);
         if (mIsCallsTable) {
-            values.put(Calls.LAST_MODIFIED, System.currentTimeMillis());
+            values.put(Calls.LAST_MODIFIED, getTimeMillis());
         }
         long rowId = mDb.insert(table, nullColumnHack, values);
         if (rowId > 0 && packagesModified.size() != 0) {
@@ -115,7 +116,7 @@
     public long insert(ContentValues values) {
         Set<String> packagesModified = getModifiedPackages(values);
         if (mIsCallsTable) {
-            values.put(Calls.LAST_MODIFIED, System.currentTimeMillis());
+            values.put(Calls.LAST_MODIFIED, getTimeMillis());
         }
         long rowId = mInsertHelper.insert(values);
         if (rowId > 0 && packagesModified.size() != 0) {
@@ -160,8 +161,12 @@
 
         boolean hasMarkedRead = false;
         if (mIsCallsTable) {
-            values.put(Calls.LAST_MODIFIED, System.currentTimeMillis());
-
+            if (values.containsKey(Voicemails.DELETED)
+                    && !values.getAsBoolean(Voicemails.DELETED)) {
+                values.put(Calls.LAST_MODIFIED, getTimeMillis());
+            } else {
+                updateLastModified(table, whereClause, whereArgs);
+            }
             if (isVoicemail) {
                 // If a calling package is modifying its own entries, it means that the change came
                 // from the server and thus is synced or "clean". Otherwise, it means that a local
@@ -199,6 +204,15 @@
         return count;
     }
 
+    private void updateLastModified(String table, String whereClause, String[] whereArgs) {
+        ContentValues values = new ContentValues();
+        values.put(Calls.LAST_MODIFIED, getTimeMillis());
+
+        mDb.update(table, values,
+                DbQueryUtils.concatenateClauses(NOT_DELETED_SELECTION, whereClause),
+                whereArgs);
+    }
+
     @Override
     public int delete(String table, String whereClause, String[] whereArgs) {
         Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
@@ -217,7 +231,7 @@
             ContentValues values = new ContentValues();
             values.put(VoicemailContract.Voicemails.DIRTY, 1);
             values.put(VoicemailContract.Voicemails.DELETED, 1);
-            values.put(VoicemailContract.Voicemails.LAST_MODIFIED, System.currentTimeMillis());
+            values.put(VoicemailContract.Voicemails.LAST_MODIFIED, getTimeMillis());
             count = mDb.update(table, values, whereClause, whereArgs);
         } else {
             count = mDb.delete(table, whereClause, whereArgs);
@@ -363,4 +377,11 @@
         }
         return values.getAsBoolean(key);
     }
+
+    private long getTimeMillis() {
+        if (CallLogProvider.getTimeForTestMillis() == null) {
+            return System.currentTimeMillis();
+        }
+        return CallLogProvider.getTimeForTestMillis();
+    }
 }
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 86dff66..0924154 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -31,6 +31,7 @@
 import android.net.Uri;
 import android.provider.BaseColumns;
 import android.provider.CallLog;
+import android.provider.CallLog.Calls;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.CommonDataKinds.Email;
@@ -1355,16 +1356,21 @@
         }
     }
 
-    protected void assertLastModified(Uri uri) {
-        assertLastModified(uri, System.currentTimeMillis(), 1000);
-    }
-
-    protected void assertLastModified(Uri uri, long time, long tolerance) {
+    protected void assertLastModified(Uri uri, long time) {
         Cursor c = mResolver.query(uri, null, null, null, null);
         c.moveToFirst();
         int index = c.getColumnIndex(CallLog.Calls.LAST_MODIFIED);
         long timeStamp = c.getLong(index);
-        assertTrue(Math.abs(time - timeStamp) < tolerance);
+        assertEquals(timeStamp, time);
+    }
+
+    protected void setTimeForTest(Long time) {
+        Uri uri = Calls.CONTENT_URI.buildUpon()
+                .appendQueryParameter(CallLogProvider.PARAM_KEY_QUERY_FOR_TESTING, "1")
+                .appendQueryParameter(CallLogProvider.PARAM_KEY_SET_TIME_FOR_TESTING,
+                        time == null ? "null" : time.toString())
+                .build();
+        mResolver.query(uri, null, null, null, null);
     }
     /**
      * A contact in the database, and the attributes used to create it.  Construct using
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index f492e73..8ee7a5b 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -85,16 +85,18 @@
     protected void tearDown() throws Exception {
         setUpWithVoicemailPermissions();
         mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
+        setTimeForTest(null);
         super.tearDown();
     }
 
     public void testInsert_RegularCallRecord() {
+        setTimeForTest(1000L);
         ContentValues values = getDefaultCallValues();
         Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
         values.put(Calls.COUNTRY_ISO, "us");
         assertStoredValues(uri, values);
         assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
-        assertLastModified(uri);
+        assertLastModified(uri, 1000);
     }
 
     private void setUpWithVoicemailPermissions() {
@@ -105,6 +107,7 @@
 
     public void testInsert_VoicemailCallRecord() {
         setUpWithVoicemailPermissions();
+        setTimeForTest(1000L);
         final ContentValues values = getDefaultCallValues();
         values.put(Calls.TYPE, Calls.VOICEMAIL_TYPE);
         values.put(Calls.VOICEMAIL_URI, "content://foo/voicemail/2");
@@ -121,10 +124,11 @@
         Uri uri  = mResolver.insert(Calls.CONTENT_URI_WITH_VOICEMAIL, values);
         assertStoredValues(uri, values);
         assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
-        assertLastModified(uri);
+        assertLastModified(uri, 1000);
     }
 
     public void testUpdate() {
+        setTimeForTest(1000L);
         Uri uri = insertCallRecord();
         ContentValues values = new ContentValues();
         values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
@@ -139,7 +143,7 @@
         int count = mResolver.update(uri, values, null, null);
         assertEquals(1, count);
         assertStoredValues(uri, values);
-        assertLastModified(uri);
+        assertLastModified(uri, 1000);
     }
 
     public void testDelete() {
diff --git a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
index b4c9e04..7266731 100644
--- a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
@@ -20,8 +20,10 @@
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.BatteryStats.Uid.Proc;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Status;
@@ -65,6 +67,7 @@
     protected void setUp() throws Exception {
         super.setUp();
         setUpForOwnPermission();
+        addProvider(CallLogProviderTestable.class, CallLog.AUTHORITY);
     }
 
     /** Returns the appropriate /voicemail URI. */
@@ -80,6 +83,7 @@
     }
 
     public void testInsert() throws Exception {
+        setTimeForTest(1000L);
         Uri uri = mResolver.insert(voicemailUri(), getTestVoicemailValues());
         // We create on purpose a new set of ContentValues here, because the code above modifies
         // the copy it gets.
@@ -87,7 +91,7 @@
         assertSelection(uri, getTestVoicemailValues(), Voicemails._ID, ContentUris.parseId(uri));
         assertEquals(1, countFilesInTestDirectory());
 
-        assertLastModified(uri);
+        assertLastModified(uri, 1000);
     }
 
     public void testInsertReadMessageIsNotNew() throws Exception {
@@ -131,6 +135,7 @@
     }
 
     public void testUpdate() {
+        setTimeForTest(1000L);
         Uri uri = insertVoicemail();
         ContentValues values = new ContentValues();
         values.put(Voicemails.NUMBER, "1-800-263-7643");
@@ -145,7 +150,7 @@
         int count = mResolver.update(uri, values, null, null);
         assertEquals(1, count);
         assertStoredValues(uri, values);
-        assertLastModified(uri);
+        assertLastModified(uri, 1000);
     }
 
     public void testUpdateOwnPackageVoicemail_NotDirty() {
@@ -182,6 +187,7 @@
 
     public void testDeleteOtherPackageVoicemail_SetsDirtyStatus() {
         setUpForFullPermission();
+        setTimeForTest(1000L);
         final Uri anotherVoicemail = insertVoicemailForSourcePackage("another-package");
         assertEquals(1, getCount(voicemailUri(), null, null));
 
@@ -195,7 +201,7 @@
 
         assertEquals(1, getCount(anotherVoicemail, null, null));
         assertStoredValues(anotherVoicemail, values);
-        assertLastModified(anotherVoicemail);
+        assertLastModified(anotherVoicemail, 1000);
     }
 
     public void testDelete() {
@@ -206,6 +212,29 @@
         assertEquals(0, getCount(uri, null, null));
     }
 
+    public void testUpdateAfterDelete_lastModifiedNotChanged() {
+        setUpForFullPermission();
+        setTimeForTest(1000L);
+        final Uri anotherVoicemail = insertVoicemailForSourcePackage("another-package");
+        assertEquals(1, getCount(voicemailUri(), null, null));
+
+        // Clear the mapping for our own UID so that this doesn't look like an internal transaction.
+        mPackageManager.removePackage(Process.myUid());
+        mResolver.delete(anotherVoicemail, null, null);
+        assertLastModified(anotherVoicemail, 1000);
+
+        mPackageManager.addPackage(Process.myUid(), mActor.packageName);
+        setTimeForTest(2000L);
+        mResolver.update(anotherVoicemail, new ContentValues(), null, null);
+        assertLastModified(anotherVoicemail, 1000);
+
+        setTimeForTest(3000L);
+        ContentValues values = new ContentValues();
+        values.put(Voicemails.DELETED, "0");
+        mResolver.update(anotherVoicemail, values, null, null);
+        assertLastModified(anotherVoicemail, 3000);
+    }
+
     public void testGetType_ItemUri() throws Exception {
         // Random item uri.
         assertEquals(Voicemails.ITEM_TYPE,