Merge "Add test for LocalCallingIdentity changes" into rvc-dev
diff --git a/tests/jni/FuseDaemonTest/Android.bp b/tests/jni/FuseDaemonTest/Android.bp
index 35cc9df..1208e1d 100644
--- a/tests/jni/FuseDaemonTest/Android.bp
+++ b/tests/jni/FuseDaemonTest/Android.bp
@@ -26,6 +26,22 @@
     sdk_version: "test_current",
     srcs: ["FilePathAccessTestHelper/src/**/*.java"],
 }
+android_test_helper_app {
+    name: "TestAppC",
+    manifest: "FilePathAccessTestHelper/TestAppC.xml",
+    static_libs: ["androidx.test.rules", "tests-fusedaemon-lib"],
+    sdk_version: "test_current",
+    srcs: ["FilePathAccessTestHelper/src/**/*.java"],
+}
+android_test_helper_app {
+    name: "TestAppCLegacy",
+    manifest: "FilePathAccessTestHelper/TestAppCLegacy.xml",
+    static_libs: ["androidx.test.rules", "tests-fusedaemon-lib"],
+    sdk_version: "test_current",
+    target_sdk_version: "28",
+    srcs: ["FilePathAccessTestHelper/src/**/*.java"],
+}
+
 android_test {
     name: "FuseDaemonTest",
     manifest: "AndroidManifest.xml",
@@ -36,9 +52,10 @@
     java_resources: [
         ":TestAppA",
         ":TestAppB",
+        ":TestAppC",
+        ":TestAppCLegacy",
     ]
 }
-
 android_test {
     name: "FuseDaemonLegacyTest",
     manifest: "legacy/AndroidManifest.xml",
diff --git a/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/TestAppC.xml b/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/TestAppC.xml
new file mode 100644
index 0000000..f3f3c3d
--- /dev/null
+++ b/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/TestAppC.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.tests.fused.testapp.C"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+  <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
+  <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+
+    <application android:label="TestAppC">
+        <activity android:name="com.android.tests.fused.FilePathAccessTestHelper">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/TestAppCLegacy.xml b/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/TestAppCLegacy.xml
new file mode 100644
index 0000000..a31ee47
--- /dev/null
+++ b/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/TestAppCLegacy.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.tests.fused.testapp.C"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application android:label="TestAppCLegacy">
+        <activity android:name="com.android.tests.fused.FilePathAccessTestHelper">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/src/com/android/tests/fused/FilePathAccessTestHelper.java b/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/src/com/android/tests/fused/FilePathAccessTestHelper.java
index 7f6a485..f7af146 100644
--- a/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/src/com/android/tests/fused/FilePathAccessTestHelper.java
+++ b/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/src/com/android/tests/fused/FilePathAccessTestHelper.java
@@ -22,7 +22,10 @@
 import static com.android.tests.fused.lib.TestUtils.DELETE_FILE_QUERY;
 import static com.android.tests.fused.lib.TestUtils.INTENT_EXCEPTION;
 import static com.android.tests.fused.lib.TestUtils.INTENT_EXTRA_PATH;
+import static com.android.tests.fused.lib.TestUtils.OPEN_FILE_FOR_READ_QUERY;
+import static com.android.tests.fused.lib.TestUtils.OPEN_FILE_FOR_WRITE_QUERY;
 import static com.android.tests.fused.lib.TestUtils.QUERY_TYPE;
+import static com.android.tests.fused.lib.TestUtils.canOpen;
 
 import android.app.Activity;
 import android.content.Intent;
@@ -54,7 +57,9 @@
                 break;
             case CREATE_FILE_QUERY:
             case DELETE_FILE_QUERY:
-                createOrDeleteFile(queryType);
+            case OPEN_FILE_FOR_READ_QUERY:
+            case OPEN_FILE_FOR_WRITE_QUERY:
+                accessFile(queryType);
                 break;
             case EXIF_METADATA_QUERY:
                 sendMetadata(queryType);
@@ -99,7 +104,7 @@
         }
     }
 
