Add additional tests for pending files from FUSE

Added new tests to verify:
1. Owned pending files from FUSE are visible when queried with
MATCH_EXCLUDE.
2. Apps can always open other apps pending files from FUSE via filePath
or via MediaStore APIs
3. Legacy apps creating file for existing db row doesn't upsert and set
IS_PENDING=1.

Bug: 158637603
Test: atest CtsScopedStorageHostTest#testCreateDoesntUpsert_hasRW
Test: atest CtsScopedStorageHostTest#testPendingFromFuse
Test: atest CtsScopedStorageHostTest#testOpenPendingFilesFromFuse
Change-Id: I848158d90bfc13491952a2f6577f87b1af4e50f6
Merged-In: I848158d90bfc13491952a2f6577f87b1af4e50f6
(cherry picked from commit d765f7da5c380428ad0ebb83b7b72c6f8973244d)
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java
index 4ca2e12..ab391c9 100644
--- a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java
@@ -178,6 +178,11 @@
     }
 
     @Test
+    public void testCreateDoesntUpsert() throws Exception {
+        runDeviceTest("testCreateDoesntUpsert");
+    }
+
+    @Test
     public void testCaseInsensitivity() throws Exception {
         runDeviceTest("testAndroidDataObbCannotBeDeleted");
     }
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
index 88be97c..d88329f6 100644
--- a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
@@ -352,6 +352,16 @@
     }
 
     @Test
+    public void testOpenOtherPendingFilesFromFuse() throws Exception {
+        grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testOpenOtherPendingFilesFromFuse");
+        } finally {
+            revokePermissions("android.permission.READ_EXTERNAL_STORAGE");
+        }
+    }
+
+    @Test
     public void testAccess_file() throws Exception {
         grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
         try {
diff --git a/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java b/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java
index 251f65a..43c6d20 100644
--- a/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java
+++ b/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java
@@ -27,6 +27,7 @@
 import static android.scopedstorage.cts.lib.TestUtils.assertFileContent;
 import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
 import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.deleteWithMediaProviderNoThrow;
 import static android.scopedstorage.cts.lib.TestUtils.executeShellCommand;
 import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
 import static android.scopedstorage.cts.lib.TestUtils.getFileOwnerPackageFromDatabase;
@@ -617,6 +618,35 @@
         }
     }
 
+    /**
+     * Test that legacy apps creating files for existing db row doesn't upsert and set IS_PENDING
+     */
+    @Test
+    public void testCreateDoesntUpsert() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File file = new File(TestUtils.getDcimDir(), IMAGE_FILE_NAME);
+        Uri uri = null;
+        try {
+            uri = TestUtils.insertFileUsingDataColumn(file);
+            assertNotNull(uri);
+
+            assertTrue(file.createNewFile());
+
+            try (Cursor c = TestUtils.queryFile(file,
+                    new String[] {MediaStore.MediaColumns.IS_PENDING})) {
+                // This file will not have IS_PENDING=1 because create didn't set IS_PENDING.
+                assertTrue(c.moveToFirst());
+                assertEquals(c.getInt(0), 0);
+            }
+        } finally {
+            file.delete();
+            // If create file fails, we should delete the inserted db row.
+            deleteWithMediaProviderNoThrow(uri);
+        }
+    }
+
     @Test
     public void testAndroidDataObbCannotBeDeleted() throws Exception {
         File canDeleteDir = new File("/sdcard/canDelete");
diff --git a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
index 22e6fb4..3a2caaf 100644
--- a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
@@ -406,7 +406,7 @@
     @NonNull
     public static Cursor queryVideoFile(File file, String... projection) {
         return queryFile(MediaStore.Video.Media.getContentUri(sStorageVolumeName), file,
-                projection);
+                /*includePending*/ true, projection);
     }
 
     /**
@@ -416,7 +416,7 @@
     @NonNull
     public static Cursor queryImageFile(File file, String... projection) {
         return queryFile(MediaStore.Images.Media.getContentUri(sStorageVolumeName), file,
-                projection);
+                /*includePending*/ true, projection);
     }
 
     /**
@@ -991,22 +991,38 @@
         }
     }
 
+    /**
+     * Queries {@link ContentResolver} for a file IS_PENDING=0 and returns a {@link Cursor} with the
+     * given columns.
+     */
     @NonNull
