Add unit tests for recent regressions

Added three tests to catch photos editing issue and similar
regressions from app compatibility testing.
TestUpsert already tests one of the bug.

Test: atest
FuseDaemonHostTest#testCreateAndRenameDoesntLeaveStaleDBRow_hasRW
Test: atest FuseDaemonHostTest#testRenameDoesntInvalidateUri_hasRW
Test: atest FuseDaemonHostTest#testCanRenameAFileWithNoDBRow_hasRW
Bug: 150579385

Change-Id: I166348c17deb924ad42d9b0e198073b527e2ff51
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/LegacyAccessHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/LegacyAccessHostTest.java
index 852ab0e..9ebdfe8 100644
--- a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/LegacyAccessHostTest.java
+++ b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/LegacyAccessHostTest.java
@@ -195,4 +195,20 @@
     public void testLegacyAppCanOwnAFile_hasW() throws Exception {
         runDeviceTest("testLegacyAppCanOwnAFile_hasW");
     }
+
+    @Test
+    public void testCreateAndRenameDoesntLeaveStaleDBRow_hasRW() throws Exception {
+        runDeviceTest("testCreateAndRenameDoesntLeaveStaleDBRow_hasRW");
+    }
+
+    @Test
+    public void testRenameDoesntInvalidateUri_hasRW() throws Exception {
+        runDeviceTest("testRenameDoesntInvalidateUri_hasRW");
+    }
+
+
+    @Test
+    public void testCanRenameAFileWithNoDBRow_hasRW() throws Exception {
+        runDeviceTest("testCanRenameAFileWithNoDBRow_hasRW");
+    }
 }
diff --git a/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java b/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java
index 8dfbb4d..b28f7fc 100644
--- a/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java
+++ b/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java
@@ -16,13 +16,17 @@
 
 package com.android.tests.fused.legacy;
 
+import static com.android.tests.fused.lib.TestUtils.BYTES_DATA1;
+import static com.android.tests.fused.lib.TestUtils.BYTES_DATA2;
+import static com.android.tests.fused.lib.TestUtils.STR_DATA1;
+import static com.android.tests.fused.lib.TestUtils.STR_DATA2;
 import static com.android.tests.fused.lib.TestUtils.assertCanRenameFile;
 import static com.android.tests.fused.lib.TestUtils.assertCanRenameDirectory;
 import static com.android.tests.fused.lib.TestUtils.assertCantRenameFile;
+import static com.android.tests.fused.lib.TestUtils.assertFileContent;
 import static com.android.tests.fused.lib.TestUtils.createFileAs;
-
-
 import static com.android.tests.fused.lib.TestUtils.deleteFileAsNoThrow;
+import static com.android.tests.fused.lib.TestUtils.getContentResolver;
 import static com.android.tests.fused.lib.TestUtils.getFileOwnerPackageFromDatabase;
 import static com.android.tests.fused.lib.TestUtils.getFileRowIdFromDatabase;
 import static com.android.tests.fused.lib.TestUtils.installApp;
@@ -36,11 +40,17 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.Manifest;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
 import android.os.Environment;
+import android.provider.MediaStore;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
@@ -52,6 +62,8 @@
 import com.android.cts.install.lib.TestApp;
 import com.android.tests.fused.lib.ReaddirTestHelper;
 