-    private void createOrDeleteFile(String queryType) {
+    private void accessFile(String queryType) {
         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
             final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
             final File file = new File(filePath);
@@ -109,9 +114,13 @@
                     returnStatus = file.createNewFile();
                 } else if (queryType.equals(DELETE_FILE_QUERY)) {
                     returnStatus = file.delete();
+                } else if (queryType.equals(OPEN_FILE_FOR_READ_QUERY)) {
+                    returnStatus = canOpen(file, false /* forWrite */);
+                } else if (queryType.equals(OPEN_FILE_FOR_WRITE_QUERY)) {
+                    returnStatus = canOpen(file, true /* forWrite */);
                 }
             } catch(IOException e) {
-                Log.e(TAG, "IOException occurred while creating/deleting " + filePath);
+                Log.e(TAG, "Failed to access file: " + filePath + ". Query type: " + queryType, e);
             }
             final Intent intent = new Intent(queryType);
             intent.putExtra(queryType, returnStatus);
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
index 10b063a..9d8a1a0 100644
--- a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
+++ b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
@@ -151,6 +151,23 @@
     }
 
     @Test
+    public void testCallingIdentityCacheInvalidation() throws Exception {
+        // General IO access
+        runDeviceTest("testReadStorageInvalidation");
+        runDeviceTest("testWriteStorageInvalidation");
+        // File manager access
+        runDeviceTest("testManageStorageInvalidation");
+        // Default gallery
+        runDeviceTest("testWriteImagesInvalidation");
+        runDeviceTest("testWriteVideoInvalidation");
+        // EXIF access
+        runDeviceTest("testAccessMediaLocationInvalidation");
+
+        runDeviceTest("testAppUpdateInvalidation");
+        runDeviceTest("testAppReinstallInvalidation");
+    }
+
+    @Test
     public void testRenameFile() throws Exception {
         runDeviceTest("testRenameFile");
     }
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 75735a5..97e39d4 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
@@ -62,6 +62,7 @@
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -80,7 +81,8 @@
     public static final String INTENT_EXCEPTION = "com.android.tests.fused.exception";
     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 OPEN_FILE_FOR_READ_QUERY = "com.android.tests.fused.openfile_read";
+    public static final String OPEN_FILE_FOR_WRITE_QUERY = "com.android.tests.fused.openfile_write";
 
     public static final String STR_DATA1 = "Just some random text";
     public static final String STR_DATA2 = "More arbitrary stuff";
@@ -91,47 +93,47 @@
     private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
     private static final long POLLING_SLEEP_MILLIS = 100;
 
-    private static final UiAutomation sUiAutomation = InstrumentationRegistry.getInstrumentation()
-            .getUiAutomation();
-
     /**
      * Grants {@link Manifest.permission#GRANT_RUNTIME_PERMISSIONS} to the given package.
      */