-    public static Cursor queryFile(@NonNull File file, String... projection) {
-        return queryFile(
-                MediaStore.Files.getContentUri(sStorageVolumeName), file, projection);
+    public static Cursor queryFileExcludingPending(@NonNull File file, String... projection) {
+        return queryFile(MediaStore.Files.getContentUri(sStorageVolumeName), file,
+                /*includePending*/ false, projection);
     }
 
     @NonNull
-    private static Cursor queryFile(@NonNull Uri uri, @NonNull File file, String... projection) {
+    public static Cursor queryFile(@NonNull File file, String... projection) {
+        return queryFile(MediaStore.Files.getContentUri(sStorageVolumeName), file,
+                /*includePending*/ true, projection);
+    }
+
+    @NonNull
+    private static Cursor queryFile(@NonNull Uri uri, @NonNull File file, boolean includePending,
+            String... projection) {
         Bundle queryArgs = new Bundle();
         queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
                 MediaStore.MediaColumns.DATA + " = ?");
         queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
                 new String[] { file.getAbsolutePath() });
-        queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
         queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
 
+        if (includePending) {
+            queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
+        } else {
+            queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_EXCLUDE);
+        }
+
         final Cursor c = getContentResolver().query(uri, projection, queryArgs, null);
         assertThat(c).isNotNull();
         return c;
diff --git a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
index 1842d1c..878a82c 100644
--- a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
+++ b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
@@ -78,6 +78,7 @@
 import static android.scopedstorage.cts.lib.TestUtils.pollForExternalStorageState;
 import static android.scopedstorage.cts.lib.TestUtils.pollForPermission;
 import static android.scopedstorage.cts.lib.TestUtils.queryFile;
+import static android.scopedstorage.cts.lib.TestUtils.queryFileExcludingPending;
 import static android.scopedstorage.cts.lib.TestUtils.queryImageFile;
 import static android.scopedstorage.cts.lib.TestUtils.queryVideoFile;
 import static android.scopedstorage.cts.lib.TestUtils.readExifMetadataFromTestApp;
@@ -2439,6 +2440,7 @@
     @Test
     public void testPendingFromFuse() throws Exception {
         final File pendingFile = new File(getDcimDir(), IMAGE_FILE_NAME);
+        final File otherPendingFile = new File(getDcimDir(), VIDEO_FILE_NAME);
         try {
             assertTrue(pendingFile.createNewFile());
             // Newly created file should have IS_PENDING set
@@ -2447,6 +2449,13 @@
                 assertThat(c.getInt(0)).isEqualTo(1);
             }
 
+            // If we query with MATCH_EXCLUDE, we should still see this pendingFile
+            try (Cursor c = queryFileExcludingPending(pendingFile, MediaColumns.IS_PENDING)) {
+                assertThat(c.getCount()).isEqualTo(1);
+                assertTrue(c.moveToFirst());
+                assertThat(c.getInt(0)).isEqualTo(1);
+            }
+
             assertNotNull(MediaStore.scanFile(getContentResolver(), pendingFile));
 
             // IS_PENDING should be unset after the scan
@@ -2454,8 +2463,35 @@
                 assertTrue(c.moveToFirst());
                 assertThat(c.getInt(0)).isEqualTo(0);
             }
+
+            installAppWithStoragePermissions(TEST_APP_A);
+            assertCreateFilesAs(TEST_APP_A, otherPendingFile);
+            // We can't query other apps pending file from FUSE with MATCH_EXCLUDE
+            try (Cursor c = queryFileExcludingPending(otherPendingFile, MediaColumns.IS_PENDING)) {
+                assertThat(c.getCount()).isEqualTo(0);
+            }
         } finally {
             pendingFile.delete();
+            deleteFileAsNoThrow(TEST_APP_A, otherPendingFile.getAbsolutePath());
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testOpenOtherPendingFilesFromFuse() throws Exception {
+        final File otherPendingFile = new File(getDcimDir(), IMAGE_FILE_NAME);
+        try {
+            installApp(TEST_APP_A);
+            assertCreateFilesAs(TEST_APP_A, otherPendingFile);
+
+            // We can read other app's pending file from FUSE via filePath
+            assertCanQueryAndOpenFile(otherPendingFile, "r");
+
+            // We can also read other app's pending file via MediaStore API
+            assertNotNull(openWithMediaProvider(otherPendingFile, "r"));
+        } finally {
+            deleteFileAsNoThrow(TEST_APP_A, otherPendingFile.getAbsolutePath());
+            uninstallAppNoThrow(TEST_APP_A);
         }
     }