+import com.google.common.io.Files;
+
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -59,7 +71,9 @@
 
 import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 
 /**
@@ -76,6 +90,7 @@
     private static final String TAG = "LegacyFileAccessTest";
     static final String THIS_PACKAGE_NAME = InstrumentationRegistry.getContext().getPackageName();
 
+    static final String IMAGE_FILE_NAME = "FilePathAccessTest_file.jpg";
     static final String VIDEO_FILE_NAME = "LegacyAccessTest_file.mp4";
     static final String NONMEDIA_FILE_NAME = "LegacyAccessTest_file.pdf";
 
@@ -460,6 +475,134 @@
         }
     }
 
+    /**
+     * b/14966134: Test that FuseDaemon doesn't leave stale database entries after create() and
+     * rename().
+     */
+    @Test
+    public void testCreateAndRenameDoesntLeaveStaleDBRow_hasRW() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File directoryDCIM = new File(Environment.getExternalStorageDirectory(),
+                Environment.DIRECTORY_DCIM);
+        final File videoFile = new File(directoryDCIM, VIDEO_FILE_NAME);
+        final File renamedVideoFile = new File(directoryDCIM, "Renamed_" + VIDEO_FILE_NAME);
+        final ContentResolver cr = getContentResolver();
+
+        try {
+            assertThat(videoFile.createNewFile()).isTrue();
+            assertThat(videoFile.renameTo(renamedVideoFile)).isTrue();
+
+            ContentValues values = new ContentValues();
+            values.put(MediaStore.MediaColumns.DATA, renamedVideoFile.getAbsolutePath());
+            // Insert new renamedVideoFile to database
+            final Uri uri = cr.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values, null);
+            assertNotNull(uri);
+
+            // Query for all images/videos in the device.
+            // This shouldn't list videoFile which was renamed to renamedVideoFile.
+            final ArrayList<String> imageAndVideoFiles = getImageAndVideoFilesFromDatabase();
+            assertThat(imageAndVideoFiles).contains(renamedVideoFile.getName());
+            assertThat(imageAndVideoFiles).doesNotContain(videoFile.getName());
+        } finally {
+            videoFile.delete();
+            renamedVideoFile.delete();
+            MediaStore.scanFile(cr, renamedVideoFile);
+        }
+    }
+
+    /**
+     * b/150147690,b/150193381: Test that file rename doesn't delete any existing Uri.
+     */
+    @Test
+    public void testRenameDoesntInvalidateUri_hasRW() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File directoryDCIM = new File(Environment.getExternalStorageDirectory(),
+                Environment.DIRECTORY_DCIM);
+        final File imageFile = new File(directoryDCIM, IMAGE_FILE_NAME);
+        final File temporaryImageFile = new File(directoryDCIM, IMAGE_FILE_NAME + "_.tmp");
+        final ContentResolver cr = getContentResolver();
+
+        try {
+            assertThat(imageFile.createNewFile()).isTrue();
+            try (final FileOutputStream fos = new FileOutputStream(imageFile)) {
+                fos.write(BYTES_DATA1);
+            }
+            // Insert this file to database.
+            ContentValues values = new ContentValues();
+            values.put(MediaStore.MediaColumns.DATA, imageFile.getAbsolutePath());
+            final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, null);
+            assertNotNull(uri);
+
+            Files.copy(imageFile, temporaryImageFile);
+            // Write more bytes to temporaryImageFile
+            try (final FileOutputStream fos = new FileOutputStream(temporaryImageFile, true)) {
+                fos.write(BYTES_DATA2);
+            }
+            assertThat(imageFile.delete()).isTrue();
+            temporaryImageFile.renameTo(imageFile);
+
+            // Previous uri of imageFile is unaltered after delete & rename.
+            final Uri scannedUri = MediaStore.scanFile(cr, imageFile);
+            assertThat(scannedUri.getLastPathSegment()).isEqualTo(uri.getLastPathSegment());
+
+            final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
+            assertFileContent(imageFile, expected);
+        } finally {
+            imageFile.delete();
+            temporaryImageFile.delete();
+            MediaStore.scanFile(cr, imageFile);
+        }
+    }
+
+    /**
+     * b/150498564,b/150274099: Test that apps can rename files that are not in database.
+     */
+    @Test
+    public void testCanRenameAFileWithNoDBRow_hasRW() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File directoryDCIM = new File(Environment.getExternalStorageDirectory(),
+                Environment.DIRECTORY_DCIM);
+        final File directoryNoMedia = new File(directoryDCIM, ".directoryNoMedia");
+        final File imageInNoMediaDir = new File(directoryNoMedia, IMAGE_FILE_NAME);
+        final File renamedImageInDCIM = new File(directoryDCIM, IMAGE_FILE_NAME);
+        final File noMediaFile = new File(directoryNoMedia, ".nomedia");
+        final ContentResolver cr = getContentResolver();
+
+        try {
+            if (!directoryNoMedia.exists()) {
+                assertThat(directoryNoMedia.mkdirs()).isTrue();
+            }
+            assertThat(noMediaFile.createNewFile()).isTrue();
+            assertThat(imageInNoMediaDir.createNewFile()).isTrue();
+            // Remove imageInNoMediaDir from database.
+            MediaStore.scanFile(cr, directoryNoMedia);
+
+            // Query for all images/videos in the device. This shouldn't list imageInNoMediaDir
+            assertThat(getImageAndVideoFilesFromDatabase())
+                    .doesNotContain(imageInNoMediaDir.getName());
+
+            // Rename shouldn't throw error even if imageInNoMediaDir is not in database.
+            assertThat(imageInNoMediaDir.renameTo(renamedImageInDCIM)).isTrue();
+            // We can insert renamedImageInDCIM to database
+            ContentValues values = new ContentValues();
+            values.put(MediaStore.MediaColumns.DATA, renamedImageInDCIM.getAbsolutePath());
+            final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, null);
+            assertNotNull(uri);
+        } finally {
+            imageInNoMediaDir.delete();
+            renamedImageInDCIM.delete();
+            MediaStore.scanFile(cr, renamedImageInDCIM);
+            noMediaFile.delete();
+        }
+
+    }
+
     private static void assertCanCreateFile(File file) throws IOException {
         if (file.exists()) {
             file.delete();
@@ -489,4 +632,27 @@
             dir.delete();
         }
     }