-    public static void grantReadExternalStorage(String packageName) {
-        sUiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS");
+    public static void grantPermission(String packageName, String permission) {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS");
         try {
-            sUiAutomation.grantRuntimePermission(packageName,
-                    Manifest.permission.READ_EXTERNAL_STORAGE);
+            uiAutomation.grantRuntimePermission(packageName, permission);
             // Wait for OP_READ_EXTERNAL_STORAGE to get updated.
             SystemClock.sleep(1000);
         } finally {
-            sUiAutomation.dropShellPermissionIdentity();
+            uiAutomation.dropShellPermissionIdentity();
         }
     }
 
     /**
      * Revokes {@link Manifest.permission#GRANT_RUNTIME_PERMISSIONS} from the given package.
      */
-    public static void revokeReadExternalStorage(String packageName) {
-        sUiAutomation.adoptShellPermissionIdentity("android.permission.REVOKE_RUNTIME_PERMISSIONS");
+    public static void revokePermission(String packageName, String permission) {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity("android.permission.REVOKE_RUNTIME_PERMISSIONS");
         try {
-            sUiAutomation.revokeRuntimePermission(packageName,
-                    Manifest.permission.READ_EXTERNAL_STORAGE);
+            uiAutomation.revokeRuntimePermission(packageName, permission);
         } finally {
-            sUiAutomation.dropShellPermissionIdentity();
+            uiAutomation.dropShellPermissionIdentity();
         }
     }
 
     public static void adoptShellPermissionIdentity(String... permissions) {
-        sUiAutomation.adoptShellPermissionIdentity(permissions);
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(permissions);
     }
 
     public static void dropShellPermissionIdentity() {
-        sUiAutomation.dropShellPermissionIdentity();
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
     }
 
     public static String executeShellCommand(String cmd) throws Exception {
-        try (FileInputStream output = new FileInputStream (sUiAutomation.executeShellCommand(cmd)
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try (FileInputStream output = new FileInputStream (uiAutomation.executeShellCommand(cmd)
                 .getFileDescriptor())) {
             return new String(ByteStreams.toByteArray(output));
         }
@@ -166,7 +168,7 @@
      * <p>This method drops shell permission identity.
      */
     public static boolean createFileAs(TestApp testApp, String path) throws Exception {
-        return createOrDeleteFileFromTestApp(testApp, path, CREATE_FILE_QUERY);
+        return getResultFromTestApp(testApp, path, CREATE_FILE_QUERY);
     }
 
     /**
@@ -175,7 +177,7 @@
      * <p>This method drops shell permission identity.
      */
     public static boolean deleteFileAs(TestApp testApp, String path) throws Exception {
-        return createOrDeleteFileFromTestApp(testApp, path, DELETE_FILE_QUERY);
+        return getResultFromTestApp(testApp, path, DELETE_FILE_QUERY);
     }
 
     /**
@@ -192,14 +194,25 @@
     }
 
     /**
+     * Makes the given {@code testApp} open a file for read or write.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static boolean openFileAs(TestApp testApp, String path, boolean forWrite)
+            throws Exception {
+        return getResultFromTestApp(testApp, path,
+                forWrite ? OPEN_FILE_FOR_WRITE_QUERY : OPEN_FILE_FOR_READ_QUERY);
+    }
+
+    /**
      * Installs a {@link TestApp} and may grant it storage permissions.
      */
     public static void installApp(TestApp testApp, boolean grantStoragePermission)
             throws Exception {
-
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         try {
             final String packageName = testApp.getPackageName();
-            sUiAutomation.adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES,
+            uiAutomation.adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES,
                     Manifest.permission.DELETE_PACKAGES);
             if (InstallUtils.getInstalledVersion(packageName) != -1) {
                 Uninstall.packages(packageName);
@@ -207,10 +220,10 @@
             Install.single(testApp).commit();
             assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1);
             if (grantStoragePermission) {
-                grantReadExternalStorage(packageName);
+                grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE);
             }
         } finally {
-            sUiAutomation.dropShellPermissionIdentity();
+            uiAutomation.dropShellPermissionIdentity();
         }
     }
 
@@ -218,14 +231,15 @@
      * Uninstalls a {@link TestApp}.
      */
     public static void uninstallApp(TestApp testApp) throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         try {
             final String packageName = testApp.getPackageName();
-            sUiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES);
+            uiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES);
 
             Uninstall.packages(packageName);
             assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(-1);
         } finally {
-            sUiAutomation.dropShellPermissionIdentity();
+            uiAutomation.dropShellPermissionIdentity();
         }
     }
 
@@ -456,6 +470,22 @@
         }
     }
 