+
+    /**
+     * Queries {@link ContentResolver} for all image and video files, returns display name of
+     * corresponding files.
+     */
+    private static ArrayList<String> getImageAndVideoFilesFromDatabase() {
+        ArrayList<String> mediaFiles = new ArrayList<>();
+        final String selection = "is_pending = 0 AND is_trashed = 0 AND "
+                + "(media_type = ? OR media_type = ?)";
+        final String[] selectionArgs = new String[] {
+                String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
+                String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)};
+
+        try (Cursor c = getContentResolver().query(
+                MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
+                /* projection */ new String[]{MediaStore.MediaColumns.DISPLAY_NAME},
+                selection, selectionArgs, null)) {
+            while (c.moveToNext()) {
+                mediaFiles.add(c.getString(0));
+            }
+        }
+        return mediaFiles;
+    }
 }
diff --git a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
index 89863ed..75735a5 100644
--- a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
+++ b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
@@ -43,7 +43,9 @@
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
 import android.provider.MediaStore;
+import android.system.ErrnoException;
 import android.system.Os;
+import android.system.OsConstants;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -58,7 +60,10 @@
 import com.google.common.io.ByteStreams;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.concurrent.CountDownLatch;
@@ -76,6 +81,13 @@
     public static final String CREATE_FILE_QUERY = "com.android.tests.fused.createfile";
     public static final String DELETE_FILE_QUERY = "com.android.tests.fused.deletefile";
 
+
+    public static final String STR_DATA1 = "Just some random text";
+    public static final String STR_DATA2 = "More arbitrary stuff";
+
+    public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
+    public static final byte[] BYTES_DATA2 = STR_DATA2.getBytes();
+
     private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
     private static final long POLLING_SLEEP_MILLIS = 100;
 