+    public static boolean canOpen(File file, boolean forWrite) {
+        if (forWrite) {
+            try (FileOutputStream fis = new FileOutputStream(file)) {
+                return true;
+            } catch (IOException expected) {
+                return false;
+            }
+        } else {
+            try (FileInputStream fis = new FileInputStream(file)) {
+                return true;
+            } catch (IOException expected) {
+                return false;
+            }
+        }
+    }
+
     public static void pollForExternalStorageState() throws Exception {
         for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
             if(Environment.getExternalStorageState(Environment.getExternalStorageDirectory())
@@ -534,13 +564,14 @@
      * <p>This method drops shell permission identity.
      */
     private static void forceStopApp(String packageName) throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         try {
-            sUiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES);
+            uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES);
 
             getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName);
             Thread.sleep(1000);
         } finally {
-            sUiAutomation.dropShellPermissionIdentity();
+            uiAutomation.dropShellPermissionIdentity();
         }
     }
 
@@ -621,7 +652,7 @@
     /**
      * <p>This method drops shell permission identity.
      */
-    private static boolean createOrDeleteFileFromTestApp(TestApp testApp, String dirPath,
+    private static boolean getResultFromTestApp(TestApp testApp, String dirPath,
             String actionName) throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         final boolean[] appOutput = new boolean[1];
@@ -668,4 +699,4 @@
         assertThat(c).isNotNull();
         return c;
     }
-}
\ No newline at end of file
+}
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 54caffe..f676780 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -16,6 +16,7 @@
 
 package com.android.tests.fused;
 
+import static android.app.AppOpsManager.permissionToOp;
 import static android.os.SystemProperties.getBoolean;
 import static android.provider.MediaStore.MediaColumns;
 
@@ -36,6 +37,7 @@
 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.canOpen;
 import static com.android.tests.fused.lib.TestUtils.createFileAs;
 import static com.android.tests.fused.lib.TestUtils.deleteFileAs;
 import static com.android.tests.fused.lib.TestUtils.deleteFileAsNoThrow;
@@ -47,11 +49,13 @@
 import static com.android.tests.fused.lib.TestUtils.getFileMimeTypeFromDatabase;
 import static com.android.tests.fused.lib.TestUtils.getFileRowIdFromDatabase;
 import static com.android.tests.fused.lib.TestUtils.getFileUri;
+import static com.android.tests.fused.lib.TestUtils.grantPermission;
 import static com.android.tests.fused.lib.TestUtils.installApp;
 import static com.android.tests.fused.lib.TestUtils.listAs;
+import static com.android.tests.fused.lib.TestUtils.openFileAs;
 import static com.android.tests.fused.lib.TestUtils.openWithMediaProvider;
 import static com.android.tests.fused.lib.TestUtils.readExifMetadataFromTestApp;
-import static com.android.tests.fused.lib.TestUtils.revokeReadExternalStorage;
+import static com.android.tests.fused.lib.TestUtils.revokePermission;
 import static com.android.tests.fused.lib.TestUtils.uninstallApp;
 import static com.android.tests.fused.lib.TestUtils.uninstallAppNoThrow;
 import static com.android.tests.fused.lib.TestUtils.updateDisplayNameWithMediaProvider;
@@ -63,6 +67,7 @@
 
 import static org.junit.Assume.assumeTrue;
 
+import android.Manifest;
 import android.app.AppOpsManager;
 import android.content.ContentResolver;
 import android.database.Cursor;
@@ -77,6 +82,7 @@
 import android.system.OsConstants;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.cts.install.lib.TestApp;
@@ -89,7 +95,6 @@
 
 import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -139,10 +144,14 @@
             "com.android.tests.fused.testapp.A", 1, false, "TestAppA.apk");
     private static final TestApp TEST_APP_B  = new TestApp("TestAppB",
             "com.android.tests.fused.testapp.B", 1, false, "TestAppB.apk");