@@ -467,6 +479,32 @@
     }
 
     /**
+     * Asserts the entire content of the file equals exactly {@code expectedContent}.
+     */
+    public static void assertFileContent(File file, byte[] expectedContent) throws IOException {
+        try (final FileInputStream fis = new FileInputStream(file)) {
+            assertInputStreamContent(fis, expectedContent);
+        }
+    }
+
+    /**
+     * Asserts the entire content of the file equals exactly {@code expectedContent}.
+     * <p>Sets {@code fd} to beginning of file first.
+     */
+    public static void assertFileContent(FileDescriptor fd, byte[] expectedContent)
+            throws IOException, ErrnoException {
+        Os.lseek(fd, 0, OsConstants.SEEK_SET);
+        try (final FileInputStream fis = new FileInputStream(fd)) {
+            assertInputStreamContent(fis, expectedContent);
+        }
+    }
+
+    private static void assertInputStreamContent(InputStream in, byte[] expectedContent)
+            throws IOException {
+        assertThat(ByteStreams.toByteArray(in)).isEqualTo(expectedContent);
+    }
+
+    /**
      * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED.
      */
     private static boolean checkPermissionAndAppOp(String permission) {
diff --git a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
index a17e7b9..40ffba7 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -25,12 +25,17 @@
 import static com.android.tests.fused.lib.RedactionTestHelper.assertExifMetadataMismatch;
 import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadata;
 import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadataFromRawResource;
+import static com.android.tests.fused.lib.TestUtils.BYTES_DATA1;
+import static com.android.tests.fused.lib.TestUtils.BYTES_DATA2;
+import static com.android.tests.fused.lib.TestUtils.STR_DATA1;
+import static com.android.tests.fused.lib.TestUtils.STR_DATA2;
 import static com.android.tests.fused.lib.TestUtils.assertCanRenameFile;
 import static com.android.tests.fused.lib.TestUtils.assertCanRenameDirectory;
 import static com.android.tests.fused.lib.TestUtils.adoptShellPermissionIdentity;
 import static com.android.tests.fused.lib.TestUtils.allowAppOpsToUid;
 import static com.android.tests.fused.lib.TestUtils.assertCantRenameDirectory;
 import static com.android.tests.fused.lib.TestUtils.assertCantRenameFile;
+import static com.android.tests.fused.lib.TestUtils.assertFileContent;
 import static com.android.tests.fused.lib.TestUtils.assertThrows;
 import static com.android.tests.fused.lib.TestUtils.createFileAs;
 import static com.android.tests.fused.lib.TestUtils.deleteFileAs;
@@ -79,8 +84,6 @@
 import com.android.cts.install.lib.TestApp;
 import com.android.tests.fused.lib.ReaddirTestHelper;
 
-import com.google.common.io.ByteStreams;
-
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -123,12 +126,6 @@
     static final String IMAGE_FILE_NAME = "FilePathAccessTest_file.jpg";
     static final String NONMEDIA_FILE_NAME = "FilePathAccessTest_file.pdf";
 
-    static final String STR_DATA1 = "Just some random text";
-    static final String STR_DATA2 = "More arbitrary stuff";
-
-    static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
-    static final byte[] BYTES_DATA2 = STR_DATA2.getBytes();
-
     static final String FILE_CREATION_ERROR_MESSAGE = "No such file or directory";
 
     private static final TestApp TEST_APP_A  = new TestApp("TestAppA",
@@ -1591,30 +1588,4 @@
                     + "running the test!");
         }
     }
-
-    /**
-     * Asserts the entire content of the file equals exactly {@code expectedContent}.
-     */
-    private static void assertFileContent(File file, byte[] expectedContent) throws IOException {
-        try (final FileInputStream fis = new FileInputStream(file)) {
-            assertInputStreamContent(fis, expectedContent);
-        }
-    }
-
-    /**
-     * Asserts the entire content of the file equals exactly {@code expectedContent}.
-     * <p>Sets {@code fd} to beginning of file first.
-     */
-    private static void assertFileContent(FileDescriptor fd, byte[] expectedContent)
-            throws IOException, ErrnoException {
-        Os.lseek(fd, 0, OsConstants.SEEK_SET);
-        try (final FileInputStream fis = new FileInputStream(fd)) {
-            assertInputStreamContent(fis, expectedContent);
-        }
-    }
-
-    private static void assertInputStreamContent(InputStream in, byte[] expectedContent)
-            throws IOException {
-        assertThat(ByteStreams.toByteArray(in)).isEqualTo(expectedContent);
-    }
 }