+    private static final TestApp TEST_APP_C  = new TestApp("TestAppC",
+            "com.android.tests.fused.testapp.C", 1, false, "TestAppC.apk");
+    private static final TestApp TEST_APP_C_LEGACY  = new TestApp("TestAppCLegacy",
+            "com.android.tests.fused.testapp.C", 1, false, "TestAppCLegacy.apk");
     private static final String[] SYSTEM_GALERY_APPOPS = { AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES,
             AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO };
-    //TODO(b/150115615): used AppOpsManager#OPSTR_MANAGE_EXTERNAL_STORAGE once it's public API
-    private static final String OPSTR_MANAGE_EXTERNAL_STORAGE = "android:manage_external_storage";
+    private static final String OPSTR_MANAGE_EXTERNAL_STORAGE =
+            permissionToOp(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
 
     @Before
     public void setUp() throws Exception {
@@ -380,22 +389,10 @@
             assertThat(nonMediaFile.exists()).isTrue();
 
             // But we can't access their content
-            try (FileInputStream fis = new FileInputStream(mediaFile)) {
-                fail("Opening for read succeeded when it should have failed: " + mediaFile);
-            } catch (IOException expected) {}
-
-            try (FileInputStream fis = new FileInputStream(nonMediaFile)) {
-                fail("Opening for read succeeded when it should have failed: " + nonMediaFile);
-            } catch (IOException expected) {}
-
-            try (FileOutputStream fos = new FileOutputStream(mediaFile)) {
-                fail("Opening for write succeeded when it should have failed: " + mediaFile);
-            } catch (IOException expected) {}
-
-            try (FileOutputStream fos = new FileOutputStream(nonMediaFile)) {
-                fail("Opening for write succeeded when it should have failed: " + nonMediaFile);
-            } catch (IOException expected) {}
-
+            assertThat(canOpen(mediaFile, /* forWrite */ false)).isFalse();
+            assertThat(canOpen(nonMediaFile, /* forWrite */ true)).isFalse();
+            assertThat(canOpen(mediaFile, /* forWrite */ false)).isFalse();
+            assertThat(canOpen(nonMediaFile, /* forWrite */ true)).isFalse();
         } finally {
             deleteFileAsNoThrow(TEST_APP_A, nonMediaFile.getPath());
             deleteFileAsNoThrow(TEST_APP_A, mediaFile.getPath());
@@ -560,7 +557,8 @@
             assertThat(listAs(TEST_APP_B, dir.getPath())).containsExactly(videoFileName);
 
             // Revoke storage permission for TEST_APP_B
-            revokeReadExternalStorage(TEST_APP_B.getPackageName());
+            revokePermission(TEST_APP_B.getPackageName(),
+                    Manifest.permission.READ_EXTERNAL_STORAGE);
             // TEST_APP_B without storage permission should see TEST_DIRECTORY in DCIM and should
             // not see new file in new TEST_DIRECTORY.
             assertThat(listAs(TEST_APP_B, DCIM_DIR.getPath())).contains(TEST_DIRECTORY_NAME);
@@ -908,6 +906,175 @@
     }
 
     @Test
+    public void testReadStorageInvalidation() throws Exception {
+        testAppOpInvalidation(TEST_APP_C, new File(DCIM_DIR, "read_storage.jpg"),
+                Manifest.permission.READ_EXTERNAL_STORAGE,
+                AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, /* forWrite */ false);
+    }
+
+    @Test
+    public void testWriteStorageInvalidation() throws Exception {
+        testAppOpInvalidation(TEST_APP_C_LEGACY, new File(DCIM_DIR, "write_storage.jpg"),
+                Manifest.permission.WRITE_EXTERNAL_STORAGE,
+                AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE, /* forWrite */ true);
+    }
+
+    @Test
+    public void testManageStorageInvalidation() throws Exception {
+        testAppOpInvalidation(TEST_APP_C, new File(DOWNLOAD_DIR, "manage_storage.pdf"),
+                /* permission */ null, OPSTR_MANAGE_EXTERNAL_STORAGE, /* forWrite */ true);
+    }
+
+    @Test
+    public void testWriteImagesInvalidation() throws Exception {
+        testAppOpInvalidation(TEST_APP_C, new File(DCIM_DIR, "write_images.jpg"),
+                /* permission */ null, AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, /* forWrite */ true);
+    }
+
+    @Test
+    public void testWriteVideoInvalidation() throws Exception {
+        testAppOpInvalidation(TEST_APP_C, new File(DCIM_DIR, "write_video.mp4"),
+                /* permission */ null, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO, /* forWrite */ true);
+    }
+
+    @Test
+    public void testAccessMediaLocationInvalidation() throws Exception {
+        File imgFile = new File(DCIM_DIR, "access_media_location.jpg");
+
+        try {
+            // Setup image with sensitive data on external storage
+            HashMap<String, String> originalExif = getExifMetadataFromRawResource(
+                    R.raw.img_with_metadata);
+            try (InputStream in = getContext().getResources().openRawResource(
+                    R.raw.img_with_metadata);
+                 OutputStream out = new FileOutputStream(imgFile)) {
+                // Dump the image we have to external storage
+                FileUtils.copy(in, out);
+            }
+            HashMap<String, String> exif = getExifMetadata(imgFile);
+            assertExifMetadataMatch(exif, originalExif);
+
+            // Install test app
+            installApp(TEST_APP_C, /* grantStoragePermissions */ true);
+
+            // Grant A_M_L and verify access to sensitive data
+            grantPermission(TEST_APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+            HashMap<String, String> exifFromTestApp = readExifMetadataFromTestApp(TEST_APP_C,
+                    imgFile.getPath());
+            assertExifMetadataMatch(exifFromTestApp, originalExif);
+
+            // Revoke A_M_L and verify sensitive data redaction
+            revokePermission(TEST_APP_C.getPackageName(),
+                    Manifest.permission.ACCESS_MEDIA_LOCATION);
+            exifFromTestApp = readExifMetadataFromTestApp(TEST_APP_C,
+                    imgFile.getPath());
+            assertExifMetadataMismatch(exifFromTestApp, originalExif);
+
+            // Re-grant A_M_L and verify access to sensitive data
+            grantPermission(TEST_APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+            exifFromTestApp = readExifMetadataFromTestApp(TEST_APP_C,
+                    imgFile.getPath());
+            assertExifMetadataMatch(exifFromTestApp, originalExif);
+        } finally {
+            imgFile.delete();
+            uninstallAppNoThrow(TEST_APP_C);
+        }
+    }
+
+    @Test
+    public void testAppUpdateInvalidation() throws Exception {
+        File file = new File(DCIM_DIR, "app_update.jpg");
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            // Install legacy
+            installApp(TEST_APP_C_LEGACY, /* grantStoragePermissions */ true);
+            grantPermission(TEST_APP_C_LEGACY.getPackageName(),
+                    Manifest.permission.WRITE_EXTERNAL_STORAGE); // Grants write access for legacy
+            // Legacy app can read and write media files contributed by others
+            assertThat(openFileAs(TEST_APP_C_LEGACY, file.getPath(), /* forWrite */ false))
+                    .isTrue();
+            assertThat(openFileAs(TEST_APP_C_LEGACY, file.getPath(), /* forWrite */ true)).isTrue();
+
+            // Update to non-legacy
+            installApp(TEST_APP_C, /* grantStoragePermissions */ true);
+            grantPermission(TEST_APP_C_LEGACY.getPackageName(),
+                    Manifest.permission.WRITE_EXTERNAL_STORAGE); // No effect for non-legacy
+            // Non-legacy app can read media files contributed by others
+            assertThat(openFileAs(TEST_APP_C, file.getPath(), /* forWrite */ false)).isTrue();
+            // But cannot write
+            assertThat(openFileAs(TEST_APP_C, file.getPath(), /* forWrite */ true)).isFalse();
+        } finally {
+            file.delete();
+            uninstallAppNoThrow(TEST_APP_C);
+        }
+    }
+
+    @Test
+    public void testAppReinstallInvalidation() throws Exception {
+        File file = new File(DCIM_DIR, "app_reinstall.jpg");
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            // Install
+            installApp(TEST_APP_C, /* grantStoragePermissions */ true);
+            assertThat(openFileAs(TEST_APP_C, file.getPath(), /* forWrite */ false)).isTrue();
+
+            // Re-install
+            uninstallAppNoThrow(TEST_APP_C);
+            installApp(TEST_APP_C, /* grantStoragePermissions */ false);
+            assertThat(openFileAs(TEST_APP_C, file.getPath(), /* forWrite */ false)).isFalse();
+        } finally {
+            file.delete();
+            uninstallAppNoThrow(TEST_APP_C);
+        }
+    }
+
+    private void testAppOpInvalidation(TestApp app, File file, @Nullable String permission,
+            String opstr, boolean forWrite) throws Exception {
+        try {
+            installApp(app, false);
+            assertThat(file.createNewFile()).isTrue();
+            assertAppOpInvalidation(app, file, permission, opstr, forWrite);
+        } finally {
+            file.delete();
+            uninstallApp(app);
+        }
+    }
+
+    /** If {@code permission} is null, appops are flipped, otherwise permissions are flipped */
+    private void assertAppOpInvalidation(TestApp app, File file, @Nullable String permission,
+            String opstr, boolean forWrite) throws Exception {
+        String packageName = app.getPackageName();
+        int uid = getContext().getPackageManager().getPackageUid(packageName, 0);
+
+        // Deny
+        if (permission != null) {
+            revokePermission(packageName, permission);
+        } else {
+            denyAppOpsToUid(uid, opstr);
+        }
+        assertThat(openFileAs(app, file.getPath(), forWrite)).isFalse();
+
+        // Grant
+        if (permission != null) {
+            grantPermission(packageName, permission);
+        } else {
+            allowAppOpsToUid(uid, opstr);
+        }
+        assertThat(openFileAs(app, file.getPath(), forWrite)).isTrue();
+
+        // Deny
+        if (permission != null) {
+            revokePermission(packageName, permission);
+        } else {
+            denyAppOpsToUid(uid, opstr);
+        }
+        assertThat(openFileAs(app, file.getPath(), forWrite)).isFalse();
+    }
+
+    @Test
     public void testSystemGalleryAppHasFullAccessToImages() throws Exception {
         final File otherAppImageFile = new File(DCIM_DIR, "other_" + IMAGE_FILE_NAME);
         final File topLevelImageFile = new File(EXTERNAL_STORAGE_DIR, IMAGE_FILE_NAME);
@@ -961,16 +1128,9 @@
             assertThat(createFileAs(TEST_APP_A, otherAppAudioFile.getPath())).isTrue();
             assertThat(otherAppAudioFile.exists()).isTrue();
 
-            // Assert we can't write to the file
-            try (FileInputStream fis = new FileInputStream(otherAppAudioFile)) {
-                fail("Opening for read succeeded when it should have failed: " + otherAppAudioFile);
-            } catch (IOException expected) {}
-
-            // Assert we can't read from the file
-            try (FileOutputStream fos = new FileOutputStream(otherAppAudioFile)) {
-                fail("Opening for write succeeded when it should have failed: "
-                        + otherAppAudioFile);
-            } catch (IOException expected) {}
+            // Assert we can't access the file
+            assertThat(canOpen(otherAppAudioFile, /* forWrite */ false)).isFalse();
+            assertThat(canOpen(otherAppAudioFile, /* forWrite */ true)).isFalse();
 
             // Assert we can't delete the file
             assertThat(otherAppAudioFile.delete()).isFalse();